Compare commits
45 Commits
v3.0.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5db86ee15 | ||
|
|
14f450bd25 | ||
|
|
5a1da39def | ||
|
|
24d065c43a | ||
|
|
fd72ce5ce7 | ||
|
|
043b1a3377 | ||
|
|
512952f66d | ||
|
|
d9723e76ab | ||
|
|
212baa6674 | ||
|
|
1c76e0513a | ||
|
|
c8cc94cd3c | ||
|
|
20cca35157 | ||
|
|
81d27afadb | ||
|
|
6cb2f3031c | ||
|
|
f116ea1d43 | ||
|
|
6aa0674000 | ||
|
|
2b828624a0 | ||
|
|
e60ccb93fb | ||
|
|
aa244e8098 | ||
|
|
6f60f03433 | ||
|
|
b8a0eee92d | ||
|
|
1486ebbc87 | ||
|
|
063c759275 | ||
|
|
6e9ebaf3ee | ||
|
|
0e1d4e52e1 | ||
|
|
c0fb4b79bd | ||
|
|
ec32dd65c2 | ||
|
|
04fb339622 | ||
|
|
3a22c24cf4 | ||
|
|
cf2320480f | ||
|
|
9532680879 | ||
|
|
2a945ddbf5 | ||
|
|
58bb92134d | ||
|
|
f1a279a10a | ||
|
|
faf172a91d | ||
|
|
04633ba208 | ||
|
|
58459e692b | ||
|
|
894a0fa849 | ||
|
|
21c7d29c1d | ||
|
|
ba93c42943 | ||
|
|
5c7dd40751 | ||
|
|
acc7b8b2f7 | ||
|
|
8c90838f3b | ||
|
|
0b784d24f2 | ||
|
|
444fbe396a |
342
.opencode/command/remove-deadcode.md
Normal file
342
.opencode/command/remove-deadcode.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
description: Remove unused code from this project with ultrawork mode, LSP-verified safety, atomic commits
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
You are a dead code removal specialist. Execute the FULL dead code removal workflow using ultrawork mode.
|
||||
|
||||
Your core weapon: **LSP FindReferences**. If a symbol has ZERO external references, it's dead. Remove it.
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
1. **LSP is law.** Never guess. Always verify with `LspFindReferences` before removing ANYTHING.
|
||||
2. **One removal = one commit.** Every dead code removal gets its own atomic commit.
|
||||
3. **Test after every removal.** Run `bun test` after each. If it fails, REVERT and skip.
|
||||
4. **Leaf-first order.** Remove deepest unused symbols first, then work up the dependency chain. Removing a leaf may expose new dead code upstream.
|
||||
5. **Never remove entry points.** `src/index.ts`, `src/cli/index.ts`, test files, config files, and files in `packages/` are off-limits unless explicitly targeted.
|
||||
|
||||
---
|
||||
|
||||
## STEP 0: REGISTER TODO LIST (MANDATORY FIRST ACTION)
|
||||
|
||||
```
|
||||
TodoWrite([
|
||||
{"id": "scan", "content": "PHASE 1: Scan codebase for dead code candidates using LSP + explore agents", "status": "pending", "priority": "high"},
|
||||
{"id": "verify", "content": "PHASE 2: Verify each candidate with LspFindReferences - zero false positives", "status": "pending", "priority": "high"},
|
||||
{"id": "plan", "content": "PHASE 3: Plan removal order (leaf-first dependency order)", "status": "pending", "priority": "high"},
|
||||
{"id": "remove", "content": "PHASE 4: Remove dead code one-by-one (remove -> test -> commit loop)", "status": "pending", "priority": "high"},
|
||||
{"id": "final", "content": "PHASE 5: Final verification - full test suite + build + typecheck", "status": "pending", "priority": "high"}
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: SCAN FOR DEAD CODE CANDIDATES
|
||||
|
||||
**Mark scan as in_progress.**
|
||||
|
||||
### 1.1: Launch Parallel Explore Agents (ALL BACKGROUND)
|
||||
|
||||
Fire ALL simultaneously:
|
||||
|
||||
```
|
||||
// Agent 1: Find all exported symbols
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find ALL exported functions, classes, types, interfaces, and constants across src/.
|
||||
List each with: file path, line number, symbol name, export type (named/default).
|
||||
EXCLUDE: src/index.ts root exports, test files.
|
||||
Return as structured list.")
|
||||
|
||||
// Agent 2: Find potentially unused files
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find files in src/ that are NOT imported by any other file.
|
||||
Check import/require statements across the entire codebase.
|
||||
EXCLUDE: index.ts files, test files, entry points, config files, .md files.
|
||||
Return list of potentially orphaned files.")
|
||||
|
||||
// Agent 3: Find unused imports within files
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find unused imports across src/**/*.ts files.
|
||||
Look for import statements where the imported symbol is never referenced in the file body.
|
||||
Return: file path, line number, imported symbol name.")
|
||||
|
||||
// Agent 4: Find functions/variables only used in their own declaration
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find private/non-exported functions, variables, and types in src/**/*.ts that appear
|
||||
to have zero usage beyond their declaration. Return: file path, line number, symbol name.")
|
||||
```
|
||||
|
||||
### 1.2: Direct AST-Grep Scans (WHILE AGENTS RUN)
|
||||
|
||||
```typescript
|
||||
// Find unused imports pattern
|
||||
ast_grep_search(pattern="import { $NAME } from '$PATH'", lang="typescript", paths=["src/"])
|
||||
|
||||
// Find empty export objects
|
||||
ast_grep_search(pattern="export {}", lang="typescript", paths=["src/"])
|
||||
```
|
||||
|
||||
### 1.3: Collect All Results
|
||||
|
||||
Collect background agent results. Compile into a master candidate list:
|
||||
|
||||
```
|
||||
## DEAD CODE CANDIDATES
|
||||
|
||||
| # | File | Line | Symbol | Type | Confidence |
|
||||
|---|------|------|--------|------|------------|
|
||||
| 1 | src/foo.ts | 42 | unusedFunc | function | HIGH |
|
||||
| 2 | src/bar.ts | 10 | OldType | type | MEDIUM |
|
||||
```
|
||||
|
||||
**Mark scan as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: VERIFY WITH LSP (ZERO FALSE POSITIVES)
|
||||
|
||||
**Mark verify as in_progress.**
|
||||
|
||||
For EVERY candidate from Phase 1, run this verification:
|
||||
|
||||
### 2.1: The LSP Verification Protocol
|
||||
|
||||
For each candidate symbol:
|
||||
|
||||
```typescript
|
||||
// Step 1: Find the symbol's exact position
|
||||
LspDocumentSymbols(filePath) // Get line/character of the symbol
|
||||
|
||||
// Step 2: Find ALL references across the ENTIRE workspace
|
||||
LspFindReferences(filePath, line, character, includeDeclaration=false)
|
||||
// includeDeclaration=false → only counts USAGES, not the definition itself
|
||||
|
||||
// Step 3: Evaluate
|
||||
// 0 references → CONFIRMED DEAD CODE
|
||||
// 1+ references → NOT dead, remove from candidate list
|
||||
```
|
||||
|
||||
### 2.2: False Positive Guards
|
||||
|
||||
**NEVER mark as dead code if:**
|
||||
- Symbol is in `src/index.ts` (package entry point)
|
||||
- Symbol is in any `index.ts` that re-exports (barrel file check: look if it's re-exported)
|
||||
- Symbol is referenced in test files (tests are valid consumers)
|
||||
- Symbol has `@public` or `@api` JSDoc tags
|
||||
- Symbol is in a file listed in `package.json` exports
|
||||
- Symbol is a hook factory (`createXXXHook`) registered in `src/index.ts`
|
||||
- Symbol is a tool factory (`createXXXTool`) registered in tool loading
|
||||
- Symbol is an agent definition registered in `agentSources`
|
||||
- File is a command template, skill definition, or MCP config
|
||||
|
||||
### 2.3: Build Confirmed Dead Code List
|
||||
|
||||
After verification, produce:
|
||||
|
||||
```
|
||||
## CONFIRMED DEAD CODE (LSP-verified, 0 external references)
|
||||
|
||||
| # | File | Line | Symbol | Type | Safe to Remove |
|
||||
|---|------|------|--------|------|----------------|
|
||||
| 1 | src/foo.ts | 42 | unusedFunc | function | YES |
|
||||
```
|
||||
|
||||
**If ZERO confirmed dead code found: Report "No dead code found" and STOP.**
|
||||
|
||||
**Mark verify as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: PLAN REMOVAL ORDER
|
||||
|
||||
**Mark plan as in_progress.**
|
||||
|
||||
### 3.1: Dependency Analysis
|
||||
|
||||
For each confirmed dead symbol:
|
||||
1. Check if removing it would expose other dead code
|
||||
2. Check if other dead symbols depend on this one
|
||||
3. Build removal dependency graph
|
||||
|
||||
### 3.2: Order by Leaf-First
|
||||
|
||||
```
|
||||
Removal Order:
|
||||
1. [Leaf symbols - no other dead code depends on them]
|
||||
2. [Intermediate symbols - depended on only by already-removed dead code]
|
||||
3. [Dead files - entire files with no live exports]
|
||||
```
|
||||
|
||||
### 3.3: Register Granular Todos
|
||||
|
||||
Create one todo per removal:
|
||||
|
||||
```
|
||||
TodoWrite([
|
||||
{"id": "remove-1", "content": "Remove unusedFunc from src/foo.ts:42", "status": "pending", "priority": "high"},
|
||||
{"id": "remove-2", "content": "Remove OldType from src/bar.ts:10", "status": "pending", "priority": "high"},
|
||||
// ... one per confirmed dead symbol
|
||||
])
|
||||
```
|
||||
|
||||
**Mark plan as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: ITERATIVE REMOVAL LOOP
|
||||
|
||||
**Mark remove as in_progress.**
|
||||
|
||||
For EACH dead code item, execute this exact loop:
|
||||
|
||||
### 4.1: Pre-Removal Check
|
||||
|
||||
```typescript
|
||||
// Re-verify it's still dead (previous removals may have changed things)
|
||||
LspFindReferences(filePath, line, character, includeDeclaration=false)
|
||||
// If references > 0 now → SKIP (previous removal exposed a new consumer)
|
||||
```
|
||||
|
||||
### 4.2: Remove the Dead Code
|
||||
|
||||
Use appropriate tool:
|
||||
|
||||
**For unused imports:**
|
||||
```typescript
|
||||
Edit(filePath, oldString="import { deadSymbol } from '...';\n", newString="")
|
||||
// Or if it's one of many imports, remove just the symbol from the import list
|
||||
```
|
||||
|
||||
**For unused functions/classes/types:**
|
||||
```typescript
|
||||
// Read the full symbol extent first
|
||||
Read(filePath, offset=startLine, limit=endLine-startLine+1)
|
||||
// Then remove it
|
||||
Edit(filePath, oldString="[full symbol text]", newString="")
|
||||
```
|
||||
|
||||
**For dead files:**
|
||||
```bash
|
||||
# Only after confirming ZERO imports point to this file
|
||||
rm "path/to/dead-file.ts"
|
||||
```
|
||||
|
||||
**After removal, also clean up:**
|
||||
- Remove any imports that were ONLY used by the removed code
|
||||
- Remove any now-empty import statements
|
||||
- Fix any trailing whitespace / double blank lines left behind
|
||||
|
||||
### 4.3: Post-Removal Verification
|
||||
|
||||
```typescript
|
||||
// 1. LSP diagnostics on changed file
|
||||
LspDiagnostics(filePath, severity="error")
|
||||
// Must be clean (or only pre-existing errors)
|
||||
|
||||
// 2. Run tests
|
||||
bash("bun test")
|
||||
// Must pass
|
||||
|
||||
// 3. Typecheck
|
||||
bash("bun run typecheck")
|
||||
// Must pass
|
||||
```
|
||||
|
||||
### 4.4: Handle Failures
|
||||
|
||||
If ANY verification fails:
|
||||
1. **REVERT** the change immediately (`git checkout -- [file]`)
|
||||
2. Mark this removal todo as `cancelled` with note: "Removal caused [error]. Skipped."
|
||||
3. Proceed to next item
|
||||
|
||||
### 4.5: Commit
|
||||
|
||||
```bash
|
||||
git add [changed-files]
|
||||
git commit -m "refactor: remove unused [symbolType] [symbolName] from [filePath]"
|
||||
```
|
||||
|
||||
Mark this removal todo as `completed`.
|
||||
|
||||
### 4.6: Re-scan After Removal
|
||||
|
||||
After removing a symbol, check if its removal exposed NEW dead code:
|
||||
- Were there imports that only existed to serve the removed symbol?
|
||||
- Are there other symbols in the same file now unreferenced?
|
||||
|
||||
If new dead code is found, add it to the removal queue.
|
||||
|
||||
**Repeat 4.1-4.6 for every item. Mark remove as completed when done.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: FINAL VERIFICATION
|
||||
|
||||
**Mark final as in_progress.**
|
||||
|
||||
### 5.1: Full Test Suite
|
||||
```bash
|
||||
bun test
|
||||
```
|
||||
|
||||
### 5.2: Full Typecheck
|
||||
```bash
|
||||
bun run typecheck
|
||||
```
|
||||
|
||||
### 5.3: Full Build
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
### 5.4: Summary Report
|
||||
|
||||
```markdown
|
||||
## Dead Code Removal Complete
|
||||
|
||||
### Removed
|
||||
| # | Symbol | File | Type | Commit |
|
||||
|---|--------|------|------|--------|
|
||||
| 1 | unusedFunc | src/foo.ts | function | abc1234 |
|
||||
|
||||
### Skipped (caused failures)
|
||||
| # | Symbol | File | Reason |
|
||||
|---|--------|------|--------|
|
||||
| 1 | riskyFunc | src/bar.ts | Test failure: [details] |
|
||||
|
||||
### Verification
|
||||
- Tests: PASSED (X/Y passing)
|
||||
- Typecheck: CLEAN
|
||||
- Build: SUCCESS
|
||||
- Total dead code removed: N symbols across M files
|
||||
- Total commits: K atomic commits
|
||||
```
|
||||
|
||||
**Mark final as completed.**
|
||||
|
||||
---
|
||||
|
||||
## SCOPE CONTROL
|
||||
|
||||
**If $ARGUMENTS is provided**, narrow the scan to the specified scope:
|
||||
- File path: Only scan that file
|
||||
- Directory: Only scan that directory
|
||||
- Symbol name: Only check that specific symbol
|
||||
- "all" or empty: Full project scan (default)
|
||||
|
||||
## ABORT CONDITIONS
|
||||
|
||||
**STOP and report to user if:**
|
||||
- 3 consecutive removals cause test failures
|
||||
- Build breaks and cannot be fixed by reverting
|
||||
- More than 50 candidates found (ask user to narrow scope)
|
||||
|
||||
## LANGUAGE
|
||||
|
||||
Use English for commit messages and technical output.
|
||||
|
||||
</command-instruction>
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
23
AGENTS.md
23
AGENTS.md
@@ -1,7 +1,7 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-01-23T15:59:00+09:00
|
||||
**Commit:** 599fad0e
|
||||
**Generated:** 2026-01-25T13:10:00+09:00
|
||||
**Commit:** 043b1a33
|
||||
**Branch:** dev
|
||||
|
||||
## OVERVIEW
|
||||
@@ -21,7 +21,7 @@ oh-my-opencode/
|
||||
│ ├── cli/ # CLI installer, doctor - see src/cli/AGENTS.md
|
||||
│ ├── mcp/ # Built-in MCPs - see src/mcp/AGENTS.md
|
||||
│ ├── config/ # Zod schema, TypeScript types
|
||||
│ └── index.ts # Main plugin entry (593 lines)
|
||||
│ └── index.ts # Main plugin entry (601 lines)
|
||||
├── script/ # build-schema.ts, build-binaries.ts
|
||||
├── packages/ # 7 platform-specific binaries
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
@@ -36,6 +36,7 @@ oh-my-opencode/
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
|
||||
| Add MCP | `src/mcp/` | Create config, add to index.ts |
|
||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||
| Add command | `src/features/builtin-commands/` | Add template + register in commands.ts |
|
||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1335 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (773 lines) |
|
||||
@@ -60,7 +61,7 @@ oh-my-opencode/
|
||||
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||
- **Exports**: Barrel pattern via index.ts
|
||||
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
|
||||
- **Testing**: BDD comments, 90 test files
|
||||
- **Testing**: BDD comments, 95 test files
|
||||
- **Temperature**: 0.1 for code agents, max 0.3
|
||||
|
||||
## ANTI-PATTERNS
|
||||
@@ -88,9 +89,9 @@ oh-my-opencode/
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator |
|
||||
| Atlas | anthropic/claude-opus-4-5 | Master orchestrator |
|
||||
| oracle | openai/gpt-5.2 | Consultation, debugging |
|
||||
| librarian | opencode/glm-4.7-free | Docs, GitHub search |
|
||||
| explore | opencode/grok-code | Fast codebase grep |
|
||||
| multimodal-looker | google/gemini-3-flash-preview | PDF/image analysis |
|
||||
| librarian | opencode/big-pickle | Docs, GitHub search |
|
||||
| explore | opencode/gpt-5-nano | Fast codebase grep |
|
||||
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning |
|
||||
|
||||
## COMMANDS
|
||||
@@ -99,7 +100,7 @@ oh-my-opencode/
|
||||
bun run typecheck # Type check
|
||||
bun run build # ESM + declarations + schema
|
||||
bun run rebuild # Clean + Build
|
||||
bun test # 90 test files
|
||||
bun test # 95 test files
|
||||
```
|
||||
|
||||
## DEPLOYMENT
|
||||
@@ -118,7 +119,11 @@ bun test # 90 test files
|
||||
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent |
|
||||
| `src/tools/delegate-task/tools.ts` | 1039 | Category-based delegation |
|
||||
| `src/hooks/atlas/index.ts` | 773 | Orchestrator hook |
|
||||
| `src/cli/config-manager.ts` | 641 | JSONC config parsing |
|
||||
| `src/cli/config-manager.ts` | 664 | JSONC config parsing |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
||||
| `src/index.ts` | 601 | Main plugin entry |
|
||||
| `src/tools/lsp/client.ts` | 596 | LSP JSON-RPC client |
|
||||
| `src/agents/atlas.ts` | 572 | Atlas orchestrator agent |
|
||||
|
||||
## MCP ARCHITECTURE
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **オーケストレーターがベータ版で利用可能になりました。`oh-my-opencode@3.0.0-beta.10`を使用してインストールしてください。**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0が正式リリースされました!`oh-my-opencode@latest`を使用してインストールしてください。**
|
||||
>
|
||||
> 一緒に歩みましょう!
|
||||
>
|
||||
@@ -73,7 +73,9 @@
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
>
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **오케스트레이터가 베타 버전으로 사용 가능합니다. 설치하려면 `oh-my-opencode@3.0.0-beta.10`을 사용하세요.**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0이 정식 출시되었습니다! `oh-my-opencode@latest`를 사용하여 설치하세요.**
|
||||
>
|
||||
> 함께해요!
|
||||
>
|
||||
@@ -73,10 +73,11 @@
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/stargazers)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **The Orchestrator is now available in beta. Use `oh-my-opencode@3.0.0-beta.10` to install it.**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0 is now stable! Use `oh-my-opencode@latest` to install it.**
|
||||
>
|
||||
> Be with us!
|
||||
>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **Orchestrator 现已进入测试阶段。使用 `oh-my-opencode@3.0.0-beta.10` 安装。**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0 正式发布!使用 `oh-my-opencode@latest` 安装。**
|
||||
>
|
||||
> 加入我们!
|
||||
>
|
||||
@@ -74,7 +74,9 @@
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1787,7 +1787,8 @@
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
"high",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
|
||||
28
bun.lock
28
bun.lock
@@ -27,13 +27,13 @@
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.11",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0",
|
||||
"oh-my-opencode-linux-x64": "3.0.0",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0",
|
||||
"oh-my-opencode-windows-x64": "3.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -225,19 +225,19 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.0.0-beta.11", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7cFv2bbz9HTY7sshgVTu+IhvYf7CT0czDYqHEB+dYfEqFU6TaoSMimq6uHqcWegUUR1T7PNmc0dyjYVw69FeVA=="],
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.0.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-zelvb7qz5GsS+Dhyz9rACZrkUMtWbAZGijiHSQqmRcjlN/sRPNhXtsL55VheDjlPM3VP+t3+psv+se0WA/aw5w=="],
|
||||
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.0.0-beta.11", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-rGAbDdUySWITIdm2yiuNFB9lFYaSXT8LMtg97LTlOO5vZbI3M+obIS3QlIkBtAhgOTIPB7Ni+T0W44OmJpHoYA=="],
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.0.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-dRMD1U5zIrb6BsiKQJZtAFtuD8clAQquZyU2LajMoFTHBNhcBDIgsaBBwvMBIq7dTe8rnFq91ExiFA8OfdrzBA=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.0.0-beta.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-F9dqwWwGAdqeSkE7Tre5DmHQXwDpU2Z8Jk0lwTJMLj+kMqYFDVPjLPo4iVUdwPpxpmm0pR84u/oonG/2+84/zw=="],
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.0.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Wx6Cx2Nu2T69mfZa3FQ3gk0OFONvMh48rMVYK0Cp8VX5W4Zb/GZgTUFmZlYsApyxqP+7J9m18skd46qPOhzuEQ=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.0.0-beta.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-H+zOtHkHd+TmdPj64M1A0zLOk7OHIK4C8yqfLFhfizOIBffT1yOhAs6EpK3EqPhfPLu54ADgcQcu8W96VP24UA=="],
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.0.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-mfOlptgLoXLVuhFRcXgZU7BYGuL1axZOMOOjONgncNzOp/BQYU5B9BRFihBUXdDsWGmeMiLowrYGBhVpSv3NlA=="],
|
||||
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.0.0-beta.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-IG+KODTJ8rs6cEJ2wN6Zpr6YtvCS5OpYP6jBdGJltmUpjQdMhdMsaY3ysZk+9Vxpx2KC3xj5KLHV1USg3uBTeg=="],
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vVjshfaz0UC9NrGD9FfjlYK5NvckIW0sZaE/wRv/LKjrukHFH1jJpJa5KKXxBWLsEJjt6ooJRguXXxtfNXpAWw=="],
|
||||
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.0.0-beta.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-irV+AuWrHqNm7VT7HO56qgymR0+vEfJbtB3vCq68kprH2V4NQmGp2MNKIYPnUCYL7NEK3H2NX+h06YFZJ/8ELQ=="],
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.0.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-N6cNJ7+Dj0a5dWqPf6OKfB39o8HWw5HQ3hB4omgYqc6Gzo6nChA4KIiVefEC3+tIL98x4XvMeD7OU+UYgwxHnQ=="],
|
||||
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.0.0-beta.11", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-exZ/NEwGBlxyWszN7dvOfzbYX0cuhBZXftqAAFOlVP26elDHdo+AmSmLR/4cJyzpR9nCWz4xvl/RYF84bY6OEA=="],
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.0.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-TaC0hiHpnsS42GWTVUKoTwCb+QzNLBlQtTkIQ0PjlkDYFjlEC2LuR2FFcscik055PRRIGishyB9A1n/8XAgcvA=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ A Category is an agent configuration preset optimized for specific domains.
|
||||
|
||||
| Category | Default Model | Use Cases |
|
||||
|----------|---------------|-----------|
|
||||
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
|
||||
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
||||
| `artistry` | `google/gemini-3-pro-preview` (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `artistry` | `google/gemini-3-pro` (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
|
||||
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | `google/gemini-3-flash-preview` | Documentation, prose, technical writing |
|
||||
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -70,12 +70,12 @@ A Skill is a mechanism that injects **specialized knowledge (Context)** and **to
|
||||
|
||||
### Usage
|
||||
|
||||
Add desired skill names to the `skills` array.
|
||||
Add desired skill names to the `load_skills` array.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="quick",
|
||||
skills=["git-master"],
|
||||
load_skills=["git-master"],
|
||||
prompt="Commit current changes. Follow commit message style."
|
||||
)
|
||||
```
|
||||
@@ -110,17 +110,17 @@ You can create powerful specialized agents by combining Categories and Skills.
|
||||
|
||||
### 🎨 The Designer (UI Implementation)
|
||||
- **Category**: `visual-engineering`
|
||||
- **Skills**: `["frontend-ui-ux", "playwright"]`
|
||||
- **load_skills**: `["frontend-ui-ux", "playwright"]`
|
||||
- **Effect**: Implements aesthetic UI and verifies rendering results directly in browser.
|
||||
|
||||
### 🏗️ The Architect (Design Review)
|
||||
- **Category**: `ultrabrain`
|
||||
- **Skills**: `[]` (pure reasoning)
|
||||
- **load_skills**: `[]` (pure reasoning)
|
||||
- **Effect**: Leverages GPT-5.2's logical reasoning for in-depth system architecture analysis.
|
||||
|
||||
### ⚡ The Maintainer (Quick Fixes)
|
||||
- **Category**: `quick`
|
||||
- **Skills**: `["git-master"]`
|
||||
- **load_skills**: `["git-master"]`
|
||||
- **Effect**: Uses cost-effective models to quickly fix code and generate clean commits.
|
||||
|
||||
---
|
||||
@@ -131,7 +131,7 @@ When delegating, **clear and specific** prompts are essential. Include these 7 e
|
||||
|
||||
1. **TASK**: What needs to be done? (single objective)
|
||||
2. **EXPECTED OUTCOME**: What is the deliverable?
|
||||
3. **REQUIRED SKILLS**: Which skills should be used?
|
||||
3. **REQUIRED SKILLS**: Which skills should be loaded via `load_skills`?
|
||||
4. **REQUIRED TOOLS**: Which tools must be used? (whitelist)
|
||||
5. **MUST DO**: What must be done (constraints)
|
||||
6. **MUST NOT DO**: What must never be done
|
||||
@@ -177,7 +177,7 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
||||
"categories": {
|
||||
// 1. Define new custom category
|
||||
"korean-writer": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
"temperature": 0.5,
|
||||
"prompt_append": "You are a Korean technical writer. Maintain a friendly and clear tone."
|
||||
},
|
||||
|
||||
@@ -175,7 +175,7 @@ Configuration files support **JSONC (JSON with Comments)** format. You can use c
|
||||
/* Category customization */
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ It asks about your providers (Claude, OpenAI, Gemini, etc.) and generates optima
|
||||
"agents": {
|
||||
"oracle": { "model": "openai/gpt-5.2" }, // Use GPT for debugging
|
||||
"librarian": { "model": "zai-coding-plan/glm-4.7" }, // Cheap model for research
|
||||
"explore": { "model": "opencode/grok-code" } // Free model for grep
|
||||
"explore": { "model": "opencode/gpt-5-nano" } // Free model for grep
|
||||
},
|
||||
|
||||
// Override category models (used by delegate_task)
|
||||
"categories": {
|
||||
"quick": { "model": "opencode/grok-code" }, // Fast/cheap for trivial tasks
|
||||
"visual-engineering": { "model": "google/gemini-3-pro-preview" } // Gemini for UI
|
||||
"quick": { "model": "opencode/gpt-5-nano" }, // Fast/cheap for trivial tasks
|
||||
"visual-engineering": { "model": "google/gemini-3-pro" } // Gemini for UI
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -75,7 +75,7 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
"model": "openai/gpt-5.2" // GPT for strategic reasoning
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code" // Free & fast for exploration
|
||||
"model": "opencode/gpt-5-nano" // Free & fast for exploration
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -83,7 +83,7 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
|
||||
## Google Auth
|
||||
|
||||
**Recommended**: For Google Gemini authentication, install the [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin. It provides multi-account load balancing, more models (including Claude via Antigravity), and active maintenance. See [Installation > Google Gemini](../README.md#google-gemini-antigravity-oauth).
|
||||
**Recommended**: For Google Gemini authentication, install the [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin (`@latest`). It provides multi-account load balancing, variant-based thinking levels, dual quota system (Antigravity + Gemini CLI), and active maintenance. See [Installation > Google Gemini](docs/guide/installation.md#google-gemini-antigravity-oauth).
|
||||
|
||||
## Agents
|
||||
|
||||
@@ -160,7 +160,7 @@ Available agents: `oracle`, `librarian`, `explore`, `multimodal-looker`
|
||||
Oh My OpenCode includes built-in skills that provide additional capabilities:
|
||||
|
||||
- **playwright**: Browser automation with Playwright MCP. Use for web scraping, testing, screenshots, and browser interactions.
|
||||
- **git-master**: Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with `delegate_task(category='quick', skills=['git-master'], ...)` to save context.
|
||||
- **git-master**: Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with `delegate_task(category='quick', load_skills=['git-master'], ...)` to save context.
|
||||
|
||||
Disable built-in skills via `disabled_skills` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
@@ -305,7 +305,7 @@ Categories enable domain-specific task delegation via the `delegate_task` tool.
|
||||
|
||||
| Category | Model | Description |
|
||||
| ---------------- | ----------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `visual` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design-focused tasks. High creativity (temp 0.7). |
|
||||
| `visual` | `google/gemini-3-pro` | Frontend, UI/UX, design-focused tasks. High creativity (temp 0.7). |
|
||||
| `business-logic` | `openai/gpt-5.2` | Backend logic, architecture, strategic reasoning. Low creativity (temp 0.1). |
|
||||
|
||||
**Usage:**
|
||||
@@ -332,7 +332,7 @@ Add custom categories in `oh-my-opencode.json`:
|
||||
"prompt_append": "Focus on data analysis, ML pipelines, and statistical methods."
|
||||
},
|
||||
"visual": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"prompt_append": "Use shadcn/ui components and Tailwind CSS."
|
||||
}
|
||||
}
|
||||
@@ -403,9 +403,9 @@ Each agent has a defined provider priority chain. The system tries providers in
|
||||
|-------|-------------------|-------------------------|
|
||||
| **Sisyphus** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **oracle** | `gpt-5.2` | openai → anthropic → google → github-copilot → opencode |
|
||||
| **librarian** | `glm-4.7-free` | opencode → github-copilot → anthropic |
|
||||
| **explore** | `grok-code` | opencode → anthropic → github-copilot |
|
||||
| **multimodal-looker** | `gemini-3-flash-preview` | google → anthropic → zai → openai → github-copilot → opencode |
|
||||
| **librarian** | `big-pickle` | opencode → github-copilot → anthropic |
|
||||
| **explore** | `gpt-5-nano` | anthropic → opencode |
|
||||
| **multimodal-looker** | `gemini-3-flash` | google → openai → zai-coding-plan → anthropic → opencode |
|
||||
| **Prometheus (Planner)** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **Metis (Plan Consultant)** | `claude-sonnet-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **Momus (Plan Reviewer)** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
@@ -417,13 +417,13 @@ Categories follow the same resolution logic:
|
||||
|
||||
| Category | Model (no prefix) | Provider Priority Chain |
|
||||
|----------|-------------------|-------------------------|
|
||||
| **visual-engineering** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **visual-engineering** | `gemini-3-pro` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **ultrabrain** | `gpt-5.2-codex` | openai → anthropic → google → github-copilot → opencode |
|
||||
| **artistry** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **artistry** | `gemini-3-pro` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **quick** | `claude-haiku-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **unspecified-low** | `claude-sonnet-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **unspecified-high** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **writing** | `gemini-3-flash-preview` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **writing** | `gemini-3-flash` | google → openai → anthropic → github-copilot → opencode |
|
||||
|
||||
### Checking Your Configuration
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ Oh-My-OpenCode provides 10 specialized AI agents. Each has distinct expertise, o
|
||||
|-------|-------|---------|
|
||||
| **Sisyphus** | `anthropic/claude-opus-4-5` | **The default orchestrator.** Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). |
|
||||
| **oracle** | `openai/gpt-5.2` | Architecture decisions, code review, debugging. Read-only consultation - stellar logical reasoning and deep analysis. Inspired by AmpCode. |
|
||||
| **librarian** | `opencode/glm-4.7-free` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Inspired by AmpCode. |
|
||||
| **explore** | `opencode/grok-code` | Fast codebase exploration and contextual grep. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code. |
|
||||
| **multimodal-looker** | `google/gemini-3-flash-preview` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Saves tokens by having another agent process media. |
|
||||
| **librarian** | `opencode/big-pickle` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Inspired by AmpCode. |
|
||||
| **explore** | `opencode/gpt-5-nano` | Fast codebase exploration and contextual grep. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code. |
|
||||
| **multimodal-looker** | `google/gemini-3-flash` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Saves tokens by having another agent process media. |
|
||||
|
||||
### Planning Agents
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-antigravity-auth@1.2.8"
|
||||
"opencode-antigravity-auth@latest"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -140,7 +140,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
##### Model Configuration
|
||||
|
||||
You'll also need full model settings in `opencode.json`.
|
||||
Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/opencode-antigravity-auth), copy provider/models config from the README, and merge carefully to avoid breaking the user's existing setup.
|
||||
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-opencode Agent Model Override
|
||||
|
||||
@@ -154,7 +154,17 @@ The `opencode-antigravity-auth` plugin uses different model names than the built
|
||||
}
|
||||
```
|
||||
|
||||
**Available model names**: `google/antigravity-gemini-3-pro-high`, `google/antigravity-gemini-3-pro-low`, `google/antigravity-gemini-3-flash`, `google/antigravity-claude-sonnet-4-5`, `google/antigravity-claude-sonnet-4-5-thinking-low`, `google/antigravity-claude-sonnet-4-5-thinking-medium`, `google/antigravity-claude-sonnet-4-5-thinking-high`, `google/antigravity-claude-opus-4-5-thinking-low`, `google/antigravity-claude-opus-4-5-thinking-medium`, `google/antigravity-claude-opus-4-5-thinking-high`, `google/gemini-3-pro-preview`, `google/gemini-3-flash-preview`, `google/gemini-2.5-pro`, `google/gemini-2.5-flash`
|
||||
**Available models (Antigravity quota)**:
|
||||
- `google/antigravity-gemini-3-pro` — variants: `low`, `high`
|
||||
- `google/antigravity-gemini-3-flash` — variants: `minimal`, `low`, `medium`, `high`
|
||||
- `google/antigravity-claude-sonnet-4-5` — no variants
|
||||
- `google/antigravity-claude-sonnet-4-5-thinking` — variants: `low`, `max`
|
||||
- `google/antigravity-claude-opus-4-5-thinking` — variants: `low`, `max`
|
||||
|
||||
**Available models (Gemini CLI quota)**:
|
||||
- `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.
|
||||
|
||||
Then authenticate:
|
||||
|
||||
@@ -183,7 +193,7 @@ When GitHub Copilot is the best available provider, oh-my-opencode uses these mo
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||
| **Explore** | `github-copilot/grok-code-fast-1`|
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `zai-coding-plan/glm-4.7` (if Z.ai available) or fallback |
|
||||
|
||||
GitHub Copilot acts as a proxy provider, routing requests to underlying models based on your subscription.
|
||||
@@ -203,7 +213,7 @@ If Z.ai is the only provider available, all agents will use GLM models:
|
||||
|
||||
#### OpenCode Zen
|
||||
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/grok-code`, and `opencode/glm-4.7-free`.
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/gpt-5-nano`, and `opencode/big-pickle`.
|
||||
|
||||
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
|
||||
|
||||
@@ -211,8 +221,8 @@ When OpenCode Zen is the best available provider (no native or Copilot), these m
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `opencode/claude-opus-4-5` |
|
||||
| **Oracle** | `opencode/gpt-5.2` |
|
||||
| **Explore** | `opencode/grok-code` |
|
||||
| **Librarian** | `opencode/glm-4.7-free` |
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `opencode/big-pickle` |
|
||||
|
||||
##### Setup
|
||||
|
||||
|
||||
@@ -112,12 +112,12 @@ Each agent has a **provider priority chain**. The system tries providers in orde
|
||||
|
||||
```
|
||||
Example: multimodal-looker
|
||||
google → anthropic → zai → openai → github-copilot → opencode
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
gemini haiku glm-4.6v gpt-5.2 fallback fallback
|
||||
google → openai → zai-coding-plan → anthropic → opencode
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
gemini gpt-5.2 glm-4.6v haiku gpt-5-nano
|
||||
```
|
||||
|
||||
If you have Gemini, it uses `google/gemini-3-flash-preview`. No Gemini but have Claude? Uses `anthropic/claude-haiku-4-5`. And so on.
|
||||
If you have Gemini, it uses `google/gemini-3-flash`. No Gemini but have Claude? Uses `anthropic/claude-haiku-4-5`. And so on.
|
||||
|
||||
### Example Configuration
|
||||
|
||||
@@ -128,14 +128,14 @@ Here's a real-world config for a user with **Claude, OpenAI, Gemini, and Z.ai**
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
// Override specific agents only - rest use fallback chain
|
||||
"Atlas": { "model": "anthropic/claude-sonnet-4-5", "variant": "max" },
|
||||
"atlas": { "model": "anthropic/claude-sonnet-4-5", "variant": "max" },
|
||||
"librarian": { "model": "zai-coding-plan/glm-4.7" },
|
||||
"explore": { "model": "opencode/grok-code" },
|
||||
"explore": { "model": "opencode/gpt-5-nano" },
|
||||
"multimodal-looker": { "model": "zai-coding-plan/glm-4.6v" }
|
||||
},
|
||||
"categories": {
|
||||
// Override categories for cost optimization
|
||||
"quick": { "model": "opencode/grok-code" },
|
||||
"quick": { "model": "opencode/gpt-5-nano" },
|
||||
"unspecified-low": { "model": "zai-coding-plan/glm-4.7" }
|
||||
},
|
||||
"experimental": {
|
||||
|
||||
@@ -326,13 +326,13 @@ Skills prepend specialized instructions to subagent prompts:
|
||||
// Category + Skill combination
|
||||
delegate_task(
|
||||
category="visual-engineering",
|
||||
skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
load_skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
prompt="..."
|
||||
)
|
||||
|
||||
delegate_task(
|
||||
category="general",
|
||||
skills=["playwright"], // Adds browser automation expertise
|
||||
load_skills=["playwright"], // Adds browser automation expertise
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -341,8 +341,8 @@ delegate_task(
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| Hardcoded: `frontend-ui-ux-engineer` (Gemini 3 Pro) | `category="visual-engineering" + skills=["frontend-ui-ux"]` |
|
||||
| One-size-fits-all | `category="visual-engineering" + skills=["unity-master"]` |
|
||||
| Hardcoded: `frontend-ui-ux-engineer` (Gemini 3 Pro) | `category="visual-engineering" + load_skills=["frontend-ui-ux"]` |
|
||||
| One-size-fits-all | `category="visual-engineering" + load_skills=["unity-master"]` |
|
||||
| Model bias | Category-based: model abstraction eliminates bias |
|
||||
|
||||
---
|
||||
@@ -365,7 +365,7 @@ sequenceDiagram
|
||||
|
||||
Note over Orchestrator: Prompt Structure:<br/>1. TASK (exact checkbox)<br/>2. EXPECTED OUTCOME<br/>3. REQUIRED SKILLS<br/>4. REQUIRED TOOLS<br/>5. MUST DO<br/>6. MUST NOT DO<br/>7. CONTEXT + Wisdom
|
||||
|
||||
Orchestrator->>Junior: delegate_task(category, skills, prompt)
|
||||
Orchestrator->>Junior: delegate_task(category, load_skills, prompt)
|
||||
|
||||
Junior->>Junior: Create todos, execute
|
||||
Junior->>Junior: Verify (lsp_diagnostics, tests)
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -73,13 +73,13 @@
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.15",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.15",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.15",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.15",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.15",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.15",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.15"
|
||||
"oh-my-opencode-darwin-arm64": "3.0.1",
|
||||
"oh-my-opencode-darwin-x64": "3.0.1",
|
||||
"oh-my-opencode-linux-arm64": "3.0.1",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.1",
|
||||
"oh-my-opencode-linux-x64": "3.0.1",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.1",
|
||||
"oh-my-opencode-windows-x64": "3.0.1"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64-musl",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -735,6 +735,86 @@
|
||||
"created_at": "2026-01-23T08:27:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 985
|
||||
},
|
||||
{
|
||||
"name": "RouHim",
|
||||
"id": 3582050,
|
||||
"comment_id": 3791988227,
|
||||
"created_at": "2026-01-23T19:32:01Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1031
|
||||
},
|
||||
{
|
||||
"name": "gongxh0901",
|
||||
"id": 15622561,
|
||||
"comment_id": 3793478620,
|
||||
"created_at": "2026-01-24T02:15:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1037
|
||||
},
|
||||
{
|
||||
"name": "gongxh0901",
|
||||
"id": 15622561,
|
||||
"comment_id": 3793521632,
|
||||
"created_at": "2026-01-24T02:23:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1037
|
||||
},
|
||||
{
|
||||
"name": "AndersHsueh",
|
||||
"id": 121805544,
|
||||
"comment_id": 3793787614,
|
||||
"created_at": "2026-01-24T04:41:46Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1042
|
||||
},
|
||||
{
|
||||
"name": "AamiRobin",
|
||||
"id": 22963668,
|
||||
"comment_id": 3794632200,
|
||||
"created_at": "2026-01-24T13:28:22Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1067
|
||||
},
|
||||
{
|
||||
"name": "ThanhNguyxn",
|
||||
"id": 74597207,
|
||||
"comment_id": 3795232176,
|
||||
"created_at": "2026-01-24T17:41:53Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1075
|
||||
},
|
||||
{
|
||||
"name": "sadnow",
|
||||
"id": 87896100,
|
||||
"comment_id": 3795495342,
|
||||
"created_at": "2026-01-24T20:49:29Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1080
|
||||
},
|
||||
{
|
||||
"name": "jsl9208",
|
||||
"id": 4048787,
|
||||
"comment_id": 3795582626,
|
||||
"created_at": "2026-01-24T21:41:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1082
|
||||
},
|
||||
{
|
||||
"name": "potb",
|
||||
"id": 10779093,
|
||||
"comment_id": 3795856573,
|
||||
"created_at": "2026-01-25T02:38:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1083
|
||||
},
|
||||
{
|
||||
"name": "kvokka",
|
||||
"id": 15954013,
|
||||
"comment_id": 3795884358,
|
||||
"created_at": "2026-01-25T03:13:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1084
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -239,7 +239,7 @@ Ask yourself:
|
||||
I will use delegate_task with:
|
||||
- **Category**: [selected-category-name]
|
||||
- **Why this category**: [how category description matches task domain]
|
||||
- **Skills**: [list of selected skills]
|
||||
- **load_skills**: [list of selected skills]
|
||||
- **Skill evaluation**:
|
||||
- [skill-1]: INCLUDED because [reason based on skill description]
|
||||
- [skill-2]: OMITTED because [reason why skill domain doesn't apply]
|
||||
@@ -256,7 +256,7 @@ I will use delegate_task with:
|
||||
I will use delegate_task with:
|
||||
- **Category**: [category-name]
|
||||
- **Why this category**: Category description says "[quote description]" which matches this task's requirements
|
||||
- **Skills**: ["skill-a", "skill-b"]
|
||||
- **load_skills**: ["skill-a", "skill-b"]
|
||||
- **Skill evaluation**:
|
||||
- skill-a: INCLUDED - description says "[quote]" which applies to this task
|
||||
- skill-b: INCLUDED - description says "[quote]" which is needed here
|
||||
@@ -265,7 +265,7 @@ I will use delegate_task with:
|
||||
|
||||
delegate_task(
|
||||
category="[category-name]",
|
||||
skills=["skill-a", "skill-b"],
|
||||
load_skills=["skill-a", "skill-b"],
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -276,12 +276,12 @@ delegate_task(
|
||||
I will use delegate_task with:
|
||||
- **Agent**: [agent-name]
|
||||
- **Reason**: This requires [agent's specialty] based on agent description
|
||||
- **Skills**: [] (agents have built-in expertise)
|
||||
- **load_skills**: [] (agents have built-in expertise)
|
||||
- **Expected Outcome**: [what agent should return]
|
||||
|
||||
delegate_task(
|
||||
subagent_type="[agent-name]",
|
||||
skills=[],
|
||||
load_skills=[],
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -292,13 +292,13 @@ delegate_task(
|
||||
I will use delegate_task with:
|
||||
- **Agent**: explore
|
||||
- **Reason**: Need to find all authentication implementations across the codebase - this is contextual grep
|
||||
- **Skills**: []
|
||||
- **load_skills**: []
|
||||
- **Expected Outcome**: List of files containing auth patterns
|
||||
|
||||
delegate_task(
|
||||
subagent_type="explore",
|
||||
run_in_background=true,
|
||||
skills=[],
|
||||
load_skills=[],
|
||||
prompt="Find all authentication implementations in the codebase"
|
||||
)
|
||||
```
|
||||
@@ -306,7 +306,7 @@ delegate_task(
|
||||
**WRONG: No Skill Evaluation**
|
||||
|
||||
```
|
||||
delegate_task(category="...", skills=[], prompt="...") // Where's the justification?
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Where's the justification?
|
||||
```
|
||||
|
||||
**WRONG: Vague Category Selection**
|
||||
@@ -329,11 +329,11 @@ I'll use this category because it seems right.
|
||||
```typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find error handling patterns here...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
@@ -416,7 +416,7 @@ Skills inject specialized instructions into the subagent. Read the description t
|
||||
For EVERY skill listed above, ask yourself:
|
||||
> "Does this skill's expertise domain overlap with my task?"
|
||||
|
||||
- If YES → INCLUDE in `skills=[...]`
|
||||
- If YES → INCLUDE in `load_skills=[...]`
|
||||
- If NO → You MUST justify why (see below)
|
||||
|
||||
**STEP 3: Justify Omissions**
|
||||
@@ -444,14 +444,14 @@ SKILL EVALUATION for "[skill-name]":
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="[selected-category]",
|
||||
skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
|
||||
**ANTI-PATTERN (will produce poor results):**
|
||||
```typescript
|
||||
delegate_task(category="...", skills=[], prompt="...") // Empty skills without justification
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Empty load_skills without justification
|
||||
```
|
||||
### Delegation Table:
|
||||
|
||||
@@ -724,7 +724,7 @@ If the user's approach seems problematic:
|
||||
| **Error Handling** | Empty catch blocks `catch(e) {}` |
|
||||
| **Testing** | Deleting failing tests to "pass" |
|
||||
| **Search** | Firing agents for single-line typos or obvious syntax errors |
|
||||
| **Delegation** | Using `skills=[]` without justifying why no skills apply |
|
||||
| **Delegation** | Using `load_skills=[]` without justifying why no skills apply |
|
||||
| **Debugging** | Shotgun debugging, random changes |
|
||||
## Soft Guidelines
|
||||
|
||||
|
||||
@@ -8,17 +8,17 @@
|
||||
|
||||
```
|
||||
agents/
|
||||
├── atlas.ts # Master Orchestrator (543 lines)
|
||||
├── sisyphus.ts # Main prompt (615 lines)
|
||||
├── sisyphus-junior.ts # Delegated task executor
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
|
||||
├── atlas.ts # Master Orchestrator (572 lines)
|
||||
├── sisyphus.ts # Main prompt (450 lines)
|
||||
├── sisyphus-junior.ts # Delegated task executor (135 lines)
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation (359 lines)
|
||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||
├── librarian.ts # Multi-repo research (GLM-4.7-free)
|
||||
├── librarian.ts # Multi-repo research (326 lines)
|
||||
├── explore.ts # Fast grep (Grok Code)
|
||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||
├── prometheus-prompt.ts # Planning (1196 lines)
|
||||
├── metis.ts # Plan consultant
|
||||
├── momus.ts # Plan reviewer
|
||||
├── metis.ts # Plan consultant (315 lines)
|
||||
├── momus.ts # Plan reviewer (444 lines)
|
||||
├── types.ts # AgentModelConfig, AgentPromptMetadata
|
||||
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback()
|
||||
└── index.ts # builtinAgents export
|
||||
@@ -31,9 +31,9 @@ agents/
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator |
|
||||
| Atlas | anthropic/claude-opus-4-5 | 0.1 | Master orchestrator |
|
||||
| oracle | openai/gpt-5.2 | 0.1 | Consultation, debugging |
|
||||
| librarian | opencode/glm-4.7-free | 0.1 | Docs, GitHub search |
|
||||
| explore | opencode/grok-code | 0.1 | Fast contextual grep |
|
||||
| multimodal-looker | google/gemini-3-flash-preview | 0.1 | PDF/image analysis |
|
||||
| librarian | opencode/big-pickle | 0.1 | Docs, GitHub search |
|
||||
| explore | opencode/gpt-5-nano | 0.1 | Fast contextual grep |
|
||||
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning |
|
||||
| Metis | anthropic/claude-sonnet-4-5 | 0.3 | Pre-planning analysis |
|
||||
| Momus | anthropic/claude-sonnet-4-5 | 0.1 | Plan validation |
|
||||
|
||||
@@ -58,7 +58,7 @@ Categories spawn \`Sisyphus-Junior-{category}\` with optimized settings:
|
||||
${categoryRows.join("\n")}
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category-name]", skills=[...], prompt="...")
|
||||
delegate_task(category="[category-name]", load_skills=[...], prompt="...")
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -84,12 +84,12 @@ ${skillRows.join("\n")}
|
||||
**MANDATORY: Evaluate ALL skills for relevance to your task.**
|
||||
|
||||
Read each skill's description and ask: "Does this skill's domain overlap with my task?"
|
||||
- If YES: INCLUDE in skills=[...]
|
||||
- If YES: INCLUDE in load_skills=[...]
|
||||
- If NO: You MUST justify why in your pre-delegation declaration
|
||||
|
||||
**Usage:**
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category]", skills=["skill-1", "skill-2"], prompt="...")
|
||||
delegate_task(category="[category]", load_skills=["skill-1", "skill-2"], prompt="...")
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT:**
|
||||
@@ -102,7 +102,7 @@ function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: Record<s
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
|
||||
const categoryRows = Object.entries(allCategories).map(([name]) =>
|
||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", skills=[...]\` |`
|
||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", load_skills=[...]\` |`
|
||||
)
|
||||
|
||||
const agentRows = agents.map((a) => {
|
||||
@@ -323,7 +323,7 @@ delegate_task(
|
||||
**If verification fails**: Resume the SAME session with the ACTUAL error output:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
resume="ses_xyz789", // ALWAYS use the session from the failed task
|
||||
session_id="ses_xyz789", // ALWAYS use the session from the failed task
|
||||
load_skills=[...],
|
||||
prompt="Verification failed: {actual error}. Fix."
|
||||
)
|
||||
@@ -331,24 +331,24 @@ delegate_task(
|
||||
|
||||
### 3.5 Handle Failures (USE RESUME)
|
||||
|
||||
**CRITICAL: When re-delegating, ALWAYS use \`resume\` parameter.**
|
||||
**CRITICAL: When re-delegating, ALWAYS use \`session_id\` parameter.**
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. STORE IT.
|
||||
|
||||
If task fails:
|
||||
1. Identify what went wrong
|
||||
2. **Resume the SAME session** - subagent has full context already:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
resume="ses_xyz789", // Session from failed task
|
||||
load_skills=[...],
|
||||
prompt="FAILED: {error}. Fix by: {specific instruction}"
|
||||
)
|
||||
\`\`\`
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
session_id="ses_xyz789", // Session from failed task
|
||||
load_skills=[...],
|
||||
prompt="FAILED: {error}. Fix by: {specific instruction}"
|
||||
)
|
||||
\`\`\`
|
||||
3. Maximum 3 retry attempts with the SAME session
|
||||
4. If blocked after 3 attempts: Document and continue to independent tasks
|
||||
|
||||
**Why resume is MANDATORY for failures:**
|
||||
**Why session_id is MANDATORY for failures:**
|
||||
- Subagent already read all files, knows the context
|
||||
- No repeated exploration = 70%+ token savings
|
||||
- Subagent knows what approaches already failed
|
||||
@@ -493,7 +493,7 @@ You are the QA gate. Subagents lie. Verify EVERYTHING.
|
||||
- Parallelize independent tasks
|
||||
- Verify with your own tools
|
||||
- **Store session_id from every delegation output**
|
||||
- **Use \`resume="{session_id}"\` for retries, fixes, and follow-ups**
|
||||
- **Use \`session_id="{session_id}"\` for retries, fixes, and follow-ups**
|
||||
</critical_overrides>
|
||||
`
|
||||
|
||||
|
||||
@@ -144,11 +144,11 @@ ${librarianSection}
|
||||
\`\`\`typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find error handling patterns here...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
@@ -209,15 +209,15 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
**ALWAYS resume when:**
|
||||
**ALWAYS continue when:**
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Task failed/incomplete | \`resume="{session_id}", prompt="Fix: {specific error}"\` |
|
||||
| Follow-up question on result | \`resume="{session_id}", prompt="Also: {question}"\` |
|
||||
| Multi-turn with same agent | \`resume="{session_id}"\` - NEVER start fresh |
|
||||
| Verification failed | \`resume="{session_id}", prompt="Failed verification: {error}. Fix."\` |
|
||||
| Task failed/incomplete | \`session_id="{session_id}", prompt="Fix: {specific error}"\` |
|
||||
| Follow-up question on result | \`session_id="{session_id}", prompt="Also: {question}"\` |
|
||||
| Multi-turn with same agent | \`session_id="{session_id}"\` - NEVER start fresh |
|
||||
| Verification failed | \`session_id="{session_id}", prompt="Failed verification: {error}. Fix."\` |
|
||||
|
||||
**Why resume is CRITICAL:**
|
||||
**Why session_id is CRITICAL:**
|
||||
- Subagent has FULL conversation context preserved
|
||||
- No repeated file reads, exploration, or setup
|
||||
- Saves 70%+ tokens on follow-ups
|
||||
@@ -228,10 +228,10 @@ Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
delegate_task(category="quick", prompt="Fix the type error in auth.ts...")
|
||||
|
||||
// CORRECT: Resume preserves everything
|
||||
delegate_task(resume="ses_abc123", prompt="Fix: Type error on line 42")
|
||||
delegate_task(session_id="ses_abc123", prompt="Fix: Type error on line 42")
|
||||
\`\`\`
|
||||
|
||||
**After EVERY delegation, STORE the session_id for potential resume.**
|
||||
**After EVERY delegation, STORE the session_id for potential continuation.**
|
||||
|
||||
### Code Changes:
|
||||
- Match existing patterns (if codebase is disciplined)
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("buildAgent with category and skills", () => {
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then - category's built-in model is applied
|
||||
expect(agent.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(agent.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("agent with category and existing model keeps existing model", () => {
|
||||
|
||||
@@ -10,8 +10,9 @@ CLI entry: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics. Com
|
||||
cli/
|
||||
├── index.ts # Commander.js entry
|
||||
├── install.ts # Interactive TUI (520 lines)
|
||||
├── config-manager.ts # JSONC parsing (641 lines)
|
||||
├── config-manager.ts # JSONC parsing (664 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
├── doctor/
|
||||
│ ├── index.ts # Doctor entry
|
||||
│ ├── runner.ts # Check orchestration
|
||||
@@ -25,6 +26,7 @@ cli/
|
||||
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
||||
│ ├── lsp.ts # LSP connectivity
|
||||
│ ├── mcp.ts # MCP validation
|
||||
│ ├── model-resolution.ts # Model resolution check
|
||||
│ └── gh.ts # GitHub CLI
|
||||
├── run/
|
||||
│ └── index.ts # Session launcher
|
||||
|
||||
@@ -5,54 +5,54 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"momus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -196,10 +196,10 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -230,7 +230,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
@@ -263,10 +263,10 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -297,7 +297,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
@@ -327,57 +327,57 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -388,57 +388,57 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -466,7 +466,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -482,7 +482,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -499,10 +499,10 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -530,7 +530,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -547,7 +547,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -565,10 +565,10 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -585,7 +585,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
@@ -596,7 +596,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -612,7 +612,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -629,10 +629,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -649,7 +649,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
@@ -660,7 +660,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -677,7 +677,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -695,10 +695,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -712,7 +712,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
@@ -726,7 +726,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -742,7 +742,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -759,10 +759,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -776,7 +776,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
@@ -790,7 +790,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -807,7 +807,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -825,10 +825,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -839,51 +839,51 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"momus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
@@ -897,28 +897,28 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"momus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
@@ -926,22 +926,22 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
@@ -961,7 +961,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
@@ -972,7 +972,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -988,7 +988,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1005,10 +1005,10 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1022,7 +1022,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
@@ -1036,7 +1036,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1052,7 +1052,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1069,10 +1069,10 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1099,7 +1099,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
@@ -1163,7 +1163,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
@@ -1179,7 +1179,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1196,10 +1196,10 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1227,7 +1227,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -1243,7 +1243,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1260,10 +1260,10 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1307,7 +1307,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1324,10 +1324,10 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1355,7 +1355,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1372,7 +1372,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1390,10 +1390,10 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("fetchNpmDistTags", () => {
|
||||
})
|
||||
|
||||
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
test("Gemini models include full spec (limit + modalities)", () => {
|
||||
test("all models include full spec (limit + modalities + Antigravity label)", () => {
|
||||
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
||||
expect(google).toBeTruthy()
|
||||
|
||||
@@ -178,9 +178,11 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(models).toBeTruthy()
|
||||
|
||||
const required = [
|
||||
"antigravity-gemini-3-pro-high",
|
||||
"antigravity-gemini-3-pro-low",
|
||||
"antigravity-gemini-3-pro",
|
||||
"antigravity-gemini-3-flash",
|
||||
"antigravity-claude-sonnet-4-5",
|
||||
"antigravity-claude-sonnet-4-5-thinking",
|
||||
"antigravity-claude-opus-4-5-thinking",
|
||||
]
|
||||
|
||||
for (const key of required) {
|
||||
@@ -198,6 +200,43 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(Array.isArray(model.modalities.output)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("Gemini models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Gemini Pro variants
|
||||
const pro = models["antigravity-gemini-3-pro"]
|
||||
// #then should have low and high variants
|
||||
expect(pro.variants).toBeTruthy()
|
||||
expect(pro.variants.low).toBeTruthy()
|
||||
expect(pro.variants.high).toBeTruthy()
|
||||
|
||||
// #when checking Gemini Flash variants
|
||||
const flash = models["antigravity-gemini-3-flash"]
|
||||
// #then should have minimal, low, medium, high variants
|
||||
expect(flash.variants).toBeTruthy()
|
||||
expect(flash.variants.minimal).toBeTruthy()
|
||||
expect(flash.variants.low).toBeTruthy()
|
||||
expect(flash.variants.medium).toBeTruthy()
|
||||
expect(flash.variants.high).toBeTruthy()
|
||||
})
|
||||
|
||||
test("Claude thinking models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Claude thinking variants
|
||||
const sonnetThinking = models["antigravity-claude-sonnet-4-5-thinking"]
|
||||
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
|
||||
|
||||
// #then both should have low and max variants
|
||||
for (const model of [sonnetThinking, opusThinking]) {
|
||||
expect(model.variants).toBeTruthy()
|
||||
expect(model.variants.low).toBeTruthy()
|
||||
expect(model.variants.max).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("generateOmoConfig - model fallback system", () => {
|
||||
@@ -277,7 +316,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
|
||||
// #then should use ultimate fallback for all agents
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/glm-4.7-free")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/big-pickle")
|
||||
})
|
||||
|
||||
test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => {
|
||||
|
||||
@@ -497,38 +497,61 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
*
|
||||
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
|
||||
*
|
||||
* The opencode-antigravity-auth plugin supports two naming conventions:
|
||||
* - `antigravity-gemini-3-pro-high` (RECOMMENDED, explicit Antigravity quota routing)
|
||||
* - `gemini-3-pro-high` (LEGACY, backward compatible but may break in future)
|
||||
* Since opencode-antigravity-auth v1.3.0, models use a variant system:
|
||||
* - `antigravity-gemini-3-pro` with variants: low, high
|
||||
* - `antigravity-gemini-3-flash` with variants: minimal, low, medium, high
|
||||
*
|
||||
* Legacy names rely on Gemini CLI using `-preview` suffix for disambiguation.
|
||||
* If Google removes `-preview`, legacy names may route to wrong quota.
|
||||
* Legacy tier-suffixed names (e.g., `antigravity-gemini-3-pro-high`) still work
|
||||
* but variants are the recommended approach.
|
||||
*
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#migration-guide-v127
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#models
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
models: {
|
||||
"antigravity-gemini-3-pro-high": {
|
||||
name: "Gemini 3 Pro High (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"antigravity-gemini-3-pro-low": {
|
||||
name: "Gemini 3 Pro Low (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
"antigravity-gemini-3-pro": {
|
||||
name: "Gemini 3 Pro (Antigravity)",
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingLevel: "low" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-gemini-3-flash": {
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65536 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
minimal: { thinkingLevel: "minimal" },
|
||||
low: { thinkingLevel: "low" },
|
||||
medium: { thinkingLevel: "medium" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-sonnet-4-5": {
|
||||
name: "Claude Sonnet 4.5 (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"antigravity-claude-sonnet-4-5-thinking": {
|
||||
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-opus-4-5-thinking": {
|
||||
name: "Claude Opus 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("model-resolution check", () => {
|
||||
// #then: Should have category entries
|
||||
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||
expect(visual).toBeDefined()
|
||||
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro-preview")
|
||||
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro")
|
||||
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("google")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -178,7 +178,7 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
|
||||
const claude = await p.select({
|
||||
message: "Do you have a Claude Pro/Max subscription?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Will use opencode/glm-4.7-free as fallback" },
|
||||
{ value: "no" as const, label: "No", hint: "Will use opencode/big-pickle as fallback" },
|
||||
{ value: "yes" as const, label: "Yes (standard)", hint: "Claude Opus 4.5 for orchestration" },
|
||||
{ value: "max20" as const, label: "Yes (max20 mode)", hint: "Full power with Claude Sonnet 4.5 for Librarian" },
|
||||
],
|
||||
@@ -363,7 +363,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
|
||||
if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) {
|
||||
printWarning("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
printWarning("No model providers configured. Using opencode/big-pickle as fallback.")
|
||||
}
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`)
|
||||
@@ -480,7 +480,7 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
|
||||
if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) {
|
||||
p.log.warn("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
p.log.warn("No model providers configured. Using opencode/big-pickle as fallback.")
|
||||
}
|
||||
|
||||
p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
@@ -310,15 +310,15 @@ describe("generateModelConfig", () => {
|
||||
})
|
||||
|
||||
describe("explore agent special cases", () => {
|
||||
test("explore uses grok-code when only Gemini available (no Claude)", () => {
|
||||
test("explore uses gpt-5-nano when only Gemini available (no Claude)", () => {
|
||||
// #given only Gemini is available (no Claude)
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use grok-code (Claude haiku not available)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/grok-code")
|
||||
// #then explore should use gpt-5-nano (Claude haiku not available)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses Claude haiku when Claude available", () => {
|
||||
@@ -343,15 +343,15 @@ describe("generateModelConfig", () => {
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("explore uses grok-code when only OpenAI available", () => {
|
||||
test("explore uses gpt-5-nano when only OpenAI available", () => {
|
||||
// #given only OpenAI is available
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use grok-code (fallback)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/grok-code")
|
||||
// #then explore should use gpt-5-nano (fallback)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface GeneratedOmoConfig {
|
||||
|
||||
const ZAI_MODEL = "zai-coding-plan/glm-4.7"
|
||||
|
||||
const ULTIMATE_FALLBACK = "opencode/glm-4.7-free"
|
||||
const ULTIMATE_FALLBACK = "opencode/big-pickle"
|
||||
const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
|
||||
|
||||
function toProviderAvailability(config: InstallConfig): ProviderAvailability {
|
||||
@@ -103,7 +103,7 @@ function getSisyphusFallbackChain(isMaxPlan: boolean): FallbackEntry[] {
|
||||
return [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -139,14 +139,14 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
continue
|
||||
}
|
||||
|
||||
// Special case: explore uses Claude haiku → OpenCode grok-code
|
||||
// Special case: explore uses Claude haiku → OpenCode gpt-5-nano
|
||||
if (role === "explore") {
|
||||
if (avail.native.claude) {
|
||||
agents[role] = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else if (avail.opencodeZen) {
|
||||
agents[role] = { model: "opencode/claude-haiku-4-5" }
|
||||
} else {
|
||||
agents[role] = { model: "opencode/grok-code" }
|
||||
agents[role] = { model: "opencode/gpt-5-nano" }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -345,6 +345,20 @@ describe("CategoryConfigSchema", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("accepts reasoningEffort as optional string with xhigh", () => {
|
||||
// #given
|
||||
const config = { reasoningEffort: "xhigh" }
|
||||
|
||||
// #when
|
||||
const result = CategoryConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.reasoningEffort).toBe("xhigh")
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects non-string variant", () => {
|
||||
// #given
|
||||
const config = { model: "openai/gpt-5.2", variant: 123 }
|
||||
|
||||
@@ -160,7 +160,7 @@ export const CategoryConfigSchema = z.object({
|
||||
type: z.enum(["enabled", "disabled"]),
|
||||
budgetTokens: z.number().optional(),
|
||||
}).optional(),
|
||||
reasoningEffort: z.enum(["low", "medium", "high"]).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(),
|
||||
|
||||
@@ -12,12 +12,14 @@ features/
|
||||
│ ├── manager.ts # Launch → poll → complete
|
||||
│ ├── concurrency.ts # Per-provider limits
|
||||
│ └── types.ts # BackgroundTask, LaunchInput
|
||||
├── skill-mcp-manager/ # MCP client lifecycle
|
||||
├── skill-mcp-manager/ # MCP client lifecycle (520 lines)
|
||||
│ ├── manager.ts # Lazy loading, cleanup
|
||||
│ └── types.ts # SkillMcpConfig
|
||||
├── builtin-skills/ # Playwright, git-master, frontend-ui-ux
|
||||
│ └── skills.ts # 1203 lines
|
||||
├── builtin-commands/ # ralph-loop, refactor, init-deep
|
||||
├── builtin-commands/ # ralph-loop, refactor, init-deep, start-work, remove-deadcode
|
||||
│ ├── commands.ts # Command registry
|
||||
│ └── templates/ # Command templates (4 files)
|
||||
├── claude-code-agent-loader/ # ~/.claude/agents/*.md
|
||||
├── claude-code-command-loader/ # ~/.claude/commands/*.md
|
||||
├── claude-code-mcp-loader/ # .mcp.json
|
||||
@@ -26,7 +28,8 @@ features/
|
||||
├── opencode-skill-loader/ # Skills from 6 directories
|
||||
├── context-injector/ # AGENTS.md/README.md injection
|
||||
├── boulder-state/ # Todo state persistence
|
||||
└── hook-message-injector/ # Message injection
|
||||
├── hook-message-injector/ # Message injection
|
||||
└── task-toast-manager/ # Background task notifications
|
||||
```
|
||||
|
||||
## LOADER PRIORITY
|
||||
|
||||
@@ -81,7 +81,7 @@ export function loadBuiltinCommands(
|
||||
for (const [name, definition] of Object.entries(BUILTIN_COMMAND_DEFINITIONS)) {
|
||||
if (!disabled.has(name as BuiltinCommandName)) {
|
||||
const { argumentHint: _argumentHint, ...openCodeCompatible } = definition
|
||||
commands[name] = openCodeCompatible as CommandDefinition
|
||||
commands[name] = { ...openCodeCompatible, name } as CommandDefinition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const RALPH_LOOP_TEMPLATE = `You are starting a Ralph Loop - a self-refer
|
||||
|
||||
## Exit Conditions
|
||||
|
||||
1. **Completion**: Output \`<promise>DONE</promise>\` (or custom promise text) when fully complete
|
||||
1. **Completion**: Output your completion promise tag when fully complete
|
||||
2. **Max Iterations**: Loop stops automatically at limit
|
||||
3. **Cancel**: User runs \`/cancel-ralph\` command
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: git-master
|
||||
description: "MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'."
|
||||
description: "MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', load_skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'."
|
||||
---
|
||||
|
||||
# Git Master Agent
|
||||
|
||||
@@ -95,7 +95,7 @@ Interpret creatively and make unexpected choices that feel genuinely designed fo
|
||||
const gitMasterSkill: BuiltinSkill = {
|
||||
name: "git-master",
|
||||
description:
|
||||
"MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'.",
|
||||
"MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', load_skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'.",
|
||||
template: `# Git Master Agent
|
||||
|
||||
You are a Git expert combining three specializations:
|
||||
|
||||
@@ -77,7 +77,13 @@ export async function loadMcpConfigs(): Promise<McpLoadResult> {
|
||||
|
||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
if (serverConfig.disabled) {
|
||||
log(`Skipping disabled MCP server "${name}"`, { path })
|
||||
log(`Disabling MCP server "${name}"`, { path })
|
||||
delete servers[name]
|
||||
const existingIndex = loadedServers.findIndex((s) => s.name === name)
|
||||
if (existingIndex !== -1) {
|
||||
loadedServers.splice(existingIndex, 1)
|
||||
log(`Removed previously loaded MCP server "${name}"`, { path })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("TaskToastManager", () => {
|
||||
const task = {
|
||||
id: "task_1",
|
||||
description: "Test task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
isBackground: true,
|
||||
skills: ["playwright", "git-master"],
|
||||
}
|
||||
@@ -127,7 +127,7 @@ describe("TaskToastManager", () => {
|
||||
const task = {
|
||||
id: "task_1",
|
||||
description: "Full info task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
isBackground: true,
|
||||
skills: ["frontend-ui-ux"],
|
||||
}
|
||||
@@ -149,9 +149,9 @@ describe("TaskToastManager", () => {
|
||||
const task = {
|
||||
id: "task_1",
|
||||
description: "Task with category default model",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "google/gemini-3-pro-preview", type: "category-default" as const },
|
||||
modelInfo: { model: "google/gemini-3-pro", type: "category-default" as const },
|
||||
}
|
||||
|
||||
// #when - addTask is called
|
||||
@@ -169,7 +169,7 @@ describe("TaskToastManager", () => {
|
||||
const task = {
|
||||
id: "task_1b",
|
||||
description: "Task with system default model",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "anthropic/claude-sonnet-4-5", type: "system-default" as const },
|
||||
}
|
||||
@@ -190,7 +190,7 @@ describe("TaskToastManager", () => {
|
||||
const task = {
|
||||
id: "task_2",
|
||||
description: "Task with inherited model",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "cliproxy/claude-opus-4-5", type: "inherited" as const },
|
||||
}
|
||||
@@ -211,7 +211,7 @@ describe("TaskToastManager", () => {
|
||||
const task = {
|
||||
id: "task_3",
|
||||
description: "Task with user model",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
isBackground: false,
|
||||
modelInfo: { model: "my-provider/my-model", type: "user-defined" as const },
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
hooks/
|
||||
├── atlas/ # Main orchestration (773 lines)
|
||||
├── anthropic-context-window-limit-recovery/ # Auto-summarize
|
||||
├── todo-continuation-enforcer.ts # Force TODO completion
|
||||
├── todo-continuation-enforcer.ts # Force TODO completion (489 lines)
|
||||
├── ralph-loop/ # Self-referential dev loop
|
||||
├── claude-code-hooks/ # settings.json compat layer - see AGENTS.md
|
||||
├── comment-checker/ # Prevents AI slop
|
||||
@@ -28,7 +28,15 @@ hooks/
|
||||
├── prometheus-md-only/ # Planner read-only mode
|
||||
├── agent-usage-reminder/ # Specialized agent hints
|
||||
├── auto-update-checker/ # Plugin update check
|
||||
└── tool-output-truncator.ts # Prevents context bloat
|
||||
├── tool-output-truncator.ts # Prevents context bloat
|
||||
├── compaction-context-injector/ # Injects context on compaction
|
||||
├── delegate-task-retry/ # Retries failed delegations
|
||||
├── interactive-bash-session/ # Tmux session management
|
||||
├── non-interactive-env/ # Non-TTY environment handling
|
||||
├── start-work/ # Sisyphus work session starter
|
||||
├── task-resume-info/ # Resume info for cancelled tasks
|
||||
├── question-label-truncator/ # Auto-truncates question labels >30 chars
|
||||
└── index.ts # Hook aggregation + registration
|
||||
```
|
||||
|
||||
## HOOK EVENTS
|
||||
|
||||
@@ -141,7 +141,7 @@ describe("atlas hook", () => {
|
||||
// #then - standalone verification reminder appended
|
||||
expect(output.output).toContain("Task completed successfully")
|
||||
expect(output.output).toContain("MANDATORY:")
|
||||
expect(output.output).toContain("delegate_task(resume=")
|
||||
expect(output.output).toContain("delegate_task(session_id=")
|
||||
|
||||
cleanupMessageStorage(sessionID)
|
||||
})
|
||||
@@ -180,7 +180,7 @@ describe("atlas hook", () => {
|
||||
expect(output.output).toContain("SUBAGENT WORK COMPLETED")
|
||||
expect(output.output).toContain("test-plan")
|
||||
expect(output.output).toContain("LIE")
|
||||
expect(output.output).toContain("delegate_task(resume=")
|
||||
expect(output.output).toContain("delegate_task(session_id=")
|
||||
|
||||
cleanupMessageStorage(sessionID)
|
||||
})
|
||||
@@ -332,7 +332,7 @@ describe("atlas hook", () => {
|
||||
cleanupMessageStorage(sessionID)
|
||||
})
|
||||
|
||||
test("should include resume and checkbox instructions in reminder", async () => {
|
||||
test("should include session_id and checkbox instructions in reminder", async () => {
|
||||
// #given - boulder state, Atlas caller
|
||||
const sessionID = "session-resume-test"
|
||||
setupMessageStorage(sessionID, "atlas")
|
||||
@@ -361,8 +361,8 @@ describe("atlas hook", () => {
|
||||
output
|
||||
)
|
||||
|
||||
// #then - should include resume instructions and verification
|
||||
expect(output.output).toContain("delegate_task(resume=")
|
||||
// #then - should include session_id instructions and verification
|
||||
expect(output.output).toContain("delegate_task(session_id=")
|
||||
expect(output.output).toContain("[x]")
|
||||
expect(output.output).toContain("MANDATORY:")
|
||||
|
||||
|
||||
@@ -179,13 +179,13 @@ If you were NOT given **exactly ONE atomic task**, you MUST:
|
||||
`
|
||||
|
||||
function buildVerificationReminder(sessionId: string): string {
|
||||
return `${VERIFICATION_REMINDER}
|
||||
return `${VERIFICATION_REMINDER}
|
||||
|
||||
---
|
||||
|
||||
**If ANY verification fails, use this immediately:**
|
||||
\`\`\`
|
||||
delegate_task(resume="${sessionId}", prompt="fix: [describe the specific failure]")
|
||||
delegate_task(session_id="${sessionId}", prompt="fix: [describe the specific failure]")
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -711,8 +711,8 @@ export function createAtlasHook(
|
||||
return
|
||||
}
|
||||
|
||||
const outputStr = output.output && typeof output.output === "string" ? output.output : ""
|
||||
const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task resumed")
|
||||
const outputStr = output.output && typeof output.output === "string" ? output.output : ""
|
||||
const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued")
|
||||
|
||||
if (isBackgroundLaunch) {
|
||||
return
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
|
||||
interface CompactingInput {
|
||||
sessionID: string
|
||||
}
|
||||
|
||||
interface CompactingOutput {
|
||||
context: string[]
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Background agent compaction hook - preserves task state during context compaction.
|
||||
*
|
||||
* When OpenCode compacts session context to save tokens, this hook injects
|
||||
* information about running and recently completed background tasks so the
|
||||
* agent doesn't lose awareness of delegated work.
|
||||
*/
|
||||
export function createBackgroundCompactionHook(manager: BackgroundManager) {
|
||||
return {
|
||||
"experimental.session.compacting": async (
|
||||
input: CompactingInput,
|
||||
output: CompactingOutput
|
||||
): Promise<void> => {
|
||||
const { sessionID } = input
|
||||
|
||||
// Get running tasks for this session
|
||||
const running = manager.getRunningTasks()
|
||||
.filter(t => t.parentSessionID === sessionID)
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
agent: t.agent,
|
||||
description: t.description,
|
||||
startedAt: t.startedAt,
|
||||
}))
|
||||
|
||||
// Get recently completed tasks (still in memory within 5-min retention)
|
||||
const completed = manager.getCompletedTasks()
|
||||
.filter(t => t.parentSessionID === sessionID)
|
||||
.slice(-10) // Last 10 completed
|
||||
.map(t => ({
|
||||
id: t.id,
|
||||
agent: t.agent,
|
||||
description: t.description,
|
||||
status: t.status,
|
||||
}))
|
||||
|
||||
// Early exit if nothing to preserve
|
||||
if (running.length === 0 && completed.length === 0) return
|
||||
|
||||
const sections: string[] = ["<background-tasks>"]
|
||||
|
||||
// Running tasks section
|
||||
if (running.length > 0) {
|
||||
sections.push("## Running Background Tasks")
|
||||
sections.push("")
|
||||
for (const t of running) {
|
||||
const elapsed = t.startedAt
|
||||
? Math.floor((Date.now() - t.startedAt.getTime()) / 1000)
|
||||
: 0
|
||||
sections.push(`- **\`${t.id}\`** (${t.agent}): ${t.description} [${elapsed}s elapsed]`)
|
||||
}
|
||||
sections.push("")
|
||||
sections.push("> **Note:** You WILL be notified when tasks complete.")
|
||||
sections.push("> Do NOT poll - continue productive work.")
|
||||
sections.push("")
|
||||
}
|
||||
|
||||
// Completed tasks section
|
||||
if (completed.length > 0) {
|
||||
sections.push("## Recently Completed Tasks")
|
||||
sections.push("")
|
||||
for (const t of completed) {
|
||||
const statusLabel = t.status === "completed" ? "[DONE]" : t.status === "error" ? "[ERROR]" : "[PENDING]"
|
||||
sections.push(`- ${statusLabel} **\`${t.id}\`**: ${t.description}`)
|
||||
}
|
||||
sections.push("")
|
||||
}
|
||||
|
||||
sections.push("## Retrieval")
|
||||
sections.push('Use `background_output(task_id="<id>")` to retrieve task results.')
|
||||
sections.push("</background-tasks>")
|
||||
|
||||
output.context.push(sections.join("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,3 +30,4 @@ export { createTaskResumeInfoHook } from "./task-resume-info";
|
||||
export { createStartWorkHook } from "./start-work";
|
||||
export { createAtlasHook } from "./atlas";
|
||||
export { createDelegateTaskRetryHook } from "./delegate-task-retry";
|
||||
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
||||
|
||||
@@ -169,10 +169,10 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
## AGENTS / **CATEGORY + SKILLS** UTILIZATION PRINCIPLES (by capability, not by name)
|
||||
- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
|
||||
- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs
|
||||
- **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown
|
||||
- MUST USE PLAN AGENT. MUST USE PLAN AGENT. MUST USE PLAN AGENT.
|
||||
- ALWAYS ASK PLAN AGENT TO WHAT CATEGORY + SKILLS / AGENTS TO LEVERAGE.
|
||||
- IF IMPLEMENT TASK, MUST ADD TODO NOW: "CONSULT WITH PLAN AGENT WITH CATEGORY + SKILLS"
|
||||
- **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn the Plan agent for work breakdown
|
||||
- MUST invoke: \`delegate_task(subagent_type="plan", prompt="<gathered context + user request>")\`
|
||||
- In your prompt to the Plan agent, ASK it to recommend which CATEGORY + SKILLS / AGENTS to leverage for implementation.
|
||||
- IF IMPLEMENT TASK, MUST ADD TODO NOW: "Consult Plan agent via delegate_task(subagent_type='plan') for work breakdown with category + skills recommendations"
|
||||
- **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning
|
||||
- **SPECIAL TASKS COVERED WITH CATEGORY + LOAD_SKILLS**: Delegate to specialized agents with category+skills for design and implementation, as following guide:
|
||||
- CATEGORY + SKILL GUIDE
|
||||
@@ -192,7 +192,7 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
## WORKFLOW
|
||||
1. Analyze the request and identify required capabilities
|
||||
2. Spawn exploration/librarian agents via delegate_task(background=true) in PARALLEL (10+ if needed)
|
||||
3. Always Use Plan agent with gathered context to create detailed work breakdown
|
||||
3. Spawn Plan agent: \`delegate_task(subagent_type="plan", prompt="<context + request>")\` to create detailed work breakdown
|
||||
4. Execute with continuous verification against original requirements
|
||||
|
||||
## VERIFICATION GUARANTEE (NON-NEGOTIABLE)
|
||||
@@ -266,9 +266,9 @@ Write these criteria explicitly. Share with user if scope is non-trivial.
|
||||
|
||||
THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTING POINT.
|
||||
|
||||
1. EXPLORES + LIBRARIANS
|
||||
2. GATHER -> PLAN AGENT SPAWN
|
||||
3. WORK BY DELEGATING TO ANOTHER AGENTS
|
||||
1. EXPLORES + LIBRARIANS (background)
|
||||
2. GATHER -> delegate_task(subagent_type="plan", prompt="<context + request>")
|
||||
3. WORK BY DELEGATING TO CATEGORY + SKILLS AGENTS
|
||||
|
||||
NOW.
|
||||
|
||||
|
||||
136
src/hooks/question-label-truncator/index.test.ts
Normal file
136
src/hooks/question-label-truncator/index.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { createQuestionLabelTruncatorHook } from "./index";
|
||||
|
||||
describe("createQuestionLabelTruncatorHook", () => {
|
||||
const hook = createQuestionLabelTruncatorHook();
|
||||
|
||||
describe("tool.execute.before", () => {
|
||||
it("truncates labels exceeding 30 characters with ellipsis", async () => {
|
||||
// #given
|
||||
const longLabel = "This is a very long label that exceeds thirty characters";
|
||||
const input = { tool: "AskUserQuestion" };
|
||||
const output = {
|
||||
args: {
|
||||
questions: [
|
||||
{
|
||||
question: "Choose an option",
|
||||
options: [
|
||||
{ label: longLabel, description: "A long option" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// #when
|
||||
await hook["tool.execute.before"]?.(input as any, output as any);
|
||||
|
||||
// #then
|
||||
const truncatedLabel = (output.args as any).questions[0].options[0].label;
|
||||
expect(truncatedLabel.length).toBeLessThanOrEqual(30);
|
||||
expect(truncatedLabel).toBe("This is a very long label t...");
|
||||
expect(truncatedLabel.endsWith("...")).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves labels within 30 characters", async () => {
|
||||
// #given
|
||||
const shortLabel = "Short label";
|
||||
const input = { tool: "AskUserQuestion" };
|
||||
const output = {
|
||||
args: {
|
||||
questions: [
|
||||
{
|
||||
question: "Choose an option",
|
||||
options: [
|
||||
{ label: shortLabel, description: "A short option" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// #when
|
||||
await hook["tool.execute.before"]?.(input as any, output as any);
|
||||
|
||||
// #then
|
||||
const resultLabel = (output.args as any).questions[0].options[0].label;
|
||||
expect(resultLabel).toBe(shortLabel);
|
||||
});
|
||||
|
||||
it("handles exactly 30 character labels without truncation", async () => {
|
||||
// #given
|
||||
const exactLabel = "Exactly thirty chars here!!!!!"; // 30 chars
|
||||
expect(exactLabel.length).toBe(30);
|
||||
const input = { tool: "ask_user_question" };
|
||||
const output = {
|
||||
args: {
|
||||
questions: [
|
||||
{
|
||||
question: "Choose",
|
||||
options: [{ label: exactLabel }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// #when
|
||||
await hook["tool.execute.before"]?.(input as any, output as any);
|
||||
|
||||
// #then
|
||||
const resultLabel = (output.args as any).questions[0].options[0].label;
|
||||
expect(resultLabel).toBe(exactLabel);
|
||||
});
|
||||
|
||||
it("ignores non-AskUserQuestion tools", async () => {
|
||||
// #given
|
||||
const input = { tool: "Bash" };
|
||||
const output = {
|
||||
args: { command: "echo hello" },
|
||||
};
|
||||
const originalArgs = { ...output.args };
|
||||
|
||||
// #when
|
||||
await hook["tool.execute.before"]?.(input as any, output as any);
|
||||
|
||||
// #then
|
||||
expect(output.args).toEqual(originalArgs);
|
||||
});
|
||||
|
||||
it("handles multiple questions with multiple options", async () => {
|
||||
// #given
|
||||
const input = { tool: "AskUserQuestion" };
|
||||
const output = {
|
||||
args: {
|
||||
questions: [
|
||||
{
|
||||
question: "Q1",
|
||||
options: [
|
||||
{ label: "Very long label number one that needs truncation" },
|
||||
{ label: "Short" },
|
||||
],
|
||||
},
|
||||
{
|
||||
question: "Q2",
|
||||
options: [
|
||||
{ label: "Another extremely long label for testing purposes" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// #when
|
||||
await hook["tool.execute.before"]?.(input as any, output as any);
|
||||
|
||||
// #then
|
||||
const q1opts = (output.args as any).questions[0].options;
|
||||
const q2opts = (output.args as any).questions[1].options;
|
||||
|
||||
expect(q1opts[0].label).toBe("Very long label number one ...");
|
||||
expect(q1opts[0].label.length).toBeLessThanOrEqual(30);
|
||||
expect(q1opts[1].label).toBe("Short");
|
||||
expect(q2opts[0].label).toBe("Another extremely long labe...");
|
||||
expect(q2opts[0].label.length).toBeLessThanOrEqual(30);
|
||||
});
|
||||
});
|
||||
});
|
||||
61
src/hooks/question-label-truncator/index.ts
Normal file
61
src/hooks/question-label-truncator/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
const MAX_LABEL_LENGTH = 30;
|
||||
|
||||
interface QuestionOption {
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface Question {
|
||||
question: string;
|
||||
header?: string;
|
||||
options: QuestionOption[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
interface AskUserQuestionArgs {
|
||||
questions: Question[];
|
||||
}
|
||||
|
||||
function truncateLabel(label: string, maxLength: number = MAX_LABEL_LENGTH): string {
|
||||
if (label.length <= maxLength) {
|
||||
return label;
|
||||
}
|
||||
return label.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
function truncateQuestionLabels(args: AskUserQuestionArgs): AskUserQuestionArgs {
|
||||
if (!args.questions || !Array.isArray(args.questions)) {
|
||||
return args;
|
||||
}
|
||||
|
||||
return {
|
||||
...args,
|
||||
questions: args.questions.map((question) => ({
|
||||
...question,
|
||||
options: question.options?.map((option) => ({
|
||||
...option,
|
||||
label: truncateLabel(option.label),
|
||||
})) ?? [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function createQuestionLabelTruncatorHook() {
|
||||
return {
|
||||
"tool.execute.before": async (
|
||||
input: { tool: string },
|
||||
output: { args: Record<string, unknown> }
|
||||
): Promise<void> => {
|
||||
const toolName = input.tool?.toLowerCase();
|
||||
|
||||
if (toolName === "askuserquestion" || toolName === "ask_user_question") {
|
||||
const args = output.args as unknown as AskUserQuestionArgs | undefined;
|
||||
|
||||
if (args?.questions) {
|
||||
const truncatedArgs = truncateQuestionLabels(args);
|
||||
Object.assign(output.args, truncatedArgs);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -459,7 +459,7 @@ describe("ralph-loop", () => {
|
||||
})
|
||||
hook.startLoop("session-123", "Build something", { completionPromise: "COMPLETE" })
|
||||
|
||||
writeFileSync(transcriptPath, JSON.stringify({ content: "Task done <promise>COMPLETE</promise>" }))
|
||||
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "Task done <promise>COMPLETE</promise>" } }) + "\n")
|
||||
|
||||
// #when - session goes idle (transcriptPath now derived from sessionID via getTranscriptPath)
|
||||
await hook.event({
|
||||
@@ -703,10 +703,105 @@ describe("ralph-loop", () => {
|
||||
expect(promptCalls[0].text).toContain("2/50")
|
||||
})
|
||||
|
||||
test("should NOT detect completion from user message in transcript (issue #622)", async () => {
|
||||
// #given - transcript contains user message with template text that includes completion promise
|
||||
// This reproduces the bug where the RALPH_LOOP_TEMPLATE instructional text
|
||||
// containing `<promise>DONE</promise>` is recorded as a user message and
|
||||
// falsely triggers completion detection
|
||||
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
|
||||
const templateText = `You are starting a Ralph Loop...
|
||||
Output <promise>DONE</promise> when fully complete`
|
||||
const userEntry = JSON.stringify({
|
||||
type: "user",
|
||||
timestamp: new Date().toISOString(),
|
||||
content: templateText,
|
||||
})
|
||||
writeFileSync(transcriptPath, userEntry + "\n")
|
||||
|
||||
const hook = createRalphLoopHook(createMockPluginInput(), {
|
||||
getTranscriptPath: () => transcriptPath,
|
||||
})
|
||||
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: "session-123" },
|
||||
},
|
||||
})
|
||||
|
||||
// #then - loop should CONTINUE (user message completion promise is instructional, not actual)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(hook.getState()?.iteration).toBe(2)
|
||||
})
|
||||
|
||||
test("should NOT detect completion from continuation prompt in transcript (issue #622)", async () => {
|
||||
// #given - transcript contains continuation prompt (also a user message) with completion promise
|
||||
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
|
||||
const continuationText = `RALPH LOOP 2/100
|
||||
When FULLY complete, output: <promise>DONE</promise>
|
||||
Original task: Build something`
|
||||
const userEntry = JSON.stringify({
|
||||
type: "user",
|
||||
timestamp: new Date().toISOString(),
|
||||
content: continuationText,
|
||||
})
|
||||
writeFileSync(transcriptPath, userEntry + "\n")
|
||||
|
||||
const hook = createRalphLoopHook(createMockPluginInput(), {
|
||||
getTranscriptPath: () => transcriptPath,
|
||||
})
|
||||
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: "session-123" },
|
||||
},
|
||||
})
|
||||
|
||||
// #then - loop should CONTINUE (continuation prompt text is not actual completion)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(hook.getState()?.iteration).toBe(2)
|
||||
})
|
||||
|
||||
test("should detect completion from tool_result entry in transcript", async () => {
|
||||
// #given - transcript contains a tool_result with completion promise
|
||||
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
|
||||
const toolResultEntry = JSON.stringify({
|
||||
type: "tool_result",
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: "write",
|
||||
tool_input: {},
|
||||
tool_output: { output: "Task complete! <promise>DONE</promise>" },
|
||||
})
|
||||
writeFileSync(transcriptPath, toolResultEntry + "\n")
|
||||
|
||||
const hook = createRalphLoopHook(createMockPluginInput(), {
|
||||
getTranscriptPath: () => transcriptPath,
|
||||
})
|
||||
hook.startLoop("session-123", "Build something", { completionPromise: "DONE" })
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.event({
|
||||
event: {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: "session-123" },
|
||||
},
|
||||
})
|
||||
|
||||
// #then - loop should complete (tool_result contains actual completion output)
|
||||
expect(promptCalls.length).toBe(0)
|
||||
expect(toastCalls.some((t) => t.title === "Ralph Loop Complete!")).toBe(true)
|
||||
expect(hook.getState()).toBeNull()
|
||||
})
|
||||
|
||||
test("should check transcript BEFORE API to optimize performance", async () => {
|
||||
// #given - transcript has completion promise
|
||||
const transcriptPath = join(TEST_DIR, "transcript.jsonl")
|
||||
writeFileSync(transcriptPath, JSON.stringify({ content: "<promise>DONE</promise>" }))
|
||||
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "<promise>DONE</promise>" } }) + "\n")
|
||||
mockSessionMessages = [
|
||||
{ info: { role: "assistant" }, parts: [{ type: "text", text: "No promise here" }] },
|
||||
]
|
||||
@@ -736,7 +831,7 @@ describe("ralph-loop", () => {
|
||||
const hook = createRalphLoopHook(createMockPluginInput(), {
|
||||
getTranscriptPath: () => transcriptPath,
|
||||
})
|
||||
writeFileSync(transcriptPath, JSON.stringify({ content: "<promise>DONE</promise>" }))
|
||||
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "<promise>DONE</promise>" } }) + "\n")
|
||||
hook.startLoop("test-id", "Build API", { ultrawork: true })
|
||||
|
||||
// #when - idle event triggered
|
||||
@@ -754,7 +849,7 @@ describe("ralph-loop", () => {
|
||||
const hook = createRalphLoopHook(createMockPluginInput(), {
|
||||
getTranscriptPath: () => transcriptPath,
|
||||
})
|
||||
writeFileSync(transcriptPath, JSON.stringify({ content: "<promise>DONE</promise>" }))
|
||||
writeFileSync(transcriptPath, JSON.stringify({ type: "tool_result", tool_name: "write", tool_output: { output: "<promise>DONE</promise>" } }) + "\n")
|
||||
hook.startLoop("test-id", "Build API")
|
||||
|
||||
// #when - idle event triggered
|
||||
|
||||
@@ -100,7 +100,18 @@ export function createRalphLoopHook(
|
||||
|
||||
const content = readFileSync(transcriptPath, "utf-8")
|
||||
const pattern = new RegExp(`<promise>\\s*${escapeRegex(promise)}\\s*</promise>`, "is")
|
||||
return pattern.test(content)
|
||||
const lines = content.split("\n").filter(l => l.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = JSON.parse(line)
|
||||
if (entry.type === "user") continue
|
||||
if (pattern.test(line)) return true
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -16,21 +16,21 @@ function extractSessionId(output: string): string | null {
|
||||
}
|
||||
|
||||
export function createTaskResumeInfoHook() {
|
||||
const toolExecuteAfter = async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { title: string; output: string; metadata: unknown }
|
||||
) => {
|
||||
if (!TARGET_TOOLS.includes(input.tool)) return
|
||||
if (output.output.startsWith("Error:") || output.output.startsWith("Failed")) return
|
||||
if (output.output.includes("\nto resume:")) return
|
||||
const toolExecuteAfter = async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { title: string; output: string; metadata: unknown }
|
||||
) => {
|
||||
if (!TARGET_TOOLS.includes(input.tool)) return
|
||||
if (output.output.startsWith("Error:") || output.output.startsWith("Failed")) return
|
||||
if (output.output.includes("\nto continue:")) return
|
||||
|
||||
const sessionId = extractSessionId(output.output)
|
||||
if (!sessionId) return
|
||||
const sessionId = extractSessionId(output.output)
|
||||
if (!sessionId) return
|
||||
|
||||
output.output = output.output.trimEnd() + `\n\nto resume: delegate_task(resume="${sessionId}", prompt="...")`
|
||||
}
|
||||
output.output = output.output.trimEnd() + `\n\nto continue: delegate_task(session_id="${sessionId}", prompt="...")`
|
||||
}
|
||||
|
||||
return {
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
}
|
||||
return {
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("createThinkModeHook integration", () => {
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3-pro",
|
||||
"think about this"
|
||||
)
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("createThinkModeHook integration", () => {
|
||||
|
||||
// #then should upgrade to high variant and inject google thinking config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-pro-preview-high")
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-pro-high")
|
||||
expect(message.providerOptions).toBeDefined()
|
||||
const googleOptions = (
|
||||
message.providerOptions as Record<string, unknown>
|
||||
@@ -125,7 +125,7 @@ describe("createThinkModeHook integration", () => {
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-3-flash",
|
||||
"ultrathink"
|
||||
)
|
||||
|
||||
@@ -134,7 +134,7 @@ describe("createThinkModeHook integration", () => {
|
||||
|
||||
// #then should upgrade to high variant
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-flash-preview-high")
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-flash-high")
|
||||
expect(message.providerOptions).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("think-mode switcher", () => {
|
||||
describe("Gemini models via github-copilot", () => {
|
||||
it("should resolve github-copilot Gemini Pro to google config", () => {
|
||||
// #given a github-copilot provider with Gemini Pro model
|
||||
const config = getThinkingConfig("github-copilot", "gemini-3-pro-preview")
|
||||
const config = getThinkingConfig("github-copilot", "gemini-3-pro")
|
||||
|
||||
// #then should return google thinking config
|
||||
expect(config).not.toBeNull()
|
||||
@@ -65,7 +65,7 @@ describe("think-mode switcher", () => {
|
||||
// #given a github-copilot provider with Gemini Flash model
|
||||
const config = getThinkingConfig(
|
||||
"github-copilot",
|
||||
"gemini-3-flash-preview"
|
||||
"gemini-3-flash"
|
||||
)
|
||||
|
||||
// #then should return google thinking config
|
||||
@@ -159,11 +159,11 @@ describe("think-mode switcher", () => {
|
||||
|
||||
it("should handle Gemini preview variants", () => {
|
||||
// #given Gemini preview model IDs
|
||||
expect(getHighVariant("gemini-3-pro-preview")).toBe(
|
||||
"gemini-3-pro-preview-high"
|
||||
expect(getHighVariant("gemini-3-pro")).toBe(
|
||||
"gemini-3-pro-high"
|
||||
)
|
||||
expect(getHighVariant("gemini-3-flash-preview")).toBe(
|
||||
"gemini-3-flash-preview-high"
|
||||
expect(getHighVariant("gemini-3-flash")).toBe(
|
||||
"gemini-3-flash-high"
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -89,12 +89,10 @@ const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||
// Claude
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5-high",
|
||||
"claude-opus-4-5": "claude-opus-4-5-high",
|
||||
// Gemini
|
||||
"gemini-3-pro": "gemini-3-pro-high",
|
||||
"gemini-3-pro-low": "gemini-3-pro-high",
|
||||
"gemini-3-pro-preview": "gemini-3-pro-preview-high",
|
||||
"gemini-3-flash": "gemini-3-flash-high",
|
||||
"gemini-3-flash-preview": "gemini-3-flash-preview-high",
|
||||
// Gemini
|
||||
"gemini-3-pro": "gemini-3-pro-high",
|
||||
"gemini-3-pro-low": "gemini-3-pro-high",
|
||||
"gemini-3-flash": "gemini-3-flash-high",
|
||||
// GPT-5
|
||||
"gpt-5": "gpt-5-high",
|
||||
"gpt-5-mini": "gpt-5-mini-high",
|
||||
|
||||
@@ -873,4 +873,193 @@ describe("todo-continuation-enforcer", () => {
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// COMPACTION AGENT FILTERING TESTS
|
||||
// These tests verify that compaction agent messages are filtered
|
||||
// when resolving agent info, preventing infinite continuation loops
|
||||
// ============================================================
|
||||
|
||||
test("should skip compaction agent messages when resolving agent info", async () => {
|
||||
// #given - session where last message is from compaction agent but previous was Sisyphus
|
||||
const sessionID = "main-compaction-filter"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const mockMessagesWithCompaction = [
|
||||
{ info: { id: "msg-1", role: "user", agent: "Sisyphus", model: { providerID: "anthropic", modelID: "claude-sonnet-4-5" } } },
|
||||
{ info: { id: "msg-2", role: "assistant", agent: "Sisyphus", modelID: "claude-sonnet-4-5", providerID: "anthropic" } },
|
||||
{ info: { id: "msg-3", role: "assistant", agent: "compaction", modelID: "claude-sonnet-4-5", providerID: "anthropic" } },
|
||||
]
|
||||
|
||||
const mockInput = {
|
||||
client: {
|
||||
session: {
|
||||
todo: async () => ({
|
||||
data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }],
|
||||
}),
|
||||
messages: async () => ({ data: mockMessagesWithCompaction }),
|
||||
prompt: async (opts: any) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
agent: opts.body.agent,
|
||||
model: opts.body.model,
|
||||
text: opts.body.parts[0].text,
|
||||
})
|
||||
return {}
|
||||
},
|
||||
},
|
||||
tui: { showToast: async () => ({}) },
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {
|
||||
backgroundManager: createMockBackgroundManager(false),
|
||||
})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
|
||||
await new Promise(r => setTimeout(r, 2500))
|
||||
|
||||
// #then - continuation uses Sisyphus (skipped compaction agent)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
expect(promptCalls[0].agent).toBe("Sisyphus")
|
||||
})
|
||||
|
||||
test("should skip injection when only compaction agent messages exist", async () => {
|
||||
// #given - session with only compaction agent (post-compaction, no prior agent info)
|
||||
const sessionID = "main-only-compaction"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const mockMessagesOnlyCompaction = [
|
||||
{ info: { id: "msg-1", role: "assistant", agent: "compaction" } },
|
||||
]
|
||||
|
||||
const mockInput = {
|
||||
client: {
|
||||
session: {
|
||||
todo: async () => ({
|
||||
data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }],
|
||||
}),
|
||||
messages: async () => ({ data: mockMessagesOnlyCompaction }),
|
||||
prompt: async (opts: any) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
agent: opts.body.agent,
|
||||
model: opts.body.model,
|
||||
text: opts.body.parts[0].text,
|
||||
})
|
||||
return {}
|
||||
},
|
||||
},
|
||||
tui: { showToast: async () => ({}) },
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - no continuation (compaction is in default skipAgents)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should skip injection when prometheus agent is after compaction", async () => {
|
||||
// #given - prometheus session that was compacted
|
||||
const sessionID = "main-prometheus-compacted"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const mockMessagesPrometheusCompacted = [
|
||||
{ info: { id: "msg-1", role: "user", agent: "prometheus" } },
|
||||
{ info: { id: "msg-2", role: "assistant", agent: "prometheus" } },
|
||||
{ info: { id: "msg-3", role: "assistant", agent: "compaction" } },
|
||||
]
|
||||
|
||||
const mockInput = {
|
||||
client: {
|
||||
session: {
|
||||
todo: async () => ({
|
||||
data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }],
|
||||
}),
|
||||
messages: async () => ({ data: mockMessagesPrometheusCompacted }),
|
||||
prompt: async (opts: any) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
agent: opts.body.agent,
|
||||
model: opts.body.model,
|
||||
text: opts.body.parts[0].text,
|
||||
})
|
||||
return {}
|
||||
},
|
||||
},
|
||||
tui: { showToast: async () => ({}) },
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - no continuation (prometheus found after filtering compaction, prometheus is in skipAgents)
|
||||
expect(promptCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should inject when agent info is undefined but skipAgents is empty", async () => {
|
||||
// #given - session with no agent info but skipAgents is empty
|
||||
const sessionID = "main-no-agent-no-skip"
|
||||
setMainSession(sessionID)
|
||||
|
||||
const mockMessagesNoAgent = [
|
||||
{ info: { id: "msg-1", role: "user" } },
|
||||
{ info: { id: "msg-2", role: "assistant" } },
|
||||
]
|
||||
|
||||
const mockInput = {
|
||||
client: {
|
||||
session: {
|
||||
todo: async () => ({
|
||||
data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }],
|
||||
}),
|
||||
messages: async () => ({ data: mockMessagesNoAgent }),
|
||||
prompt: async (opts: any) => {
|
||||
promptCalls.push({
|
||||
sessionID: opts.path.id,
|
||||
agent: opts.body.agent,
|
||||
model: opts.body.model,
|
||||
text: opts.body.parts[0].text,
|
||||
})
|
||||
return {}
|
||||
},
|
||||
},
|
||||
tui: { showToast: async () => ({}) },
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
} as any
|
||||
|
||||
const hook = createTodoContinuationEnforcer(mockInput, {
|
||||
skipAgents: [],
|
||||
})
|
||||
|
||||
// #when - session goes idle
|
||||
await hook.handler({
|
||||
event: { type: "session.idle", properties: { sessionID } },
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
|
||||
// #then - continuation injected (no agents to skip)
|
||||
expect(promptCalls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-di
|
||||
|
||||
const HOOK_NAME = "todo-continuation-enforcer"
|
||||
|
||||
const DEFAULT_SKIP_AGENTS = ["prometheus"]
|
||||
const DEFAULT_SKIP_AGENTS = ["prometheus", "compaction"]
|
||||
|
||||
export interface TodoContinuationEnforcerOptions {
|
||||
backgroundManager?: BackgroundManager
|
||||
@@ -373,6 +373,7 @@ export function createTodoContinuationEnforcer(
|
||||
}
|
||||
|
||||
let resolvedInfo: ResolvedMessageInfo | undefined
|
||||
let hasCompactionMessage = false
|
||||
try {
|
||||
const messagesResp = await ctx.client.session.messages({
|
||||
path: { id: sessionID },
|
||||
@@ -388,6 +389,10 @@ export function createTodoContinuationEnforcer(
|
||||
}>
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const info = messages[i].info
|
||||
if (info?.agent === "compaction") {
|
||||
hasCompactionMessage = true
|
||||
continue
|
||||
}
|
||||
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
||||
resolvedInfo = {
|
||||
agent: info.agent,
|
||||
@@ -401,11 +406,15 @@ export function createTodoContinuationEnforcer(
|
||||
log(`[${HOOK_NAME}] Failed to fetch messages for agent check`, { sessionID, error: String(err) })
|
||||
}
|
||||
|
||||
log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents })
|
||||
log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents, hasCompactionMessage })
|
||||
if (resolvedInfo?.agent && skipAgents.includes(resolvedInfo.agent)) {
|
||||
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedInfo.agent })
|
||||
return
|
||||
}
|
||||
if (hasCompactionMessage && !resolvedInfo?.agent) {
|
||||
log(`[${HOOK_NAME}] Skipped: compaction occurred but no agent info resolved`, { sessionID })
|
||||
return
|
||||
}
|
||||
|
||||
startCountdown(sessionID, incompleteCount, todos.length, resolvedInfo)
|
||||
return
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
createStartWorkHook,
|
||||
createAtlasHook,
|
||||
createPrometheusMdOnlyHook,
|
||||
createQuestionLabelTruncatorHook,
|
||||
} from "./hooks";
|
||||
import {
|
||||
contextCollector,
|
||||
@@ -203,9 +204,11 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
? createPrometheusMdOnlyHook(ctx)
|
||||
: null;
|
||||
|
||||
const questionLabelTruncator = createQuestionLabelTruncatorHook();
|
||||
|
||||
const taskResumeInfo = createTaskResumeInfoHook();
|
||||
|
||||
const backgroundManager = new BackgroundManager(ctx);
|
||||
const backgroundManager = new BackgroundManager(ctx, pluginConfig.background_task);
|
||||
|
||||
const atlasHook = isHookEnabled("atlas")
|
||||
? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager })
|
||||
@@ -484,6 +487,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
},
|
||||
|
||||
"tool.execute.before": async (input, output) => {
|
||||
await questionLabelTruncator["tool.execute.before"]?.(input, output);
|
||||
await claudeCodeHooks["tool.execute.before"](input, output);
|
||||
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
||||
await commentChecker?.["tool.execute.before"](input, output);
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("mergeConfigs", () => {
|
||||
temperature: 0.3,
|
||||
},
|
||||
visual: {
|
||||
model: "google/gemini-3-pro-preview",
|
||||
model: "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
} as unknown as OhMyOpenCodeConfig;
|
||||
@@ -41,7 +41,7 @@ describe("mergeConfigs", () => {
|
||||
// #then quick should be preserved from base
|
||||
expect(result.categories?.quick?.model).toBe("anthropic/claude-haiku-4-5");
|
||||
// #then visual should be added from override
|
||||
expect(result.categories?.visual?.model).toBe("google/gemini-3-pro-preview");
|
||||
expect(result.categories?.visual?.model).toBe("google/gemini-3-pro");
|
||||
});
|
||||
|
||||
it("should preserve base categories when override has no categories", () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("Prometheus category config resolution", () => {
|
||||
|
||||
// #then
|
||||
expect(config).toBeDefined()
|
||||
expect(config?.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(config?.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("user categories override default categories", () => {
|
||||
|
||||
@@ -106,13 +106,38 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
}
|
||||
|
||||
if (!(config.model as string | undefined)?.trim()) {
|
||||
const paths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
throw new Error(
|
||||
'oh-my-opencode requires a default model.\n\n' +
|
||||
`Add this to ${paths.configJsonc}:\n\n` +
|
||||
' "model": "anthropic/claude-sonnet-4-5"\n\n' +
|
||||
'(Replace with your preferred provider/model)'
|
||||
)
|
||||
let fallbackModel: string | undefined
|
||||
|
||||
for (const agentConfig of Object.values(pluginConfig.agents ?? {})) {
|
||||
const model = (agentConfig as { model?: string })?.model
|
||||
if (model && typeof model === 'string' && model.trim()) {
|
||||
fallbackModel = model.trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallbackModel) {
|
||||
for (const categoryConfig of Object.values(pluginConfig.categories ?? {})) {
|
||||
const model = (categoryConfig as { model?: string })?.model
|
||||
if (model && typeof model === 'string' && model.trim()) {
|
||||
fallbackModel = model.trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackModel) {
|
||||
config.model = fallbackModel
|
||||
log(`No default model specified, using fallback from config: ${fallbackModel}`)
|
||||
} else {
|
||||
const paths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
throw new Error(
|
||||
'oh-my-opencode requires a default model.\n\n' +
|
||||
`Add this to ${paths.configJsonc}:\n\n` +
|
||||
' "model": "anthropic/claude-sonnet-4-5"\n\n' +
|
||||
'(Replace with your preferred provider/model)'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate disabled_agents from old names to new names
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
50 cross-cutting utilities: path resolution, token truncation, config parsing, model resolution.
|
||||
34 cross-cutting utilities: path resolution, token truncation, config parsing, model resolution, agent display names.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
@@ -19,12 +19,27 @@ shared/
|
||||
├── migration.ts # Legacy config migration
|
||||
├── opencode-version.ts # Version comparison
|
||||
├── external-plugin-detector.ts # OAuth spoofing detection
|
||||
├── env-expander.ts # ${VAR} expansion
|
||||
├── model-requirements.ts # Agent/Category requirements
|
||||
├── model-availability.ts # Models fetch + fuzzy match
|
||||
├── model-resolver.ts # 3-step resolution
|
||||
├── model-sanitizer.ts # Model ID normalization
|
||||
├── shell-env.ts # Cross-platform shell
|
||||
├── prompt-parts-helper.ts # Prompt manipulation
|
||||
├── agent-display-names.ts # Agent display name mapping
|
||||
├── agent-tool-restrictions.ts # Tool restriction helpers
|
||||
├── agent-variant.ts # Agent variant detection
|
||||
├── command-executor.ts # Subprocess execution
|
||||
├── config-errors.ts # Config error types
|
||||
├── deep-merge.ts # Deep object merge
|
||||
├── file-reference-resolver.ts # File path resolution
|
||||
├── file-utils.ts # File utilities
|
||||
├── hook-disabled.ts # Hook enable/disable check
|
||||
├── pattern-matcher.ts # Glob pattern matching
|
||||
├── session-cursor.ts # Session cursor tracking
|
||||
├── snake-case.ts # String case conversion
|
||||
├── system-directive.ts # System prompt helpers
|
||||
├── tool-name.ts # Tool name constants
|
||||
├── zip-extractor.ts # ZIP file extraction
|
||||
├── index.ts # Barrel export
|
||||
└── *.test.ts # Colocated tests
|
||||
```
|
||||
|
||||
@@ -40,6 +55,7 @@ shared/
|
||||
| Resolve paths | `getOpenCodeConfigDir()` |
|
||||
| Compare versions | `isOpenCodeVersionAtLeast("1.1.0")` |
|
||||
| Resolve model | `resolveModelWithFallback()` |
|
||||
| Agent display name | `getAgentDisplayName(agentName)` |
|
||||
|
||||
## PATTERNS
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("Agent Config Integration", () => {
|
||||
const config = {
|
||||
sisyphus: { model: "anthropic/claude-opus-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
librarian: { model: "opencode/glm-4.7-free" },
|
||||
librarian: { model: "opencode/big-pickle" },
|
||||
}
|
||||
|
||||
// #when - migration is applied
|
||||
@@ -65,7 +65,7 @@ describe("Agent Config Integration", () => {
|
||||
Sisyphus: { model: "anthropic/claude-opus-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-5" },
|
||||
librarian: { model: "opencode/glm-4.7-free" },
|
||||
librarian: { model: "opencode/big-pickle" },
|
||||
}
|
||||
|
||||
// #when - migration is applied
|
||||
|
||||
@@ -30,7 +30,7 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
|
||||
read: true,
|
||||
},
|
||||
|
||||
"Sisyphus-Junior": {
|
||||
"sisyphus-junior": {
|
||||
task: false,
|
||||
delegate_task: false,
|
||||
},
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import * as fs from "fs"
|
||||
|
||||
/**
|
||||
* Returns the user-level config directory based on the OS.
|
||||
* @deprecated Use getOpenCodeConfigDir() from opencode-config-dir.ts instead.
|
||||
*/
|
||||
export function getUserConfigDir(): string {
|
||||
if (process.platform === "win32") {
|
||||
const crossPlatformDir = path.join(os.homedir(), ".config")
|
||||
const crossPlatformConfigPath = path.join(crossPlatformDir, "opencode", "oh-my-opencode.json")
|
||||
|
||||
const appdataDir = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming")
|
||||
const appdataConfigPath = path.join(appdataDir, "opencode", "oh-my-opencode.json")
|
||||
|
||||
if (fs.existsSync(crossPlatformConfigPath)) {
|
||||
return crossPlatformDir
|
||||
}
|
||||
|
||||
if (fs.existsSync(appdataConfigPath)) {
|
||||
return appdataDir
|
||||
}
|
||||
|
||||
return crossPlatformDir
|
||||
}
|
||||
|
||||
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path to the user-level oh-my-opencode config file.
|
||||
*/
|
||||
export function getUserConfigPath(): string {
|
||||
return path.join(getUserConfigDir(), "opencode", "oh-my-opencode.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path to the project-level oh-my-opencode config file.
|
||||
*/
|
||||
export function getProjectConfigPath(directory: string): string {
|
||||
return path.join(directory, ".opencode", "oh-my-opencode.json")
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export * from "./hook-disabled"
|
||||
export * from "./deep-merge"
|
||||
export * from "./file-utils"
|
||||
export * from "./dynamic-truncator"
|
||||
export * from "./config-path"
|
||||
export * from "./data-path"
|
||||
export * from "./config-errors"
|
||||
export * from "./claude-config-dir"
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("migrateAgentNames", () => {
|
||||
const agents = {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
librarian: { model: "google/gemini-3-flash" },
|
||||
explore: { model: "opencode/grok-code" },
|
||||
explore: { model: "opencode/gpt-5-nano" },
|
||||
}
|
||||
|
||||
// #when: Migrate agent names
|
||||
@@ -47,7 +47,7 @@ describe("migrateAgentNames", () => {
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.2" })
|
||||
expect(migrated["librarian"]).toEqual({ model: "google/gemini-3-flash" })
|
||||
expect(migrated["explore"]).toEqual({ model: "opencode/grok-code" })
|
||||
expect(migrated["explore"]).toEqual({ model: "opencode/gpt-5-nano" })
|
||||
})
|
||||
|
||||
test("handles case-insensitive migration", () => {
|
||||
@@ -418,7 +418,7 @@ describe("migrateAgentConfigToCategory", () => {
|
||||
test("migrates model to category when mapping exists", () => {
|
||||
// #given: Config with a model that has a category mapping
|
||||
const config = {
|
||||
model: "google/gemini-3-pro-preview",
|
||||
model: "google/gemini-3-pro",
|
||||
temperature: 0.5,
|
||||
top_p: 0.9,
|
||||
}
|
||||
@@ -467,14 +467,15 @@ describe("migrateAgentConfigToCategory", () => {
|
||||
test("handles all mapped models correctly", () => {
|
||||
// #given: Configs for each mapped model
|
||||
const configs = [
|
||||
{ model: "google/gemini-3-pro-preview" },
|
||||
{ model: "google/gemini-3-pro" },
|
||||
{ model: "google/gemini-3-flash" },
|
||||
{ model: "openai/gpt-5.2" },
|
||||
{ model: "anthropic/claude-haiku-4-5" },
|
||||
{ model: "anthropic/claude-opus-4-5" },
|
||||
{ model: "anthropic/claude-sonnet-4-5" },
|
||||
]
|
||||
|
||||
const expectedCategories = ["visual-engineering", "ultrabrain", "quick", "unspecified-high", "unspecified-low"]
|
||||
const expectedCategories = ["visual-engineering", "writing", "ultrabrain", "quick", "unspecified-high", "unspecified-low"]
|
||||
|
||||
// #when: Migrate each config
|
||||
const results = configs.map(migrateAgentConfigToCategory)
|
||||
@@ -536,7 +537,7 @@ describe("shouldDeleteAgentConfig", () => {
|
||||
// #given: Config with fields matching category defaults
|
||||
const config = {
|
||||
category: "visual-engineering",
|
||||
model: "google/gemini-3-pro-preview",
|
||||
model: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
// #when: Check if config should be deleted
|
||||
@@ -664,7 +665,7 @@ describe("migrateConfigFile with backup", () => {
|
||||
agents: {
|
||||
"multimodal-looker": { model: "anthropic/claude-haiku-4-5" },
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
"my-custom-agent": { model: "google/gemini-3-pro-preview" },
|
||||
"my-custom-agent": { model: "google/gemini-3-pro" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -680,7 +681,7 @@ describe("migrateConfigFile with backup", () => {
|
||||
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
|
||||
expect(agents["multimodal-looker"].model).toBe("anthropic/claude-haiku-4-5")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents["my-custom-agent"].model).toBe("google/gemini-3-pro-preview")
|
||||
expect(agents["my-custom-agent"].model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("preserves category setting when explicitly set", () => {
|
||||
|
||||
@@ -82,7 +82,8 @@ export const HOOK_NAME_MAP: Record<string, string | null> = {
|
||||
* This map will be removed in a future major version once migration period ends.
|
||||
*/
|
||||
export const MODEL_TO_CATEGORY_MAP: Record<string, string> = {
|
||||
"google/gemini-3-pro-preview": "visual-engineering",
|
||||
"google/gemini-3-pro": "visual-engineering",
|
||||
"google/gemini-3-flash": "writing",
|
||||
"openai/gpt-5.2": "ultrabrain",
|
||||
"anthropic/claude-haiku-4-5": "quick",
|
||||
"anthropic/claude-opus-4-5": "unspecified-high",
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("fetchAvailableModels", () => {
|
||||
openai: { id: "openai", models: { "gpt-5.2-codex": { id: "gpt-5.2-codex" } } },
|
||||
anthropic: { id: "anthropic", models: { "claude-sonnet-4-5": { id: "claude-sonnet-4-5" } } },
|
||||
google: { id: "google", models: { "gemini-3-flash": { id: "gemini-3-flash" } } },
|
||||
opencode: { id: "opencode", models: { "grok-code": { id: "grok-code" } } },
|
||||
opencode: { id: "opencode", models: { "gpt-5-nano": { id: "gpt-5-nano" } } },
|
||||
})
|
||||
|
||||
const result = await fetchAvailableModels()
|
||||
@@ -89,7 +89,7 @@ describe("fetchAvailableModels", () => {
|
||||
expect(result.has("openai/gpt-5.2-codex")).toBe(true)
|
||||
expect(result.has("anthropic/claude-sonnet-4-5")).toBe(true)
|
||||
expect(result.has("google/gemini-3-flash")).toBe(true)
|
||||
expect(result.has("opencode/grok-code")).toBe(true)
|
||||
expect(result.has("opencode/gpt-5-nano")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -69,19 +69,19 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.model).toBe("claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("multimodal-looker has valid fallbackChain with gemini-3-flash-preview as primary", () => {
|
||||
test("multimodal-looker has valid fallbackChain with gemini-3-flash as primary", () => {
|
||||
// #given - multimodal-looker agent requirement
|
||||
const multimodalLooker = AGENT_MODEL_REQUIREMENTS["multimodal-looker"]
|
||||
|
||||
// #when - accessing multimodal-looker requirement
|
||||
// #then - fallbackChain exists with gemini-3-flash-preview as first entry
|
||||
// #then - fallbackChain exists with gemini-3-flash as first entry
|
||||
expect(multimodalLooker).toBeDefined()
|
||||
expect(multimodalLooker.fallbackChain).toBeArray()
|
||||
expect(multimodalLooker.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = multimodalLooker.fallbackChain[0]
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
expect(primary.model).toBe("gemini-3-flash-preview")
|
||||
expect(primary.model).toBe("gemini-3-flash")
|
||||
})
|
||||
|
||||
test("prometheus has valid fallbackChain with claude-opus-4-5 as primary", () => {
|
||||
@@ -199,19 +199,19 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("openai")
|
||||
})
|
||||
|
||||
test("visual-engineering has valid fallbackChain with gemini-3-pro-preview as primary", () => {
|
||||
test("visual-engineering has valid fallbackChain with gemini-3-pro as primary", () => {
|
||||
// #given - visual-engineering category requirement
|
||||
const visualEngineering = CATEGORY_MODEL_REQUIREMENTS["visual-engineering"]
|
||||
|
||||
// #when - accessing visual-engineering requirement
|
||||
// #then - fallbackChain exists with gemini-3-pro-preview as first entry
|
||||
// #then - fallbackChain exists with gemini-3-pro as first entry
|
||||
expect(visualEngineering).toBeDefined()
|
||||
expect(visualEngineering.fallbackChain).toBeArray()
|
||||
expect(visualEngineering.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = visualEngineering.fallbackChain[0]
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
expect(primary.model).toBe("gemini-3-pro-preview")
|
||||
expect(primary.model).toBe("gemini-3-pro")
|
||||
})
|
||||
|
||||
test("quick has valid fallbackChain with claude-haiku-4-5 as primary", () => {
|
||||
@@ -260,34 +260,34 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("anthropic")
|
||||
})
|
||||
|
||||
test("artistry has valid fallbackChain with gemini-3-pro-preview as primary", () => {
|
||||
test("artistry has valid fallbackChain with gemini-3-pro as primary", () => {
|
||||
// #given - artistry category requirement
|
||||
const artistry = CATEGORY_MODEL_REQUIREMENTS["artistry"]
|
||||
|
||||
// #when - accessing artistry requirement
|
||||
// #then - fallbackChain exists with gemini-3-pro-preview as first entry
|
||||
// #then - fallbackChain exists with gemini-3-pro as first entry
|
||||
expect(artistry).toBeDefined()
|
||||
expect(artistry.fallbackChain).toBeArray()
|
||||
expect(artistry.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = artistry.fallbackChain[0]
|
||||
expect(primary.model).toBe("gemini-3-pro-preview")
|
||||
expect(primary.model).toBe("gemini-3-pro")
|
||||
expect(primary.variant).toBe("max")
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
})
|
||||
|
||||
test("writing has valid fallbackChain with gemini-3-flash-preview as primary", () => {
|
||||
test("writing has valid fallbackChain with gemini-3-flash as primary", () => {
|
||||
// #given - writing category requirement
|
||||
const writing = CATEGORY_MODEL_REQUIREMENTS["writing"]
|
||||
|
||||
// #when - accessing writing requirement
|
||||
// #then - fallbackChain exists with gemini-3-flash-preview as first entry
|
||||
// #then - fallbackChain exists with gemini-3-flash as first entry
|
||||
expect(writing).toBeDefined()
|
||||
expect(writing.fallbackChain).toBeArray()
|
||||
expect(writing.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = writing.fallbackChain[0]
|
||||
expect(primary.model).toBe("gemini-3-flash-preview")
|
||||
expect(primary.model).toBe("gemini-3-flash")
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
})
|
||||
|
||||
@@ -344,7 +344,7 @@ describe("FallbackEntry type", () => {
|
||||
// #given - a FallbackEntry without variant
|
||||
const entry: FallbackEntry = {
|
||||
providers: ["opencode", "anthropic"],
|
||||
model: "glm-4.7-free",
|
||||
model: "big-pickle",
|
||||
}
|
||||
|
||||
// #when - accessing variant
|
||||
@@ -374,7 +374,7 @@ describe("ModelRequirement type", () => {
|
||||
test("ModelRequirement variant is optional", () => {
|
||||
// #given - a ModelRequirement without top-level variant
|
||||
const requirement: ModelRequirement = {
|
||||
fallbackChain: [{ providers: ["opencode"], model: "glm-4.7-free" }],
|
||||
fallbackChain: [{ providers: ["opencode"], model: "big-pickle" }],
|
||||
}
|
||||
|
||||
// #when - accessing variant
|
||||
|
||||
@@ -15,63 +15,64 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
],
|
||||
},
|
||||
oracle: {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
],
|
||||
},
|
||||
librarian: {
|
||||
fallbackChain: [
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
{ providers: ["opencode"], model: "glm-4.7-free" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
],
|
||||
},
|
||||
librarian: {
|
||||
fallbackChain: [
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
{ providers: ["opencode"], model: "big-pickle" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
],
|
||||
},
|
||||
explore: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" },
|
||||
{ providers: ["opencode"], model: "grok-code" },
|
||||
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
},
|
||||
"multimodal-looker": {
|
||||
fallbackChain: [
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
|
||||
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
},
|
||||
prometheus: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
],
|
||||
},
|
||||
metis: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview", variant: "max" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "max" },
|
||||
],
|
||||
},
|
||||
momus: {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "medium" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview", variant: "max" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "max" },
|
||||
],
|
||||
},
|
||||
atlas: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -79,7 +80,7 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
"visual-engineering": {
|
||||
fallbackChain: [
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
],
|
||||
@@ -88,12 +89,12 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "xhigh" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
],
|
||||
},
|
||||
artistry: {
|
||||
fallbackChain: [
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview", variant: "max" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "max" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
],
|
||||
@@ -101,27 +102,27 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
quick: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" },
|
||||
{ providers: ["opencode"], model: "grok-code" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
},
|
||||
"unspecified-low": {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
],
|
||||
},
|
||||
"unspecified-high": {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
],
|
||||
},
|
||||
writing: {
|
||||
fallbackChain: [
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
|
||||
@@ -236,9 +236,9 @@ describe("resolveModelWithFallback", () => {
|
||||
// #given
|
||||
const input: ExtendedModelResolutionInput = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "opencode", "github-copilot"], model: "grok-code" },
|
||||
{ providers: ["anthropic", "opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
availableModels: new Set(["opencode/grok-code", "github-copilot/grok-code-preview"]),
|
||||
availableModels: new Set(["opencode/gpt-5-nano"]),
|
||||
systemDefaultModel: "google/gemini-3-pro",
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("resolveModelWithFallback", () => {
|
||||
const result = resolveModelWithFallback(input)
|
||||
|
||||
// #then
|
||||
expect(result.model).toBe("opencode/grok-code")
|
||||
expect(result.model).toBe("opencode/gpt-5-nano")
|
||||
expect(result.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
@@ -392,20 +392,20 @@ describe("resolveModelWithFallback", () => {
|
||||
|
||||
test("tries all providers in first entry before moving to second entry", () => {
|
||||
// #given
|
||||
const availableModels = new Set(["google/gemini-3-pro-preview"])
|
||||
const availableModels = new Set(["google/gemini-3-pro"])
|
||||
|
||||
// #when
|
||||
const result = resolveModelWithFallback({
|
||||
fallbackChain: [
|
||||
{ providers: ["openai", "anthropic"], model: "gpt-5.2" },
|
||||
{ providers: ["google"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google"], model: "gemini-3-pro" },
|
||||
],
|
||||
availableModels,
|
||||
systemDefaultModel: "system/default",
|
||||
})
|
||||
|
||||
// #then
|
||||
expect(result.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(result.model).toBe("google/gemini-3-pro")
|
||||
expect(result.source).toBe("provider-fallback")
|
||||
})
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ tools/
|
||||
├── skill-mcp/ # Skill MCP operations
|
||||
├── slashcommand/ # Slash command dispatch
|
||||
├── call-omo-agent/ # Direct agent invocation
|
||||
└── background-task/ # background_output, background_cancel
|
||||
└── background-task/ # background_output, background_cancel (513 lines)
|
||||
```
|
||||
|
||||
## TOOL CATEGORIES
|
||||
@@ -48,7 +48,7 @@ tools/
|
||||
|
||||
## LSP SPECIFICS
|
||||
|
||||
- **Client**: `client.ts` manages stdio, JSON-RPC
|
||||
- **Client**: `client.ts` manages stdio, JSON-RPC (596 lines)
|
||||
- **Singleton**: `LSPServerManager` with ref counting
|
||||
- **Capabilities**: definition, references, symbols, diagnostics, rename
|
||||
|
||||
|
||||
@@ -406,27 +406,61 @@ export function createBackgroundCancel(manager: BackgroundManager, client: Openc
|
||||
return `No running or pending background tasks to cancel.`
|
||||
}
|
||||
|
||||
const results: string[] = []
|
||||
const cancelledInfo: Array<{
|
||||
id: string
|
||||
description: string
|
||||
status: string
|
||||
sessionID?: string
|
||||
}> = []
|
||||
|
||||
for (const task of cancellableTasks) {
|
||||
if (task.status === "pending") {
|
||||
// Pending task: use manager method (no session to abort)
|
||||
manager.cancelPendingTask(task.id)
|
||||
results.push(`- ${task.id}: ${task.description} (pending)`)
|
||||
cancelledInfo.push({
|
||||
id: task.id,
|
||||
description: task.description,
|
||||
status: "pending",
|
||||
sessionID: undefined,
|
||||
})
|
||||
} else if (task.sessionID) {
|
||||
// Running task: abort session
|
||||
client.session.abort({
|
||||
path: { id: task.sessionID },
|
||||
}).catch(() => {})
|
||||
|
||||
task.status = "cancelled"
|
||||
task.completedAt = new Date()
|
||||
results.push(`- ${task.id}: ${task.description} (running)`)
|
||||
cancelledInfo.push({
|
||||
id: task.id,
|
||||
description: task.description,
|
||||
status: "running",
|
||||
sessionID: task.sessionID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const tableRows = cancelledInfo
|
||||
.map(t => `| \`${t.id}\` | ${t.description} | ${t.status} | ${t.sessionID ? `\`${t.sessionID}\`` : "(not started)"} |`)
|
||||
.join("\n")
|
||||
|
||||
const resumableTasks = cancelledInfo.filter(t => t.sessionID)
|
||||
const resumeSection = resumableTasks.length > 0
|
||||
? `\n## Continue Instructions
|
||||
|
||||
To continue a cancelled task, use:
|
||||
\`\`\`
|
||||
delegate_task(session_id="<session_id>", prompt="Continue: <your follow-up>")
|
||||
\`\`\`
|
||||
|
||||
Continuable sessions:
|
||||
${resumableTasks.map(t => `- \`${t.sessionID}\` (${t.description})`).join("\n")}`
|
||||
: ""
|
||||
|
||||
return `Cancelled ${cancellableTasks.length} background task(s):
|
||||
|
||||
${results.join("\n")}`
|
||||
| Task ID | Description | Status | Session ID |
|
||||
|---------|-------------|--------|------------|
|
||||
${tableRows}
|
||||
${resumeSection}`
|
||||
}
|
||||
|
||||
const task = manager.getTask(args.taskId!)
|
||||
|
||||
@@ -4,4 +4,4 @@ export const CALL_OMO_AGENT_DESCRIPTION = `Spawn explore/librarian agent. run_in
|
||||
|
||||
Available: {agents}
|
||||
|
||||
Pass \`resume=session_id\` to continue previous agent with full context. Prompts MUST be in English. Use \`background_output\` for async results.`
|
||||
Pass \`session_id=<id>\` to continue previous agent with full context. Prompts MUST be in English. Use \`background_output\` for async results.`
|
||||
|
||||
@@ -156,13 +156,13 @@ Approach:
|
||||
|
||||
|
||||
export const DEFAULT_CATEGORIES: Record<string, CategoryConfig> = {
|
||||
"visual-engineering": { model: "google/gemini-3-pro-preview" },
|
||||
"visual-engineering": { model: "google/gemini-3-pro" },
|
||||
ultrabrain: { model: "openai/gpt-5.2-codex", variant: "xhigh" },
|
||||
artistry: { model: "google/gemini-3-pro-preview", variant: "max" },
|
||||
artistry: { model: "google/gemini-3-pro", variant: "max" },
|
||||
quick: { model: "anthropic/claude-haiku-4-5" },
|
||||
"unspecified-low": { model: "anthropic/claude-sonnet-4-5" },
|
||||
"unspecified-high": { model: "anthropic/claude-opus-4-5", variant: "max" },
|
||||
writing: { model: "google/gemini-3-flash-preview" },
|
||||
writing: { model: "google/gemini-3-flash" },
|
||||
}
|
||||
|
||||
export const CATEGORY_PROMPT_APPENDS: Record<string, string> = {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// #when / #then
|
||||
expect(category).toBeDefined()
|
||||
expect(category.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(category.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("ultrabrain category has model and variant config", () => {
|
||||
@@ -142,7 +142,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// #then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.config.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(result!.config.model).toBe("google/gemini-3-pro")
|
||||
expect(result!.promptAppend).toContain("VISUAL/UI")
|
||||
})
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("sisyphus-task", () => {
|
||||
const categoryName = "visual-engineering"
|
||||
const userCategories = {
|
||||
"visual-engineering": {
|
||||
model: "google/gemini-3-pro-preview",
|
||||
model: "google/gemini-3-pro",
|
||||
prompt_append: "Custom instructions here",
|
||||
},
|
||||
}
|
||||
@@ -206,7 +206,7 @@ describe("sisyphus-task", () => {
|
||||
const categoryName = "visual-engineering"
|
||||
const userCategories = {
|
||||
"visual-engineering": {
|
||||
model: "google/gemini-3-pro-preview",
|
||||
model: "google/gemini-3-pro",
|
||||
temperature: 0.3,
|
||||
},
|
||||
}
|
||||
@@ -229,7 +229,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// #then - category's built-in model wins over inheritedModel
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.config.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(result!.config.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("systemDefaultModel is used as fallback when custom category has no model", () => {
|
||||
@@ -271,7 +271,7 @@ describe("sisyphus-task", () => {
|
||||
|
||||
// #then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.config.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(result!.config.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -288,7 +288,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-variant",
|
||||
sessionID: "session-variant",
|
||||
description: "Variant task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -351,7 +351,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-default-variant",
|
||||
sessionID: "session-default-variant",
|
||||
description: "Default variant task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -594,16 +594,16 @@ describe("sisyphus-task", () => {
|
||||
}, { timeout: 20000 })
|
||||
})
|
||||
|
||||
describe("resume with background parameter", () => {
|
||||
test("resume with background=false should wait for result and return content", async () => {
|
||||
describe("session_id with background parameter", () => {
|
||||
test("session_id with background=false should wait for result and return content", async () => {
|
||||
// Note: This test needs extended timeout because the implementation has MIN_STABILITY_TIME_MS = 5000
|
||||
// #given
|
||||
const { createDelegateTask } = require("./tools")
|
||||
|
||||
const mockTask = {
|
||||
id: "task-123",
|
||||
sessionID: "ses_resume_test",
|
||||
description: "Resumed task",
|
||||
sessionID: "ses_continue_test",
|
||||
description: "Continued task",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
}
|
||||
@@ -620,7 +620,7 @@ describe("sisyphus-task", () => {
|
||||
data: [
|
||||
{
|
||||
info: { role: "assistant", time: { created: Date.now() } },
|
||||
parts: [{ type: "text", text: "This is the resumed task result" }],
|
||||
parts: [{ type: "text", text: "This is the continued task result" }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -646,28 +646,28 @@ describe("sisyphus-task", () => {
|
||||
// #when
|
||||
const result = await tool.execute(
|
||||
{
|
||||
description: "Resume test",
|
||||
description: "Continue test",
|
||||
prompt: "Continue the task",
|
||||
resume: "ses_resume_test",
|
||||
session_id: "ses_continue_test",
|
||||
run_in_background: false,
|
||||
load_skills: ["git-master"],
|
||||
},
|
||||
toolContext
|
||||
)
|
||||
|
||||
// #then - should contain actual result, not just "Background task resumed"
|
||||
expect(result).toContain("This is the resumed task result")
|
||||
expect(result).not.toContain("Background task resumed")
|
||||
// #then - should contain actual result, not just "Background task continued"
|
||||
expect(result).toContain("This is the continued task result")
|
||||
expect(result).not.toContain("Background task continued")
|
||||
}, { timeout: 10000 })
|
||||
|
||||
test("resume with background=true should return immediately without waiting", async () => {
|
||||
test("session_id with background=true should return immediately without waiting", async () => {
|
||||
// #given
|
||||
const { createDelegateTask } = require("./tools")
|
||||
|
||||
const mockTask = {
|
||||
id: "task-456",
|
||||
sessionID: "ses_bg_resume",
|
||||
description: "Background resumed task",
|
||||
sessionID: "ses_bg_continue",
|
||||
description: "Background continued task",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
}
|
||||
@@ -701,9 +701,9 @@ describe("sisyphus-task", () => {
|
||||
// #when
|
||||
const result = await tool.execute(
|
||||
{
|
||||
description: "Resume bg test",
|
||||
description: "Continue bg test",
|
||||
prompt: "Continue in background",
|
||||
resume: "ses_bg_resume",
|
||||
session_id: "ses_bg_continue",
|
||||
run_in_background: true,
|
||||
load_skills: ["git-master"],
|
||||
},
|
||||
@@ -711,7 +711,7 @@ describe("sisyphus-task", () => {
|
||||
)
|
||||
|
||||
// #then - should return background message
|
||||
expect(result).toContain("Background task resumed")
|
||||
expect(result).toContain("Background task continued")
|
||||
expect(result).toContain("task-456")
|
||||
})
|
||||
})
|
||||
@@ -951,7 +951,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-unstable",
|
||||
sessionID: "ses_unstable_gemini",
|
||||
description: "Unstable gemini task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -960,7 +960,7 @@ describe("sisyphus-task", () => {
|
||||
const mockClient = {
|
||||
app: { agents: async () => ({ data: [] }) },
|
||||
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
||||
model: { list: async () => [{ id: "google/gemini-3-pro-preview" }] },
|
||||
model: { list: async () => [{ id: "google/gemini-3-pro" }] },
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => ({ data: { id: "ses_unstable_gemini" } }),
|
||||
@@ -1016,7 +1016,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-normal-bg",
|
||||
sessionID: "ses_normal_bg",
|
||||
description: "Normal background task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -1135,7 +1135,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-artistry",
|
||||
sessionID: "ses_artistry_gemini",
|
||||
description: "Artistry gemini task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -1144,7 +1144,7 @@ describe("sisyphus-task", () => {
|
||||
const mockClient = {
|
||||
app: { agents: async () => ({ data: [] }) },
|
||||
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
||||
model: { list: async () => [{ id: "google/gemini-3-pro-preview" }] },
|
||||
model: { list: async () => [{ id: "google/gemini-3-pro" }] },
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => ({ data: { id: "ses_artistry_gemini" } }),
|
||||
@@ -1170,7 +1170,7 @@ describe("sisyphus-task", () => {
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
// #when - artistry category (gemini-3-pro-preview with max variant)
|
||||
// #when - artistry category (gemini-3-pro with max variant)
|
||||
const result = await tool.execute(
|
||||
{
|
||||
description: "Test artistry forced background",
|
||||
@@ -1189,7 +1189,7 @@ describe("sisyphus-task", () => {
|
||||
}, { timeout: 20000 })
|
||||
|
||||
test("writing category (gemini-flash) with run_in_background=false should force background but wait for result", async () => {
|
||||
// #given - writing uses gemini-3-flash-preview
|
||||
// #given - writing uses gemini-3-flash
|
||||
const { createDelegateTask } = require("./tools")
|
||||
let launchCalled = false
|
||||
|
||||
@@ -1200,7 +1200,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-writing",
|
||||
sessionID: "ses_writing_gemini",
|
||||
description: "Writing gemini task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -1209,7 +1209,7 @@ describe("sisyphus-task", () => {
|
||||
const mockClient = {
|
||||
app: { agents: async () => ({ data: [] }) },
|
||||
config: { get: async () => ({ data: { model: SYSTEM_DEFAULT_MODEL } }) },
|
||||
model: { list: async () => [{ id: "google/gemini-3-flash-preview" }] },
|
||||
model: { list: async () => [{ id: "google/gemini-3-flash" }] },
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => ({ data: { id: "ses_writing_gemini" } }),
|
||||
@@ -1235,7 +1235,7 @@ describe("sisyphus-task", () => {
|
||||
abort: new AbortController().signal,
|
||||
}
|
||||
|
||||
// #when - writing category (gemini-3-flash-preview)
|
||||
// #when - writing category (gemini-3-flash)
|
||||
const result = await tool.execute(
|
||||
{
|
||||
description: "Test writing forced background",
|
||||
@@ -1265,7 +1265,7 @@ describe("sisyphus-task", () => {
|
||||
id: "task-custom-unstable",
|
||||
sessionID: "ses_custom_unstable",
|
||||
description: "Custom unstable task",
|
||||
agent: "Sisyphus-Junior",
|
||||
agent: "sisyphus-junior",
|
||||
status: "running",
|
||||
}
|
||||
},
|
||||
@@ -1535,9 +1535,9 @@ describe("sisyphus-task", () => {
|
||||
// #when resolveCategoryConfig is called
|
||||
const resolved = resolveCategoryConfig(categoryName, { userCategories, inheritedModel, systemDefaultModel: SYSTEM_DEFAULT_MODEL })
|
||||
|
||||
// #then should use category's built-in model (gemini-3-pro-preview for visual-engineering)
|
||||
// #then should use category's built-in model (gemini-3-pro for visual-engineering)
|
||||
expect(resolved).not.toBeNull()
|
||||
expect(resolved!.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(resolved!.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("systemDefaultModel is used when no other model is available", () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
const SISYPHUS_JUNIOR_AGENT = "Sisyphus-Junior"
|
||||
const SISYPHUS_JUNIOR_AGENT = "sisyphus-junior"
|
||||
|
||||
function parseModelString(model: string): { providerID: string; modelID: string } | undefined {
|
||||
const parts = model.split("/")
|
||||
@@ -86,8 +86,8 @@ function formatDetailedError(error: unknown, ctx: ErrorContext): string {
|
||||
lines.push(`- subagent_type: ${ctx.args.subagent_type ?? "(none)"}`)
|
||||
lines.push(`- run_in_background: ${ctx.args.run_in_background}`)
|
||||
lines.push(`- load_skills: [${ctx.args.load_skills?.join(", ") ?? ""}]`)
|
||||
if (ctx.args.resume) {
|
||||
lines.push(`- resume: ${ctx.args.resume}`)
|
||||
if (ctx.args.session_id) {
|
||||
lines.push(`- session_id: ${ctx.args.session_id}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function createDelegateTask(options: DelegateTaskToolOptions): ToolDefini
|
||||
|
||||
const description = `Spawn agent task with category-based or direct agent selection.
|
||||
|
||||
MUTUALLY EXCLUSIVE: Provide EITHER category OR subagent_type, not both (unless resuming).
|
||||
MUTUALLY EXCLUSIVE: Provide EITHER category OR subagent_type, not both (unless continuing a session).
|
||||
|
||||
- load_skills: ALWAYS REQUIRED. Pass at least one skill name (e.g., ["playwright"], ["git-master", "frontend-ui-ux"]).
|
||||
- category: Use predefined category → Spawns Sisyphus-Junior with category config
|
||||
@@ -202,12 +202,13 @@ MUTUALLY EXCLUSIVE: Provide EITHER category OR subagent_type, not both (unless r
|
||||
${categoryList}
|
||||
- subagent_type: Use specific agent directly (e.g., "oracle", "explore")
|
||||
- run_in_background: true=async (returns task_id), false=sync (waits for result). Default: false. Use background=true ONLY for parallel exploration with 5+ independent queries.
|
||||
- resume: Session ID to resume (from previous task output). Continues agent with FULL CONTEXT PRESERVED - saves tokens, maintains continuity.
|
||||
- session_id: Existing Task session to continue (from previous task output). Continues agent with FULL CONTEXT PRESERVED - saves tokens, maintains continuity.
|
||||
- command: The command that triggered this task (optional, for slash command tracking).
|
||||
|
||||
**WHEN TO USE resume:**
|
||||
- Task failed/incomplete → resume with "fix: [specific issue]"
|
||||
- Need follow-up on previous result → resume with additional question
|
||||
- Multi-turn conversation with same agent → always resume instead of new task
|
||||
**WHEN TO USE session_id:**
|
||||
- Task failed/incomplete → session_id with "fix: [specific issue]"
|
||||
- Need follow-up on previous result → session_id with additional question
|
||||
- Multi-turn conversation with same agent → always session_id instead of new task
|
||||
|
||||
Prompts MUST be in English.`
|
||||
|
||||
@@ -220,7 +221,8 @@ Prompts MUST be in English.`
|
||||
run_in_background: tool.schema.boolean().describe("true=async (returns task_id), false=sync (waits). Default: false"),
|
||||
category: tool.schema.string().optional().describe(`Category (e.g., ${categoryExamples}). Mutually exclusive with subagent_type.`),
|
||||
subagent_type: tool.schema.string().optional().describe("Agent name (e.g., 'oracle', 'explore'). Mutually exclusive with category."),
|
||||
resume: tool.schema.string().optional().describe("Session ID to resume"),
|
||||
session_id: tool.schema.string().optional().describe("Existing Task session to continue"),
|
||||
command: tool.schema.string().optional().describe("The command that triggered this task"),
|
||||
},
|
||||
async execute(args: DelegateTaskArgs, toolContext) {
|
||||
const ctx = toolContext as ToolContextWithMetadata
|
||||
@@ -265,11 +267,11 @@ Prompts MUST be in English.`
|
||||
? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID }
|
||||
: undefined
|
||||
|
||||
if (args.resume) {
|
||||
if (args.session_id) {
|
||||
if (runInBackground) {
|
||||
try {
|
||||
const task = await manager.resume({
|
||||
sessionId: args.resume,
|
||||
sessionId: args.session_id,
|
||||
prompt: args.prompt,
|
||||
parentSessionID: ctx.sessionID,
|
||||
parentMessageID: ctx.messageID,
|
||||
@@ -278,7 +280,7 @@ Prompts MUST be in English.`
|
||||
})
|
||||
|
||||
ctx.metadata?.({
|
||||
title: `Resume: ${task.description}`,
|
||||
title: `Continue: ${task.description}`,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
agent: task.agent,
|
||||
@@ -286,10 +288,11 @@ Prompts MUST be in English.`
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: task.sessionID,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
|
||||
return `Background task resumed.
|
||||
return `Background task continued.
|
||||
|
||||
Task ID: ${task.id}
|
||||
Session ID: ${task.sessionID}
|
||||
@@ -301,35 +304,36 @@ Agent continues with full previous context preserved.
|
||||
Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
} catch (error) {
|
||||
return formatDetailedError(error, {
|
||||
operation: "Resume background task",
|
||||
operation: "Continue background task",
|
||||
args,
|
||||
sessionID: args.resume,
|
||||
sessionID: args.session_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const toastManager = getTaskToastManager()
|
||||
const taskId = `resume_sync_${args.resume.slice(0, 8)}`
|
||||
const taskId = `resume_sync_${args.session_id.slice(0, 8)}`
|
||||
const startTime = new Date()
|
||||
|
||||
if (toastManager) {
|
||||
toastManager.addTask({
|
||||
id: taskId,
|
||||
description: args.description,
|
||||
agent: "resume",
|
||||
agent: "continue",
|
||||
isBackground: false,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.metadata?.({
|
||||
title: `Resume: ${args.description}`,
|
||||
title: `Continue: ${args.description}`,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
load_skills: args.load_skills,
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: args.resume,
|
||||
sessionId: args.session_id,
|
||||
sync: true,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -338,7 +342,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
let resumeModel: { providerID: string; modelID: string } | undefined
|
||||
|
||||
try {
|
||||
const messagesResp = await client.session.messages({ path: { id: args.resume } })
|
||||
const messagesResp = await client.session.messages({ path: { id: args.session_id } })
|
||||
const messages = (messagesResp.data ?? []) as Array<{
|
||||
info?: { agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string }
|
||||
}>
|
||||
@@ -351,7 +355,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
const resumeMessageDir = getMessageDir(args.resume)
|
||||
const resumeMessageDir = getMessageDir(args.session_id)
|
||||
const resumeMessage = resumeMessageDir ? findNearestMessageWithFields(resumeMessageDir) : null
|
||||
resumeAgent = resumeMessage?.agent
|
||||
resumeModel = resumeMessage?.model?.providerID && resumeMessage?.model?.modelID
|
||||
@@ -360,7 +364,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
}
|
||||
|
||||
await client.session.prompt({
|
||||
path: { id: args.resume },
|
||||
path: { id: args.session_id },
|
||||
body: {
|
||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||
...(resumeModel !== undefined ? { model: resumeModel } : {}),
|
||||
@@ -378,7 +382,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
|
||||
return `Failed to send resume prompt: ${errorMessage}\n\nSession ID: ${args.resume}`
|
||||
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
|
||||
}
|
||||
|
||||
// Wait for message stability after prompt completes
|
||||
@@ -395,7 +399,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
const elapsed = Date.now() - pollStart
|
||||
if (elapsed < MIN_STABILITY_TIME_MS) continue
|
||||
|
||||
const messagesCheck = await client.session.messages({ path: { id: args.resume } })
|
||||
const messagesCheck = await client.session.messages({ path: { id: args.session_id } })
|
||||
const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array<unknown>
|
||||
const currentMsgCount = msgs.length
|
||||
|
||||
@@ -409,14 +413,14 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
}
|
||||
|
||||
const messagesResult = await client.session.messages({
|
||||
path: { id: args.resume },
|
||||
path: { id: args.session_id },
|
||||
})
|
||||
|
||||
if (messagesResult.error) {
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
return `Error fetching result: ${messagesResult.error}\n\nSession ID: ${args.resume}`
|
||||
return `Error fetching result: ${messagesResult.error}\n\nSession ID: ${args.session_id}`
|
||||
}
|
||||
|
||||
const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as Array<{
|
||||
@@ -434,7 +438,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
}
|
||||
|
||||
if (!lastMessage) {
|
||||
return `No assistant response found.\n\nSession ID: ${args.resume}`
|
||||
return `No assistant response found.\n\nSession ID: ${args.session_id}`
|
||||
}
|
||||
|
||||
// Extract text from both "text" and "reasoning" parts (thinking models use "reasoning")
|
||||
@@ -443,16 +447,16 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
|
||||
const duration = formatDuration(startTime)
|
||||
|
||||
return `Task resumed and completed in ${duration}.
|
||||
return `Task continued and completed in ${duration}.
|
||||
|
||||
Session ID: ${args.resume}
|
||||
Session ID: ${args.session_id}
|
||||
|
||||
---
|
||||
|
||||
${textContent || "(No text output)"}
|
||||
|
||||
---
|
||||
To resume this session: resume="${args.resume}"`
|
||||
To continue this session: session_id="${args.session_id}"`
|
||||
}
|
||||
|
||||
if (args.category && args.subagent_type) {
|
||||
@@ -618,6 +622,7 @@ To resume this session: resume="${args.resume}"`
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: sessionID,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -705,7 +710,7 @@ RESULT:
|
||||
${textContent || "(No text output)"}
|
||||
|
||||
---
|
||||
To resume this session: resume="${sessionID}"`
|
||||
To continue this session: session_id="${sessionID}"`
|
||||
} catch (error) {
|
||||
return formatDetailedError(error, {
|
||||
operation: "Launch monitored background task",
|
||||
@@ -788,6 +793,7 @@ Sisyphus-Junior is spawned automatically when you specify a category. Pick the a
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: task.sessionID,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -800,7 +806,7 @@ Agent: ${task.agent}${args.category ? ` (category: ${args.category})` : ""}
|
||||
Status: ${task.status}
|
||||
|
||||
System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check.
|
||||
To resume this session: resume="${task.sessionID}"`
|
||||
To continue this session: session_id="${task.sessionID}"`
|
||||
} catch (error) {
|
||||
return formatDetailedError(error, {
|
||||
operation: "Launch background task",
|
||||
@@ -864,6 +870,7 @@ To resume this session: resume="${task.sessionID}"`
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: sessionID,
|
||||
sync: true,
|
||||
command: args.command,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1018,7 +1025,7 @@ Session ID: ${sessionID}
|
||||
${textContent || "(No text output)"}
|
||||
|
||||
---
|
||||
To resume this session: resume="${sessionID}"`
|
||||
To continue this session: session_id="${sessionID}"`
|
||||
} catch (error) {
|
||||
if (toastManager && taskId !== undefined) {
|
||||
toastManager.removeTask(taskId)
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface DelegateTaskArgs {
|
||||
category?: string
|
||||
subagent_type?: string
|
||||
run_in_background: boolean
|
||||
resume?: string
|
||||
session_id?: string
|
||||
command?: string
|
||||
load_skills: string[]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export { sessionExists } from "./session-manager/storage"
|
||||
|
||||
export { interactive_bash, startBackgroundCheck as startTmuxCheck } from "./interactive-bash"
|
||||
export { createSkillTool } from "./skill"
|
||||
export { getTmuxPath } from "./interactive-bash/utils"
|
||||
export { createSkillMcpTool } from "./skill-mcp"
|
||||
|
||||
import {
|
||||
@@ -45,7 +44,7 @@ type OpencodeClient = PluginInput["client"]
|
||||
|
||||
export { createCallOmoAgent } from "./call-omo-agent"
|
||||
export { createLookAt } from "./look-at"
|
||||
export { createDelegateTask, type DelegateTaskToolOptions, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS } from "./delegate-task"
|
||||
export { createDelegateTask } from "./delegate-task"
|
||||
|
||||
export function createBackgroundTools(manager: BackgroundManager, client: OpencodeClient): Record<string, ToolDefinition> {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CommandFrontmatter } from "../../features/claude-code-command-load
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import { discoverAllSkills, type LoadedSkill } from "../../features/opencode-skill-loader"
|
||||
import { loadBuiltinCommands } from "../../features/builtin-commands"
|
||||
import type { CommandScope, CommandMetadata, CommandInfo, SlashcommandToolOptions } from "./types"
|
||||
|
||||
function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): CommandInfo[] {
|
||||
@@ -63,7 +64,22 @@ export function discoverCommandsSync(): CommandInfo[] {
|
||||
const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project")
|
||||
const opencodeProjectCommands = discoverCommandsFromDir(opencodeProjectDir, "opencode-project")
|
||||
|
||||
return [...opencodeProjectCommands, ...projectCommands, ...opencodeGlobalCommands, ...userCommands]
|
||||
const builtinCommandsMap = loadBuiltinCommands()
|
||||
const builtinCommands: CommandInfo[] = Object.values(builtinCommandsMap).map(cmd => ({
|
||||
name: cmd.name,
|
||||
metadata: {
|
||||
name: cmd.name,
|
||||
description: cmd.description || "",
|
||||
argumentHint: cmd.argumentHint,
|
||||
model: cmd.model,
|
||||
agent: cmd.agent,
|
||||
subtask: cmd.subtask
|
||||
},
|
||||
content: cmd.template,
|
||||
scope: "builtin"
|
||||
}))
|
||||
|
||||
return [...builtinCommands, ...opencodeProjectCommands, ...projectCommands, ...opencodeGlobalCommands, ...userCommands]
|
||||
}
|
||||
|
||||
function skillToCommandInfo(skill: LoadedSkill): CommandInfo {
|
||||
@@ -234,7 +250,7 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T
|
||||
if (partialMatches.length > 0) {
|
||||
const matchList = partialMatches.map((cmd) => `/${cmd.name}`).join(", ")
|
||||
return (
|
||||
`No exact match for "/${cmdName}". Did you mean: ${matchList}?\n\n` +
|
||||
`No exact match for "/${cmdName}\". Did you mean: ${matchList}?\n\n` +
|
||||
formatCommandList(allItems)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user