Compare commits

...

243 Commits

Author SHA1 Message Date
github-actions[bot]
a06e656565 release: v3.1.6 2026-01-28 16:15:27 +00:00
justsisyphus
30ed086c40 fix(delegate-task): use category default model when availableModels is empty 2026-01-29 01:11:42 +09:00
justsisyphus
7c15b06da7 fix(test): update tests to reflect new model-resolver behavior 2026-01-29 00:54:16 +09:00
justsisyphus
0e7ee2ac30 chore: remove noisy console.warn for AGENTS.md auto-disable 2026-01-29 00:46:16 +09:00
justsisyphus
ca93d2f0fe fix(model-resolver): skip fallback chain when model availability cannot be verified
When model cache is empty, the fallback chain resolution was blindly
trusting connected providers without verifying if the model actually
exists. This caused errors when a provider (e.g., opencode) was marked
as connected but didn't have the requested model (e.g., claude-haiku-4-5).

Now skips fallback chain entirely when model cache is unavailable and
falls through to system default, letting OpenCode handle the resolution.
2026-01-29 00:15:57 +09:00
YeonGyu-Kim
3ab4529bc7 fix(look-at): handle JSON parse errors from session.prompt gracefully (#1216)
When multimodal-looker agent returns empty/malformed response, the SDK
throws 'JSON Parse error: Unexpected EOF'. This commit adds try-catch
around session.prompt() to provide user-friendly error message with
troubleshooting guidance.

- Add error handling for JSON parse errors with detailed guidance
- Add error handling for generic prompt failures
- Add test cases for both error scenarios
2026-01-28 23:58:01 +09:00
github-actions[bot]
9d3e152b19 @KennyDizi has signed the CLA in code-yeongyu/oh-my-opencode#1214 2026-01-28 14:26:21 +00:00
github-actions[bot]
68c8f3dda7 release: v3.1.5 2026-01-28 14:15:42 +00:00
justsisyphus
03f6e72c9b refactor(ultrawork): replace prometheus with plan agent, add parallel task graph output
- Change all prometheus references to plan agent in ultrawork mode
- Add MANDATORY OUTPUT section to ULTRAWORK_PLANNER_SECTION:
  - Parallel Execution Waves structure
  - Dependency Matrix format
  - TODO List with category + skills + parallel group
  - Agent Dispatch Summary table
- Plan agent now outputs parallel task graphs for orchestrator execution
2026-01-28 23:09:51 +09:00
justsisyphus
4fd9f0fd04 refactor(agents): enforce zero user intervention in QA/acceptance criteria
- Prometheus: rename 'Manual QA' to 'Automated Verification Only'
- Prometheus: add explicit ZERO USER INTERVENTION principle
- Prometheus: replace placeholder examples with concrete executable commands
- Metis: add QA automation directives in output format
- Metis: strengthen CRITICAL RULES to forbid user-intervention criteria
2026-01-28 23:00:55 +09:00
github-actions[bot]
4413336724 @youming-ai has signed the CLA in code-yeongyu/oh-my-opencode#1203 2026-01-28 13:04:28 +00:00
Doyoon Kwon
895f366a11 docs: add Ollama streaming NDJSON issue guide and workaround (#1197)
* docs: add Ollama streaming NDJSON issue troubleshooting guide

- Document problem: JSON Parse error when using Ollama with stream: true
- Explain root cause: NDJSON vs single JSON object mismatch
- Provide 3 solutions: disable streaming, avoid tool agents, wait for SDK fix
- Include NDJSON parsing code example for SDK maintainers
- Add curl testing command for verification
- Link to issue #1124 and Ollama API docs

Fixes #1124

* docs: add Ollama provider configuration with streaming workaround

- Add Ollama Provider section to configurations.md
- Document stream: false requirement for Ollama
- Explain NDJSON vs single JSON mismatch
- Provide supported models table (qwen3-coder, ministral-3, lfm2.5-thinking)
- Add troubleshooting steps and curl test command
- Link to troubleshooting guide

feat: add NDJSON parser utility for Ollama streaming responses

- Create src/shared/ollama-ndjson-parser.ts
- Implement parseOllamaStreamResponse() for merging NDJSON lines
- Implement isNDJSONResponse() for format detection
- Add TypeScript interfaces for Ollama message structures
- Include JSDoc with usage examples
- Handle edge cases: malformed lines, stats aggregation

This utility can be contributed to Claude Code SDK for proper NDJSON support.

Related to #1124

* fix: use logger instead of console, remove trailing whitespace

- Replace console.warn with log() from shared/logger
- Remove trailing whitespace from troubleshooting guide
- Ensure TypeScript compatibility
2026-01-28 19:01:33 +09:00
YeonGyu-Kim
acc19fcd41 feat(hooks): auto-disable directory-agents-injector for OpenCode 1.1.37+ native support (#1204)
* feat(delegate-task): add prometheus self-delegation block and delegate_task permission

- Block prometheus from delegating to itself via delegate_task
- Grant delegate_task permission to prometheus when called as subagent
- Other subagents still have delegate_task disabled

* feat(version): add OPENCODE_NATIVE_AGENTS_INJECTION_VERSION constant

* docs: add deprecation notes for directory-agents-injector

* feat(hooks): auto-disable directory-agents-injector for OpenCode 1.1.37+

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-28 18:46:51 +09:00
justsisyphus
68e0a32183 chore(issue-templates): add English language requirement checkbox 2026-01-28 18:24:15 +09:00
justsisyphus
dee89c1556 feat(delegate-task): add prometheus self-delegation block and delegate_task permission
- Block prometheus from delegating to itself via delegate_task
- Grant delegate_task permission to prometheus when called as subagent
- Other subagents still have delegate_task disabled
2026-01-28 18:24:15 +09:00
github-actions[bot]
315c75c51e @rooftop-Owl has signed the CLA in code-yeongyu/oh-my-opencode#1197 2026-01-28 08:47:09 +00:00
YeonGyu-Kim
3dd80889a5 fix(tools): add permission field to session.create() for consistency (#1192) (#1199)
- Add permission field to look_at and call_omo_agent session.create()
- Match pattern used in delegate_task and background-agent
- Add better error messages for Unauthorized failures
- Provide actionable guidance in error messages

This addresses potential session creation failures by ensuring
consistent session configuration across all tools that create
child sessions.
2026-01-28 17:35:25 +09:00
Sisyphus
8f6ed5b20f fix(hooks): add null guard for tool.execute.after output (#1054)
/review command and some Claude Code built-in commands trigger
tool.execute.after hooks with undefined output, causing crashes
when accessing output.metadata or output.output.

Fixes #1035

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
2026-01-28 16:26:40 +09:00
TheEpTic
01500f1ebe Fix: prevent system-reminder tags from triggering mode keywords (#1155)
Automated system messages with <system-reminder> tags were incorrectly
triggering [search-mode], [analyze-mode], and other keyword modes when
they contained words like "search", "find", "explore", etc.

Changes:
- Add removeSystemReminders() to strip <system-reminder> content before keyword detection
- Add hasSystemReminder() utility function
- Update keyword-detector to clean text before pattern matching
- Add comprehensive test coverage for system-reminder filtering

Fixes issue where automated system notifications caused agents to
incorrectly enter MAXIMUM SEARCH EFFORT mode.

Co-authored-by: TheEpTic <git@eptic.me>
2026-01-28 16:26:37 +09:00
Thanh Nguyen
48f6c5e06d fix(skill): support YAML array format for allowed-tools field (#1163)
Fixes #1021

The allowed-tools field in skill frontmatter now supports both formats:
- Space-separated string: 'allowed-tools: Read Write Edit Bash'
- YAML array: 'allowed-tools: [Read, Write, Edit, Bash]'
- Multi-line YAML array format also works

Previously, skills using YAML array format would silently fail to parse,
causing them to not appear in the <available_skills> list.

Changes:
- Updated parseAllowedTools() in loader.ts, async-loader.ts, and merger.ts
  to handle both string and string[] types
- Updated SkillMetadata type to accept string | string[] for allowed-tools
- Added 4 test cases covering all allowed-tools formats
2026-01-28 16:26:34 +09:00
Moha Abdi
3e32afe646 fix(agent-variant): resolve variant based on current model, not static config (#1179) 2026-01-28 16:26:31 +09:00
Xiaoya Wang
d11c4a1f81 fix: guard JSON.parse(result.stdout) with || "{}" fallback in hook handlers (#1191)
Co-authored-by: wangxiaoya.2000 <wangxiaoya.2000@bytedance.com>
2026-01-28 16:26:28 +09:00
github-actions[bot]
5558ddf468 release: v3.1.4 2026-01-28 07:22:03 +00:00
justsisyphus
aa03d9b811 ci: sync publish.yml test isolation with ci.yml 2026-01-28 16:18:21 +09:00
YeonGyu-Kim
28a0dd06c7 fix: resolve version detection for npm global installations (#1194)
When oh-my-opencode is installed via npm global install and run as a
compiled binary, import.meta.url returns a virtual bun path ($bunfs)
instead of the actual filesystem path. This caused getCachedVersion()
to return null, resulting in 'unknown' version display.

Add fallback using process.execPath which correctly points to the actual
binary location, allowing us to walk up and find the package.json.

Fixes #1182
2026-01-28 15:54:17 +09:00
YeonGyu-Kim
995b7751af ci(cla): add repository owner to CLA allowlist (#1195)
The repository owner (code-yeongyu) was not in the CLA allowlist,
causing CLA signature requirement on their own PRs.

Added code-yeongyu to the allowlist to skip CLA for owner commits.

Co-authored-by: 김연규 <yeongyu@mengmotaMacbookAir.local>
2026-01-28 15:46:42 +09:00
justsisyphus
5087788f66 ci: split test execution to prevent mock.module pollution 2026-01-28 15:06:32 +09:00
justsisyphus
19524c8a27 ci: run tests sequentially to prevent mock.module pollution 2026-01-28 14:59:26 +09:00
justsisyphus
fbb4d46945 fix: explicit reset in mainSessionID test for parallel test safety 2026-01-28 14:40:15 +09:00
justsisyphus
5dc8d577a4 fix: add afterEach cleanup in session-state tests for parallel test isolation 2026-01-28 14:36:58 +09:00
justsisyphus
c249763d7e fix: reset sessionAgentMap in _resetForTesting for test isolation
- Add sessionAgentMap.clear() to _resetForTesting()
- Prevents test pollution when tests run in parallel in CI
2026-01-28 14:33:14 +09:00
justsisyphus
b2d618e851 fix: mock provider cache in delegate-task tests for CI stability
- Add spyOn for readConnectedProvidersCache to return connected providers
- Tests now work consistently regardless of actual provider cache state
- Fixes CI failures for category variant and unstable agent tests
2026-01-28 14:27:34 +09:00
justsisyphus
6f348a8a5c fix: resolve CI test timeouts with configurable timing
- Add timing.ts module for test-only timing configuration
- Replace hardcoded wait times with getTimingConfig()
- Enable all previously skipped tests (ralph-loop, session-state, delegate-task)
- Tests now complete in ~2s instead of timing out
2026-01-28 14:17:56 +09:00
justsisyphus
1da0adcbe8 feat(index): add provider cache missing warning toast
Show warning toast when hasConnectedProvidersCache() returns false,
indicating model filtering is disabled. Prompts user to restart
OpenCode for full functionality.
2026-01-28 13:31:11 +09:00
justsisyphus
8a9d966a3d fix(model-resolver): skip fallback chain when no cache exists
When no provider cache exists, skip the fallback chain entirely and let
OpenCode use Provider.defaultModel() as the final fallback. This prevents
incorrect model selection when the plugin loads before providers connect.

- Remove forced first-entry fallback when no cache
- Add log messages for cache miss scenarios
- Update tests for new behavior
2026-01-28 13:31:03 +09:00
justsisyphus
76f8c500cb fix(config): add 'dev-browser' to BrowserAutomationProviderSchema
Config validation was failing when 'dev-browser' was set as the browser
automation provider, causing the entire config to be rejected. This
silently disabled all config options including tmux.enabled.

- Add 'dev-browser' as valid option in BrowserAutomationProviderSchema
- Update JSDoc with dev-browser description
- Regenerate JSON schema
2026-01-28 12:05:20 +09:00
github-actions[bot]
388516bcc5 @agno01 has signed the CLA in code-yeongyu/oh-my-opencode#1188 2026-01-28 01:02:15 +00:00
github-actions[bot]
8dff875929 @zycaskevin has signed the CLA in code-yeongyu/oh-my-opencode#1184 2026-01-27 16:20:49 +00:00
github-actions[bot]
966cc90a02 release: v3.1.3 2026-01-27 16:12:43 +00:00
justsisyphus
1d27d78127 test: skip flaky sync variant test (CI timeout) 2026-01-28 01:07:14 +09:00
justsisyphus
38156d49f3 ci: use find/xargs to exclude mock-heavy test files 2026-01-28 01:01:45 +09:00
justsisyphus
897eea0263 ci: isolate mock-heavy test files to prevent parallel pollution 2026-01-28 01:00:17 +09:00
justsisyphus
9b59ef66e4 test: fix flaky tests caused by mock.module pollution across parallel test files 2026-01-28 00:54:20 +09:00
github-actions[bot]
0d938059f9 @moha-abdi has signed the CLA in code-yeongyu/oh-my-opencode#1179 2026-01-27 12:36:31 +00:00
github-actions[bot]
9d35f23725 @MoerAI has signed the CLA in code-yeongyu/oh-my-opencode#1172 2026-01-27 09:31:52 +00:00
justsisyphus
aa1646f82c fix(delegate-task): pass variant as top-level field in prompt body
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-27 17:54:58 +09:00
justsisyphus
e47ab084fd fix(keyword-detector): skip ultrawork injection for planner agents
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-27 17:54:52 +09:00
justsisyphus
baf6358736 fix(background-agent): pass variant as top-level field in prompt body 2026-01-27 16:49:03 +09:00
justsisyphus
488c89156b test(config-handler): add tests for plan demote and prometheus mode 2026-01-27 16:06:03 +09:00
justsisyphus
c4957a469d fix(prometheus): set mode to 'all' and restore plan demote logic
- Change prometheus mode from 'primary' to 'all' to allow delegate_task calls
- Restore plan agent demote logic to use prometheus config as base
- Revert d481c596 changes that broke plan agent inheritance
2026-01-27 15:57:45 +09:00
justsisyphus
d481c596bd fix(plan-agent): only inherit model from prometheus as fallback
Plan agent was incorrectly inheriting prometheus's entire config (prompt,
permission, etc.) causing it to behave as primary instead of subagent.

Now plan agent:
1. Uses plan config model if explicitly set
2. Falls back to prometheus model only if plan config has no model
3. Keeps original OpenCode plan config intact
2026-01-27 15:18:28 +09:00
justsisyphus
655d511294 Revert "docs: add v2.x to v3.x migration guide (#1057)"
This PR was incorrectly merged by AI agent without proper project owner review.

This reverts commit 1cb6b3de39a49acb43b76ac55a5b44b47ca4a9f7.
2026-01-27 14:09:37 +09:00
justsisyphus
7dedd6cf90 Revert "Add oh-my-opencode-slim (#1100)"
This PR was incorrectly merged by AI agent without proper project owner review.

The AI evaluated this as 'ULTRA SAFE' because it only modified README files,
but failed to recognize that adding external fork promotions to the project
README requires explicit project owner approval - not just technical safety.

This reverts commit 912a56db85.
2026-01-27 14:09:18 +09:00
justsisyphus
bd18f231f5 feat(sisyphus): add foundation schemas for tasks and swarm (Wave 1)
- Add SisyphusTasksConfig and SisyphusSwarmConfig to schema.ts
- Create Task JSON schema with Zod validation
- Create Mailbox IPC protocol message schemas
- Add storage utilities with Claude Code path compatibility
- 25 tests passing
2026-01-27 13:07:09 +09:00
justsisyphus
de439edc22 feat(subagent): block question tool at both SDK and hook level
- Add permission: [{ permission: 'question', action: 'deny' }] to session.create()
  in background-agent and delegate-task for SDK-level blocking
- Add subagent-question-blocker hook as backup layer to intercept question tool
  calls in tool.execute.before event
- Ensures subagents cannot ask questions to users and must work autonomously
2026-01-27 13:07:09 +09:00
github-actions[bot]
04500bae7d @code-yeongyu has signed the CLA in code-yeongyu/oh-my-opencode#1100 2026-01-27 02:59:24 +00:00
Sisyphus
1cb6b3de7d docs: add v2.x to v3.x migration guide (#1057)
Comprehensive migration guide covering:
- TL;DR quick upgrade section for most users
- What's new in v3.x (Atlas, Prometheus, categories, skills)
- Breaking changes checklist (high/medium/low impact)
- Step-by-step upgrade path
- Configuration changes (categories, permissions)
- API changes for plugin developers
- Troubleshooting common issues
- Complete agent and category reference

Consulted Oracle for migration guide strategy and structure.

Closes #1034 (item 4)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
2026-01-27 11:59:15 +09:00
Alvin
912a56db85 Add oh-my-opencode-slim (#1100) 2026-01-27 11:59:12 +09:00
itsmylife44
a5d9929c0a feat: support OPENCODE_SERVER_PORT and OPENCODE_SERVER_HOSTNAME env vars (#1157)
Add support for customizing the OpenCode server port and hostname via
environment variables. This enables orchestration tools like Open Agent
to run multiple concurrent missions without port conflicts.

Environment variables:
- OPENCODE_SERVER_PORT: Custom port for the OpenCode server
- OPENCODE_SERVER_HOSTNAME: Custom hostname for the OpenCode server

When running oh-my-opencode in parallel (e.g., multiple missions in
Open Agent), each instance can now use a unique port to avoid conflicts
with the default port 4096.
2026-01-27 11:59:10 +09:00
vmlinuzx
7f43f160b5 docs: clarify category model resolution priority and fallback behavior (#1074)
The previous documentation implied that categories automatically use their
built-in default models (e.g., Gemini for visual, GPT-5.2 for ultrabrain).

This was misleading. Categories only use built-in defaults if explicitly
configured. Otherwise, they fall back to the system default model.

Changes:
- Add explicit warning about model resolution priority
- Document all 7 built-in categories (was only showing 2)
- Show complete example config with all categories
- Explain the wasteful fallback scenario
- Add 'variant' to supported category options

Fixes confusion where users expect optimized model selection but get
system default for all unconfigured categories.

Co-authored-by: DC <vmlinux@p16.tailnet.freeflight.co>
2026-01-27 11:58:59 +09:00
0ln
af67bc8592 fix(mcp): add optional Context7 Authorization header (#1133)
Context7 should mirror `websearch` by only sending auth when
`CONTEXT7_API_KEY` is set.

Change: set bearer auth in `headers` using `CONTEXT7_API_KEY` if said environment variable is set, otherwise leave `headers` to `undefined`.
2026-01-27 11:58:55 +09:00
Peter Rallojay
c74d79e28a fix: prevent builtin MCPs from overwriting user MCP configs (#956) 2026-01-27 11:58:42 +09:00
justsisyphus
fc5298d778 feat(workflow): add ZAI Coding + OpenAI provider for sisyphus-agent
- Add zai-coding-plan provider with GLM 4.7 and GLM 4.6v models
- Add OpenAI provider with GPT-5.2 models
- Configure unspecified-low category to use zai-coding-plan/glm-4.7
- Auth is provided via OPENCODE_AUTH_JSON secret
2026-01-27 10:51:24 +09:00
justsisyphus
3e8e3db961 feat(prompts): enhance plan output with TL;DR, agent profiles, and parallelization
- prometheus-prompt: Add TL;DR section with quick summary, deliverables, effort estimate
- prometheus-prompt: Add recommended agent profile (category + skills) per task
- prometheus-prompt: Enhance parallelization with execution waves and dependency matrix
- ultrawork: Change plan agent to prometheus agent invocation
- ultrawork: Add session_id resume workflow for Prometheus iteration
2026-01-27 10:50:38 +09:00
justsisyphus
6fa5cac616 fix(compaction): preserve agent verification state (#1144) 2026-01-27 10:35:20 +09:00
justsisyphus
158ccabf24 fix(notification): prevent false positive plugin detection (#1148) 2026-01-27 10:35:20 +09:00
justsisyphus
2efbf2650f fix(cli): add baseline builds for non-AVX2 CPUs (#1154) 2026-01-27 10:35:20 +09:00
justsisyphus
acded4ba2a fix(delegate-task): add clear error when model not configured (#1139) 2026-01-27 10:35:20 +09:00
github-actions[bot]
911e43445f @ghtndl has signed the CLA in code-yeongyu/oh-my-opencode#1158 2026-01-27 01:27:26 +00:00
sisyphus-dev-ai
3049e1ebfb chore: changes by sisyphus-dev-ai 2026-01-27 01:10:31 +00:00
github-actions[bot]
62921b9e44 release: v3.1.2 2026-01-27 01:07:09 +00:00
github-actions[bot]
cd23f7ab7d release: v3.1.1 2026-01-26 23:48:28 +00:00
justsisyphus
518dceac72 Revert "feat(librarian): conditionally enable thinking based on model type"
This reverts commit f033b30549a396db90e148756130cddec1fcdb2b.
2026-01-27 08:39:45 +09:00
justsisyphus
19f43e30c8 feat(librarian): conditionally enable thinking based on model type
- Add isGeminiModel helper to detect Gemini models
- Disable thinking config for Gemini models (not supported)
- Enable thinking with 32000 token budget for other models
- Add tests verifying both Gemini and Claude behavior

🤖 Generated with assistance of OhMyOpenCode
2026-01-27 08:39:45 +09:00
justsisyphus
b3be9f33c6 feat(ultrawork): enforce plan agent invocation and parallel delegation
- Add MANDATORY section for delegate_task(subagent_type='plan') at top of ultrawork prompt
- Establish 'DELEGATE by default, work yourself only when trivial' principle
- Add parallel execution rules with anti-pattern and correct pattern examples
- Remove emoji (checkmark/cross) from PLAN_AGENT_SYSTEM_PREPEND
- Restructure workflow into clear 4-step sequence
2026-01-27 08:39:45 +09:00
github-actions[bot]
430098856a @itsmylife44 has signed the CLA in code-yeongyu/oh-my-opencode#1157 2026-01-26 23:20:52 +00:00
github-actions[bot]
5932f5f94f @acamq has signed the CLA in code-yeongyu/oh-my-opencode#1151 2026-01-26 18:20:30 +00:00
github-actions[bot]
fcf2e32071 @craftaholic has signed the CLA in code-yeongyu/oh-my-opencode#1110 2026-01-26 16:12:39 +00:00
github-actions[bot]
19827dac70 @orientpine has signed the CLA in code-yeongyu/oh-my-opencode#1145 2026-01-26 14:30:44 +00:00
github-actions[bot]
3ed1c6644e @Jeremy-Kr has signed the CLA in code-yeongyu/oh-my-opencode#1141 2026-01-26 11:59:22 +00:00
justsisyphus
cf6e714946 feat(plan-agent): apply prometheus config to plan agent with fallback chain
- Add prometheus model fallback chain (claude-opus-4-5 → gpt-5.2 → gemini-3-pro)
- Plan agent now inherits prometheus settings (model, prompt, permission, variant)
- Plan agent mode remains 'subagent' while using prometheus config
- Add name field to prometheus config to fix agent.name undefined error
2026-01-26 18:31:48 +09:00
justsisyphus
383f43548b feat(plan-agent): enforce dependency/parallel graphs and category+skill recommendations
Add mandatory sections to PLAN_AGENT_SYSTEM_PREPEND:
- Task Dependency Graph with blockers/dependents/reasons
- Parallel Execution Graph with wave structure
- Category + Skills recommendations per task
- Response format specification with exact structure

Uses ASCII art banners and visual emphasis for critical requirements.
2026-01-26 18:31:35 +09:00
justsisyphus
26b1c67964 fix(background-agent): disable question tool for background tasks 2026-01-26 18:25:06 +09:00
justsisyphus
7e065dfe12 feat(delegate-task): prepend system prompt for plan agent invocations
When plan agent (plan/prometheus/planner) is invoked via delegate_task,
automatically prepend a <system> prompt instructing the agent to:
- Launch explore/librarian agents in background to gather context
- Summarize user request and list uncertainties
- Ask clarifying questions until requirements are 100% clear
2026-01-26 18:25:06 +09:00
justsisyphus
8429da02b8 feat(config): add thinking/reasoningEffort/providerOptions to AgentOverrideConfigSchema
- Add maxTokens, thinking, reasoningEffort, textVerbosity, providerOptions fields to AgentOverrideConfigSchema
- Update think-mode hook to respect agent-level thinking settings (disabled or custom providerOptions)
- Add tests for agent-level thinking configuration override behavior
2026-01-26 18:25:06 +09:00
github-actions[bot]
ab51f5d39f @boguan has signed the CLA in code-yeongyu/oh-my-opencode#1137 2026-01-26 08:46:14 +00:00
justsisyphus
3ee519c7b0 feat: make systemDefaultModel optional for OpenCode fallback (#1136)
- Remove mandatory model requirement from plugin initialization
- Allow OpenCode to use its built-in model fallback when user doesn't specify
- Update model-resolver to handle undefined systemDefaultModel
- Remove throw errors in config-handler, utils, atlas, delegate-task
- Add tests for optional model scenarios

Closes #1129

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-26 17:01:08 +09:00
justsisyphus
c9b86b7815 test(cli): add version display test to verify package.json reading (#1134)
Closes #1063

Investigation findings:
- The CLI code correctly reads version from package.json
- The reported issue (bunx showing old version) is a caching issue
- Added test to ensure version is read as valid semver from package.json

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-26 17:00:55 +09:00
github-actions[bot]
9b6d8f629a @misyuari has signed the CLA in code-yeongyu/oh-my-opencode#1132 2026-01-26 07:31:12 +00:00
justsisyphus
6a2f43858a docs: add server mode and shell function examples for tmux integration
- Add --port flag requirement for tmux subagent pane spawning
- Add Fish shell function example with automatic port allocation
- Add Bash/Zsh equivalent function example
- Document how subagent panes work (opencode attach flow)
- Add OPENCODE_PORT environment variable documentation
- Add server mode reference section with opencode serve command
2026-01-26 16:24:14 +09:00
justsisyphus
601ea32a1c docs: add tmux integration and interactive terminal documentation
- Add Tmux Integration section to configurations.md with all config options
- Add Visual Multi-Agent with Tmux subsection to features.md
- Add Interactive Terminal Tools section documenting interactive_bash tool
2026-01-26 16:02:34 +09:00
github-actions[bot]
8f31211c75 release: v3.1.0 2026-01-26 06:46:47 +00:00
justsisyphus
04f2b513c6 feat(tmux-subagent): add replace action to prevent mass eviction
- Add column-based splittable calculation (getColumnCount, getColumnWidth)
- New decision tree: splittable → split, k=1 eviction → close+spawn, else → replace
- Add 'replace' action type using tmux respawn-pane (preserves layout)
- Replace oldest pane in-place instead of closing all panes when unsplittable
- Prevents scenario where all agent panes get closed leaving only 1
2026-01-26 15:25:11 +09:00
justsisyphus
8ebc933118 fix(tmux-subagent): enable 2D grid layout with divider-aware calculations
- Account for tmux pane dividers (1 char) in all size calculations
- Reduce MIN_PANE_WIDTH from 53 to 52 to fit 2 columns in standard terminals
- Fix enforceMainPaneWidth to use (windowWidth - divider) / 2
- Add virtual mainPane handling for close-spawn eviction loop
- Add comprehensive decision-engine tests (23 test cases)
2026-01-26 15:11:16 +09:00
justsisyphus
a67a35aea8 docs: regenerate AGENTS.md knowledge base via /init-deep 2026-01-26 14:56:55 +09:00
justsisyphus
9d66b80709 feat(hooks): add active working context section to compaction summary
Include files, code in progress, external references, and state/variables
in compaction summary for seamless continuation after context compaction.
2026-01-26 14:23:05 +09:00
justsisyphus
5c7eb02d5b chore(test): sync agent name casing in tests (#1128)
Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-26 12:10:30 +09:00
justsisyphus
68aa913499 refactor(tmux-subagent): state-first architecture with decision engine (#1125)
* refactor(tmux-subagent): add state-first architecture with decision engine

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(tmux): add pane spawn callbacks for background and sync sessions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-26 12:02:37 +09:00
justsisyphus
3a79b8761b feat(shared): add connected-providers-cache for model availability (#1121)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-26 11:53:41 +09:00
justsisyphus
da416b362b feat(hooks): add category-skill-reminder hook (#1123)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-26 11:48:32 +09:00
justsisyphus
90054b28ad chore(docs): regenerate AGENTS.md knowledge base (#1118)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-26 11:48:30 +09:00
justsisyphus
892b245779 fix(test): update builtin skills count from 3 to 4 (#1126)
* fix(test): update builtin skills count from 3 to 4 (dev-browser added)

* chore(ci): add block-master-pr workflow

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-26 02:29:28 +00:00
YeonGyu-Kim
aead4aebd2 Add tmux pane management for background agent sessions (#1094)
* feat(config): add TmuxConfigSchema for tmux subagent pane management

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(shared): add tmux module structure

* feat(shared/tmux): implement tmux pane utilities

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(tmux-subagent): add TmuxSessionManager tests (TDD RED)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(tmux-subagent): implement TmuxSessionManager

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(integration): wire TmuxSessionManager with 500ms delay

- Task 5: Add 500ms delay in BackgroundManager after session creation
- Task 6: Wire TmuxSessionManager event handlers (session.created/deleted)
- Both changes integrate tmux pane management into plugin lifecycle

Co-authored-by: Sisyphus <ultrawork@oh-my-opencode>

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Sisyphus <ultrawork@oh-my-opencode>
2026-01-25 15:34:10 +09:00
YeonGyu-Kim
bccc943173 feat(skills): add dev-browser skill with Windows support (#1093)
* feat(skills): add dev-browser skill with Windows support

* chore: trigger CI
2026-01-25 15:34:07 +09:00
justsisyphus
05904ca617 docs(agent-browser): add detailed installation guide with Playwright troubleshooting 2026-01-25 15:12:32 +09:00
YeonGyu-Kim
3af30b0a21 feat(skills): add agent-browser option for browser automation (#1090)
Add configurable browser automation allowing users to choose between
Playwright MCP (default) and Vercel's agent-browser CLI.

Changes:
- Add browser_automation_engine.provider config option
- Dynamic skill loading based on provider selection
- Comprehensive agent-browser CLI reference (inline in skills.ts)
- Propagate browserProvider to delegate_task and buildAgent
- Update documentation with provider comparison

Co-authored-by: Suyeol Jeon <devxoul@gmail.com>
Co-authored-by: YeonGyu Kim <code.yeongyu@gmail.com>
2026-01-25 15:02:41 +09:00
YeonGyu-Kim
b55fd8d76f feat(explore): add github-copilot/gpt-5-mini to fallback chain (#1091)
* feat(explore): add github-copilot/gpt-5-mini to fallback chain

* test(explore): add tests for github-copilot/gpt-5-mini fallback

---------

Co-authored-by: Suyeol Jeon <devxoul@gmail.com>
2026-01-25 05:53:11 +00:00
Sisyphus
208af055ef fix: generate skill/slashcommand descriptions synchronously when pre-provided (#1087)
* fix: generate skill/slashcommand tool descriptions synchronously when pre-provided

When skills are passed via options (pre-resolved), build the tool description
synchronously instead of fire-and-forget async. This eliminates the race
condition where the description getter returns the bare prefix before the
async cache-warming microtask completes.

Fixes #1039

* chore: changes by sisyphus-dev-ai

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
2026-01-25 14:52:50 +09:00
YeonGyu-Kim
0aa8f486af feat(hooks): add sisyphus-junior-notepad hook for conditional notepad rules injection (#1092)
* refactor(shared): extract isCallerOrchestrator to session-utils

* refactor(atlas): use shared isCallerOrchestrator, change to prepend

* refactor(prometheus-md-only): change to prepend pattern

* refactor(sisyphus-junior): remove Work_Context (moved to hook)

* feat(hooks): add sisyphus-junior-notepad hook

* fix(shared): replace dynamic require with static import in session-utils

- Change from dynamic require to static import for better bundler compatibility
- Fix import path: ../../features -> ../features
- Add barrel export to src/shared/index.ts

* feat(hooks): register sisyphus-junior-notepad hook

- Add to HookNameSchema in schema.ts
- Export from hooks/index.ts
- Register with isHookEnabled in index.ts
- Auto-generated schema.json update

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-25 14:52:11 +09:00
github-actions[bot]
a5db86ee15 release: v3.0.1 2026-01-25 05:04:20 +00:00
justsisyphus
14f450bd25 refactor: sync delegate_task schema with OpenCode Task tool (resume→session_id, add command param) 2026-01-25 13:57:45 +09:00
justsisyphus
5a1da39def refactor(ultrawork): replace vague plan agent references with explicit delegate_task(subagent_type="plan") invocation syntax 2026-01-25 13:57:45 +09:00
Sisyphus
24d065c43a fix: update documentation to use load_skills instead of skills parameter (#1088)
All documentation, agent prompts, and skill descriptions were still
referencing the old 'skills' parameter name for delegate_task, but the
tool implementation requires 'load_skills' (renamed in commit aa2b052).
This caused confusion and errors for users following the docs.

Fixes #1008

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
2026-01-25 13:45:00 +09:00
justsisyphus
fd72ce5ce7 docs: update AGENTS.md knowledge base (043b1a33)
- Add 7 missing hooks, remove deleted background-compaction
- Update line counts (atlas 572, sisyphus 450, config-manager 664)
- Add 18 undocumented shared utilities, remove stale references
- Add task-toast-manager, remove-deadcode command
- Update test count 90→95, add 4 complexity hotspots
2026-01-25 13:12:40 +09:00
justsisyphus
043b1a3377 refactor: remove dead re-exports from tools barrel (getTmuxPath, DelegateTaskToolOptions, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS) 2026-01-25 12:59:19 +09:00
justsisyphus
512952f66d refactor: remove deprecated config-path.ts (dead code, 0 references) 2026-01-25 12:58:40 +09:00
justsisyphus
d9723e76ab refactor: remove unused background-compaction hook module 2026-01-25 12:58:05 +09:00
justsisyphus
212baa6674 feat(commands): add /remove-deadcode slash command for LSP-verified dead code removal 2026-01-25 12:46:37 +09:00
justsisyphus
1c76e0513a fix: add missing name property in loadBuiltinCommands causing TypeError on slashcommand 2026-01-25 12:46:03 +09:00
justsisyphus
c8cc94cd3c fix: remove github-copilot association from gpt-5-nano model mapping
explore agent uses opencode/gpt-5-nano exclusively — github-copilot
should not be associated with gpt-5-nano in docs, tests, or fallback chains.
2026-01-25 12:46:03 +09:00
Sisyphus
20cca35157 fix(ralph-loop): skip user messages in transcript completion detection (#622) (#1086)
* fix(ralph-loop): skip user messages in transcript completion detection (#622)

The transcript-based completion detection was searching the entire JSONL
file for <promise>DONE</promise>, including user message entries. The
RALPH_LOOP_TEMPLATE instructional text contains this literal pattern,
which gets recorded as a user message, causing false positive completion
detection on every iteration. This made the loop always terminate at
iteration 1.

Fix: Parse JSONL entries line-by-line and skip entries with type 'user'
so only tool_result/assistant entries are checked for the completion
promise. Also remove the hardcoded <promise>DONE</promise> from the
template exit conditions as defense-in-depth.

* chore: changes by sisyphus-dev-ai

---------

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
2026-01-25 12:34:42 +09:00
sisyphus-dev-ai
81d27afadb chore: changes by sisyphus-dev-ai 2026-01-25 03:27:56 +00:00
github-actions[bot]
6cb2f3031c @kvokka has signed the CLA in code-yeongyu/oh-my-opencode#1084 2026-01-25 03:14:31 +00:00
github-actions[bot]
f116ea1d43 @potb has signed the CLA in code-yeongyu/oh-my-opencode#1083 2026-01-25 02:38:28 +00:00
github-actions[bot]
6aa0674000 @jsl9208 has signed the CLA in code-yeongyu/oh-my-opencode#1082 2026-01-24 21:44:22 +00:00
github-actions[bot]
2b828624a0 @sadnow has signed the CLA in code-yeongyu/oh-my-opencode#1080 2026-01-24 20:49:38 +00:00
github-actions[bot]
e60ccb93fb @ThanhNguyxn has signed the CLA in code-yeongyu/oh-my-opencode#1075 2026-01-24 17:42:03 +00:00
justsisyphus
aa244e8098 docs: fix atlas agent name case in example config 2026-01-24 22:46:40 +09:00
github-actions[bot]
6f60f03433 @AamiRobin has signed the CLA in code-yeongyu/oh-my-opencode#1067 2026-01-24 13:28:32 +00:00
github-actions[bot]
b8a0eee92d release: v3.0.0 2026-01-24 13:23:25 +00:00
justsisyphus
1486ebbc87 docs: update READMEs for 3.0 stable release
- Update TIP banner from beta.10 to stable 3.0 in all languages
- Add Korean language link to Japanese and Chinese READMEs
- Add DeepWiki badge to Japanese and Chinese READMEs
- Adjust DeepWiki badge position in Korean README for consistency
2026-01-24 21:58:53 +09:00
justsisyphus
063c759275 feat: show detailed task info and resume instructions on background_cancel(all=true) (#1062)
Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-24 17:15:31 +09:00
justsisyphus
6e9ebaf3ee fix: add missing gemini-3-flash to writing category migration (#1061)
MODEL_TO_CATEGORY_MAP was missing the mapping for google/gemini-3-flash
to the 'writing' category. Users who had configured agents with
model: 'google/gemini-3-flash' would not get auto-migrated to
category: 'writing'.

Ref: PR #1057 review comment

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-24 17:05:14 +09:00
justsisyphus
0e1d4e52e1 chore: remove website directory (fixes CI test failures) 2026-01-24 16:37:46 +09:00
sisyphus-dev-ai
c0fb4b79bd chore: changes by sisyphus-dev-ai 2026-01-24 07:12:01 +00:00
justsisyphus
ec32dd65c2 fix(question-label-truncator): fix type errors and add test coverage
- Remove invalid Pick<Plugin> type usage
- Add explicit input/output type annotations
- Add comprehensive test suite (5 tests)
- Tests verify truncation at 30 chars with '...' suffix
2026-01-24 16:07:08 +09:00
Ssoon-m
04fb339622 fix: add model fallback from agent/category configs 2026-01-24 16:03:12 +09:00
yimingll
3a22c24cf4 fix: auto-truncate question option labels exceeding 30 characters
When AI generates AskUserQuestion tool calls with option labels longer
than 30 characters, opencode validation rejects them with "too_big" error.

This fix adds a pre-tool-use hook that automatically truncates labels
to 30 characters (with "..." suffix) before the validation occurs.

Fixes the error:
"The question tool was called with invalid arguments: expected string
to have <=30 characters"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 15:59:45 +09:00
Stephen Wang
cf2320480f Fix MCP disabled flag not removing previously loaded servers (#985)
When a later-loaded MCP config (e.g., .claude/.mcp.json) marks a server
as disabled, it now properly removes that server from both the servers
object and loadedServers array.

Previously, disabled servers were only skipped during loading, which
meant they wouldn't override servers loaded from earlier configs. This
made it impossible to disable project-level MCPs using local overrides.

Now the disabled flag works as expected: local configs can disable
servers defined in project or user configs.
2026-01-24 15:55:59 +09:00
Rouven Hi!
9532680879 fix(slashcommand): include built-in commands (like start-work) in discovery (#1031)
This ensures that commands defined in src/features/builtin-commands/commands.ts
(like /start-work, /refactor, /init-deep) are visible to the slashcommand tool
and the agent. Previously, only markdown-based commands were discovered.
2026-01-24 15:55:31 +09:00
justsisyphus
2a945ddbf5 fix(background-task): pass config to BackgroundManager for concurrency limits
The background_task config (providerConcurrency, modelConcurrency, etc.)
was not being passed to BackgroundManager, causing all models to use
the hardcoded default limit of 5 instead of user-configured values.
2026-01-24 15:50:44 +09:00
justsisyphus
58bb92134d fix(todo-continuation): filter compaction agent to prevent infinite loop
- Add 'compaction' to DEFAULT_SKIP_AGENTS
- Skip compaction agent messages when resolving agent info
- Skip injection when compaction occurred but no real agent resolved
- Replace cooldown-based approach with agent-based filtering
2026-01-24 15:50:44 +09:00
Sungho Park
f1a279a10a Add xhigh reasoningEffort to config schema (#965)
* test: cover xhigh reasoningEffort

* feat: add xhigh reasoningEffort option

* test: make reasoningEffort xhigh test model-agnostic
2026-01-24 15:48:15 +09:00
YeonGyu-Kim
faf172a91d fix(multimodal-looker): update fallback chain order (#1050)
New order:
1. google/gemini-3-flash
2. openai/gpt-5.2
3. zai-coding-plan/glm-4.6v
4. anthropic/claude-haiku-4-5
5. opencode/gpt-5-nano (FREE, ultimate fallback)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-24 15:40:24 +09:00
YeonGyu-Kim
04633ba208 fix(models): update model names to match OpenCode Zen catalog (#1048)
* fix(models): update model names to match OpenCode Zen catalog

OpenCode Zen recently updated their official model catalog, deprecating
several preview and free model variants:

DEPRECATED → NEW (Official Zen Names):
- gemini-3-pro-preview → gemini-3-pro
- gemini-3-flash-preview → gemini-3-flash
- grok-code → gpt-5-nano (FREE tier maintained)
- glm-4.7-free → big-pickle (FREE tier maintained)
- glm-4.6v → glm-4.6

Changes:
- Updated 6 source files (model-requirements, delegate-task, think-mode, etc.)
- Updated 9 documentation files (installation, configurations, features, etc.)
- Updated 14 test files with new model references
- Regenerated snapshots to reflect catalog changes
- Removed duplicate think-mode entries for preview variants

Impact:
- FREE tier access preserved via gpt-5-nano and big-pickle
- All 55 model-related tests passing
- Zero breaking changes - pure string replacement
- Aligns codebase with official OpenCode Zen model catalog

Verified:
- Zero deprecated model names in codebase
- All model-related tests pass (55/55)
- Snapshots regenerated and validated

Affects: 30 files (6 source, 9 docs, 14 tests, 1 snapshot)

* fix(multimodal-looker): update fallback chain with glm-4.6v and gpt-5-nano

- Change glm-4.6 to glm-4.6v for zai-coding-plan provider
- Add opencode/gpt-5-nano as 4th fallback (FREE tier)
- Push gpt-5.2 to 5th position

Fallback chain now:
1. gemini-3-flash (google, github-copilot, opencode)
2. claude-haiku-4-5 (anthropic, github-copilot, opencode)
3. glm-4.6v (zai-coding-plan)
4. gpt-5-nano (opencode) - FREE
5. gpt-5.2 (openai, github-copilot, opencode)

* chore: update bun.lock

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-24 15:30:35 +09:00
justsisyphus
58459e692b feat(website): add layout with header, sidebar, footer and navigation
- Create Header component with logo, nav, theme toggle, language switcher
- Create Sidebar component with doc navigation from config
- Create Footer component
- Create MobileNav component with hamburger menu
- Create navigation config file (docsConfig)
- Integrate all layout components into [locale]/layout.tsx
- Add framer-motion for mobile nav animations
- All tests passing, build successful
2026-01-24 14:25:05 +09:00
justsisyphus
894a0fa849 feat(website): add next-intl i18n and dark mode support 2026-01-24 14:25:05 +09:00
justsisyphus
21c7d29c1d fix(website): resolve @opennextjs/cloudflare and test configuration issues
- Successfully installed @opennextjs/cloudflare v1.15.1
- Fixed Vitest configuration to exclude e2e tests
- Renamed e2e test files from .spec.ts to .e2e.ts to avoid Bun test runner conflicts
- Updated eslint.config.mjs and playwright.config.ts
- All tests passing: Vitest (1/1), Playwright (6/6)
- Production bundle size: ~5MB < 10MiB limit
- Marked TODO 0 complete in plan
2026-01-24 14:25:05 +09:00
justsisyphus
ba93c42943 feat(website): initialize Next.js 15 project with @opennextjs/cloudflare 2026-01-24 14:25:05 +09:00
github-actions[bot]
5c7dd40751 @AndersHsueh has signed the CLA in code-yeongyu/oh-my-opencode#1042 2026-01-24 04:41:56 +00:00
github-actions[bot]
acc7b8b2f7 @gongxh0901 has signed the CLA in code-yeongyu/oh-my-opencode#1037 2026-01-24 02:27:36 +00:00
github-actions[bot]
8c90838f3b @RouHim has signed the CLA in code-yeongyu/oh-my-opencode#1031 2026-01-23 19:32:14 +00:00
github-actions[bot]
0b784d24f2 release: v3.0.0-beta.16 2026-01-23 18:12:07 +00:00
justsisyphus
444fbe396a fix(delegate-task): use lowercase sisyphus-junior agent name in API calls
Previous fix (7ed7bf5c) only updated Atlas → atlas, but missed Sisyphus-Junior.
OpenCode does case-sensitive agent lookup, causing crash when delegate_task
tried to spawn 'Sisyphus-Junior' (registered as 'sisyphus-junior').

- SISYPHUS_JUNIOR_AGENT constant: 'Sisyphus-Junior' → 'sisyphus-junior'
- agent-tool-restrictions key: 'Sisyphus-Junior' → 'sisyphus-junior'
- Updated related test mocks
2026-01-24 03:00:58 +09:00
github-actions[bot]
ad86e58077 release: v3.0.0-beta.15 2026-01-23 17:44:45 +00:00
justsisyphus
7ed7bf5c66 fix(agents): use lowercase agent names in API calls
- atlas/index.ts: agent: 'Atlas' -> 'atlas'
- start-work/index.ts: updateSessionAgent(..., 'Atlas') -> 'atlas'
- builtin-commands/commands.ts: agent: 'Atlas' -> 'atlas'
- Updated tests to match lowercase convention
2026-01-24 02:39:12 +09:00
github-actions[bot]
1c562a95d5 release: v3.0.0-beta.14 2026-01-23 17:09:52 +00:00
justsisyphus
c2247aec60 refactor(agents): add prometheus agent and normalize agent key lookups
- Add 'prometheus' to BuiltinAgentNameSchema enum
- Update delegate_task parameter names in documentation (agent → subagent_type, background → run_in_background)
- Make agent name comparison case-insensitive in Atlas hook
- Implement case-insensitive agent config lookup in shared utilities
- Relax type signature for disabled agents parameter

🤖 Generated with assistance of OhMyOpenCode
2026-01-24 02:00:17 +09:00
justsisyphus
1c9588ff33 test: add integration tests for agent key normalization 2026-01-23 21:54:27 +09:00
justsisyphus
5d73ac819d test: update CLI tests for lowercase agent keys 2026-01-23 21:47:21 +09:00
justsisyphus
dfc57d0426 refactor(model-requirements): use lowercase agent keys 2026-01-23 21:41:55 +09:00
justsisyphus
12c9029ed7 refactor(plugin): use lowercase agent keys throughout 2026-01-23 21:32:17 +09:00
justsisyphus
91060c35ab refactor(agents): use lowercase config keys in utils 2026-01-23 21:27:26 +09:00
justsisyphus
90292db4c4 refactor(prometheus-hook): use lowercase config key 2026-01-23 20:49:17 +09:00
justsisyphus
cc4deed8ee refactor(schema): use lowercase agent config keys 2026-01-23 20:46:09 +09:00
justsisyphus
4e4288807d refactor(migration): normalize agent keys to lowercase 2026-01-23 19:01:10 +09:00
justsisyphus
629a4d3e1b feat(shared): add agent display names module 2026-01-23 18:50:03 +09:00
justsisyphus
8806ed17dc feat(publish): add platform binary verification steps
- Add STEP 8.5: Wait for publish-platform workflow completion
- Add STEP 8.6: Verify all 7 platform binary packages on npm
- Update TODO list with platform verification tasks
- Add error handling for platform-specific failures
2026-01-23 17:35:24 +09:00
github-actions[bot]
e2f8729731 @veetase has signed the CLA in code-yeongyu/oh-my-opencode#985 2026-01-23 08:27:12 +00:00
justsisyphus
bee8b3736d docs: add model configuration section to overview and quick start to configurations 2026-01-23 17:05:45 +09:00
justsisyphus
37e1a065d8 feat(agents): add aggressive resume instructions to Atlas prompt 2026-01-23 17:04:14 +09:00
justsisyphus
fc47a7a490 docs: update multimodal-looker model name and fallback chain 2026-01-23 17:02:11 +09:00
justsisyphus
9b12e2a9b5 fix(cli): update zai-coding-plan hints to include multimodal-looker 2026-01-23 17:00:22 +09:00
justsisyphus
3062277a99 feat(agents): add zai-coding-plan/glm-4.6v fallback for multimodal-looker 2026-01-23 16:58:33 +09:00
yimingll
7093583ec5 fix(lsp): add data dir to LSP server detection paths (#992)
OpenCode downloads LSP servers (like clangd) to ~/.local/share/opencode/bin,
but isServerInstalled() only checked ~/.config/opencode/bin. This caused
LSP tools to report servers as 'not installed' even when OpenCode had
successfully downloaded them.

Add ~/.local/share/opencode/bin to the detection paths to match OpenCode's
actual behavior.

Co-authored-by: yimingll <yimingll@users.noreply.github.com>
2026-01-23 16:37:40 +09:00
justsisyphus
ec61df8c17 Merge pull request #913 from carlory/fix-doctor
fix(doctor): handle file:// protocol for local dev plugin detection
2026-01-23 16:36:16 +09:00
justsisyphus
6312d2da52 Merge pull request #962 from popododo0720/fix/issues-898-919
fix(doctor): improve AST-Grep NAPI detection for bunx environments
2026-01-23 16:36:05 +09:00
justsisyphus
810dd93da2 fix(skill): enforce agent restriction in createSkillTool (#1018)
* fix(skill): enforce agent restriction in createSkillTool

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(skill): block restricted skills when agent context missing

Addresses cubic review feedback: previously agent-restricted skills
could be invoked when ctx or ctx.agent was undefined because the
guard only ran when ctx?.agent was truthy.

Changed condition from:
  skill.definition.agent && ctx?.agent && skill.definition.agent !== ctx.agent
To:
  skill.definition.agent && (!ctx?.agent || skill.definition.agent !== ctx.agent)

This ensures restricted skills are blocked unless the exact matching
agent is present in the context.

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-23 16:32:41 +09:00
justsisyphus
1a901a50ac fix(ci): build Windows binary natively to fix segfault (#1019)
Bun cross-compilation from Linux to Windows produces binaries that crash
with 'Segmentation fault at address 0xFFFFFFFFFFFFFFFF'.

Root cause: oven-sh/bun#18416

Solution:
- Use windows-latest runner for Windows platform in publish-platform.yml
- Set shell: bash for consistent behavior across runners

This is a simpler fix than PR #938 which modified publish.yml (wrong workflow).
The platform binaries are built and published by publish-platform.yml.

Fixes #873
Fixes #844

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-23 16:30:47 +09:00
justsisyphus
f8155e7d45 fix(session): preserve custom agent after switching (#1017)
Use setSessionAgent (first-write wins) instead of updateSessionAgent in chat.message handler. This prevents the default agent from overwriting a custom agent that was set via UI switch.

Fixes #893

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-23 16:25:26 +09:00
YeonGyu-Kim
39d2d44e22 fix(tools): conditionally register look_at when multimodal-looker enabled (#1016)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-23 16:25:17 +09:00
YeonGyu-Kim
15c4637e0a fix(hooks): use unix shell syntax for bash tool on all platforms (#1015)
The bash tool always runs in a Unix-like shell (bash/sh), even on Windows (via Git Bash, WSL, etc.), so we should always use unix export syntax instead of detecting the shell type dynamically.

Fixes #983

Fixes #889

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-23 16:24:58 +09:00
justsisyphus
262c7118da docs(agents): update AGENTS.md with current commit hash and line counts 2026-01-23 16:00:38 +09:00
justsisyphus
599fad0e86 fix(atlas): capture stderr from git commands to prevent help text leak
When git commands fail (e.g., not in a repo, invalid HEAD), git outputs
help text to stderr. Without explicit stdio option, execSync inherits
the parent's stdio causing help text to appear on terminal during
delegate_task execution.

Add stdio: ['pipe', 'pipe', 'pipe'] to capture stderr instead of
letting it leak to terminal.
2026-01-23 15:42:35 +09:00
justsisyphus
afbdf69037 fix(model-resolver): use first fallback entry when model cache unavailable
When availableModels is empty (no cache in CI), use the first entry
from fallbackChain directly instead of falling back to systemDefault.
This ensures categories and agents use their configured models even
when the model cache file doesn't exist.

Fixes:
- model-resolution check returning 'warn' instead of 'pass' in CI
- DEFAULT_CATEGORIES not being used when no cache available
- Unstable agent detection failing (models falling back to non-gemini)
2026-01-23 15:39:07 +09:00
github-actions[bot]
af9beee83c @Ssoon-m has signed the CLA in code-yeongyu/oh-my-opencode#1014 2026-01-23 06:31:37 +00:00
Nguyen Khac Trung Kien
6973a75bf2 Merge pull request #999 from l3aro/dev 2026-01-23 13:14:02 +07:00
justsisyphus
c6d6bd197e refactor(models): update agent/category fallback chains
- quick: replace openai fallback with opencode/grok-code
- writing: add zai-coding-plan/glm-4.7 between sonnet and gpt
- unspecified-low: gpt-5.2 → gpt-5.2-codex (medium)
- Sisyphus: add zai/glm-4.7 before openai, use gpt-5.2-codex (medium)
- Momus & Metis: add variant 'max' to gemini-3-pro
- explore: simplify to haiku (anthropic/opencode) → grok-code (opencode)
2026-01-23 15:07:58 +09:00
justsisyphus
57b10439a4 fix(agents): use resolved variant from fallback chain instead of requirement default
resolveModelWithFallback() returns entry-specific variant but it was being
ignored. Agents like oracle now correctly get variant 'high' from their
fallback chain entry instead of undefined.
2026-01-23 14:44:02 +09:00
justsisyphus
6dfe091a88 refactor(atlas): rewrite prompt with lean orchestrator structure
- Reduce prompt from ~1280 to ~280 lines (78% reduction)
- Apply prompt engineering principles: remove model-already-knows content
- Use clean XML sections: identity, mission, delegation_system, workflow, etc.
- Adopt 6-section delegation format (TASK, EXPECTED OUTCOME, REQUIRED TOOLS, MUST DO, MUST NOT DO, CONTEXT)
- Preserve: identity, category+skills system, notepad protocol, parallelization, project-level QA
- Consolidate critical overrides at end with strong framing
2026-01-23 14:37:52 +09:00
justsisyphus
75158caded fix(atlas): register tool.execute.before and pass backgroundManager
- Add atlasHook?.['tool.execute.before'] call in tool.execute.before handler
- Pass backgroundManager option to createAtlasHook for proper bg task checking
- Move atlasHook declaration after backgroundManager initialization
2026-01-23 14:25:59 +09:00
justsisyphus
e16bbbcc05 feat: show warning toast when model cache is not available
- Added isModelCacheAvailable() to check if cache file exists
- Shows warning toast on session start if cache is missing
- Suggests running 'opencode models --refresh' or restarting
2026-01-23 14:20:38 +09:00
justsisyphus
ab3e622baa fix: use cache file for model availability instead of SDK calls
- Changed fetchAvailableModels to read from ~/.cache/opencode/models.json
- Prevents plugin startup hanging caused by SDK client.config.providers() call
- Updated doctor model-resolution check to show available models from cache
- Added cache info display: provider count, model count, refresh command
2026-01-23 14:09:37 +09:00
justsisyphus
f4348885f2 fix: model fallback properly falls through to system default
- Remove Step 3 in model-resolver that forced first fallbackChain entry
  even when unavailable, blocking system default fallback
- Add sisyphusJuniorModel option to delegate_task so agents["Sisyphus-Junior"]
  model override is respected in category-based delegation
- Update tests to reflect new fallback behavior
2026-01-23 10:56:31 +09:00
github-actions[bot]
2c81c8e58e @l3aro has signed the CLA in code-yeongyu/oh-my-opencode#999 2026-01-22 19:52:54 +00:00
l3aro
3268782730 docs: rename Orchestrator-Sisyphus to Atlas 2026-01-23 02:40:13 +07:00
github-actions[bot]
dab3e1e13f release: v3.0.0-beta.13 2026-01-22 18:04:51 +00:00
justsisyphus
71474bb4a2 fix(delegate-task): reset model cache between tests 2026-01-23 02:59:03 +09:00
justsisyphus
aa6355cc46 refactor(atlas): improve delegation guidance in validation step 2026-01-23 02:59:03 +09:00
Kenny
e8b1e56e5c Merge pull request #989 from boojongmin/dev
docs: add Korean (한국어) README
2026-01-22 12:49:36 -05:00
justsisyphus
8df56794ca fix(test): update model-fallback snapshots for github-copilot model naming 2026-01-23 02:27:52 +09:00
justsisyphus
6e84a14f20 fix(model-resolver): return variant from fallback chain, handle model name normalization
- Add variant to ModelResolutionResult return type
- Return variant from matched fallback entry
- Add normalizeModelName() for Claude model hyphen/period differences
- Add transformModelForProvider() for github-copilot model names
- Update delegate-task to use resolved variant (user config takes priority)
- Fix test expectations for new fallback behavior
2026-01-23 02:20:32 +09:00
justsisyphus
7de376e24f docs: regenerate all AGENTS.md files with updated structure 2026-01-23 02:14:08 +09:00
justsisyphus
0e18efc7e4 refactor(keyword-detector): change keyword injection from synthetic to direct message transform
- Replace collector.register() with direct output.parts[textIndex].text modification
- All keyword types (ultrawork, search, analyze) now prepend to user message text
- Message format: keyword message + '---' separator + original text
- Update tests to verify text transformation instead of collector registration
- All 18 tests pass
2026-01-23 01:53:59 +09:00
justsisyphus
e15677efd5 fix(migration): add hook rename and removal mappings for v3.0.0 upgrade
- Add sisyphus-orchestrator → atlas hook rename mapping
- Add null mappings for removed hooks (preemptive-compaction, empty-message-sanitizer)
- Update migrateHookNames() to filter out removed hooks and return removed list
- Log warning when obsolete hooks are removed from disabled_hooks
- Add tests for new migration scenarios
2026-01-23 01:24:07 +09:00
justsisyphus
45b2782d55 refactor: use case-insensitive matching for agent names
Apply case-insensitive utilities across agents, delegation, and tool
restrictions. Removes duplicate implementations from agents/utils.ts.
Agent validation now normalizes input to canonical names.
2026-01-23 00:55:01 +09:00
justsisyphus
febc32d7f4 feat(shared): add case-insensitive utilities for agent name matching
Add findCaseInsensitive, includesCaseInsensitive, findByNameCaseInsensitive,
and equalsIgnoreCase functions for consistent case-insensitive lookups across
the codebase. Enables 'Oracle', 'oracle', 'ORACLE' to work interchangeably.
2026-01-23 00:54:50 +09:00
justsisyphus
76a01d4942 fix: allow 0 in providerConcurrency/modelConcurrency to disable providers
Previously min(1) validation caused entire config to fail when 0 was used,
silently ignoring all agent overrides.
2026-01-23 00:44:51 +09:00
justsisyphus
83bcf70bdd docs: sync category names with code and add missing schema fields
- Update category tables to match DEFAULT_CATEGORIES in constants.ts
- Remove non-existent 'most-capable' and 'general' categories
- Add 'unspecified-low' and 'unspecified-high' categories
- Update model names to full paths (e.g., openai/gpt-5.2-codex)
- Add new CategoryConfig schema fields: description, variant, top_p, etc.
- Fix atlas.ts prompt examples with correct category names
2026-01-23 00:13:42 +09:00
boojongmin
91d85d3df7 docs: add Korean (한국어) README 2026-01-23 00:04:45 +09:00
justsisyphus
638a314f6d docs: update AGENTS.md files and configuration docs
Refresh documentation across all AGENTS.md files and update configuration documentation with latest features.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:48:50 +09:00
justsisyphus
ff92a4caa2 refactor: update plugin handlers and shared utils
Minor updates to config handler, plugin detector, index entry, and builtin command templates.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:48:36 +09:00
justsisyphus
dda502a697 fix(tools): update background-task and session-manager
Minor improvements to background task handling and session manager utilities.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:48:21 +09:00
justsisyphus
2690301833 fix(tools): minor ast-grep improvements
Small fixes to AST-grep constants, tools, and utility functions.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:48:06 +09:00
justsisyphus
3f002ff50c fix(cli): minor fixes to formatter and events
Small improvements to version formatter and run events handling.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:47:55 +09:00
justsisyphus
bb14537b14 test(cli): add install command tests with snapshots
Add comprehensive tests for the install command with snapshot testing for generated configurations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:47:43 +09:00
justsisyphus
bdbc8d73cb fix(hooks): minor fixes across multiple hooks
Apply consistent naming and small improvements to atlas, background-compaction, claude-code-hooks, and non-interactive-env hooks.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:47:31 +09:00
justsisyphus
4b1ea1244f feat(hooks): extend keyword detector patterns
Add more keyword patterns for improved detection of work modes and triggers.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:47:16 +09:00
justsisyphus
cc7160b3b5 refactor(hooks): update delegate-task-retry naming consistency
Rename agent parameter to subagent_type for consistency with delegate-task tool changes.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:47:01 +09:00
justsisyphus
440e53ad9d feat(hooks): enhance prometheus-md-only with better patterns
Add more comprehensive markdown-only file patterns for Prometheus planning agent. Extend test coverage for new patterns.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:46:50 +09:00
justsisyphus
72098213ee refactor(task-toast): improve task toast manager types and logic
Add new toast types and improve state management for background task notifications. Update tests to cover new scenarios.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:46:36 +09:00
justsisyphus
aa2b052d28 refactor(delegate-task): enhance delegation with dynamic descriptions
Generate tool description dynamically from available categories and skills. Remove hardcoded DELEGATE_TASK_DESCRIPTION constant. Improve parameter handling with unified 'subagent_type' field replacing 'agent'.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:46:23 +09:00
justsisyphus
0edfc7f36a feat(config): add description field to category schema
Allow users to override category descriptions via config. The description is shown in delegate_task prompt to help LLMs understand category purpose.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:45:57 +09:00
justsisyphus
4ffb9b1c93 refactor(agents): simplify dynamic agent prompt builder
Reduce complexity in prompt builder by removing redundant helper functions and streamlining category/skill/agent description generation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:45:41 +09:00
justsisyphus
0610ef8c77 refactor(agents): update atlas, prometheus, sisyphus-junior prompts
Align agent prompts with new architecture. Simplify atlas prompt structure, update prometheus for cleaner flow, and minor sisyphus-junior adjustments.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:45:25 +09:00
justsisyphus
5e27ceeb81 refactor(agents): streamline sisyphus prompt and remove generator script
Simplify sisyphus prompt by removing redundant sections and inline generation. Delete generate-sisyphus-prompt.ts as prompts are now managed directly in the agent definition.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:45:05 +09:00
justsisyphus
de3a6aae11 refactor(agents): make agent creation async with model resolution
Convert createBuiltinAgents to async function that fetches available models at plugin init time. Add case-insensitive matching helpers and integrate discovered skills from both builtin and user sources.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:44:43 +09:00
justsisyphus
76211a3185 feat(cli): add doctor check for model resolution
Add new 'model-resolution' check to diagnose model fallback chain health. Validates that configured agents and categories can resolve to available models, surfacing misconfiguration early.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:44:11 +09:00
justsisyphus
04b026dd15 refactor(cli): simplify model fallback using new resolution system
Replace complex fallback logic with new shared model resolution utilities. The model-fallback module now delegates to resolveModelWithFallback() for cleaner, consistent model selection across the codebase.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:43:48 +09:00
justsisyphus
54b4844d3f refactor(shared): improve model resolver with 3-step resolution
Implement resolveModelWithFallback() that tries: 1) user override, 2) fuzzy match from requirements chain against available models, 3) system default. Export new model utilities from shared index.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:43:27 +09:00
justsisyphus
bc62c23a85 feat(shared): add model availability with fuzzy matching and fetch
Implement fuzzyMatchModel() for case-insensitive substring matching with provider filtering. Add fetchAvailableModels() to get available models from OpenCode client with caching.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:43:10 +09:00
justsisyphus
f4a0d5ec40 feat(shared): add model requirements with agent/category fallback chains
Define ModelRequirement type with FallbackEntry chains for multi-provider model resolution. Each agent and category specifies ordered fallback preferences with provider arrays and optional variants.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-22 22:42:53 +09:00
github-actions[bot]
d863daceef @boojongmin has signed the CLA in code-yeongyu/oh-my-opencode#989 2026-01-22 12:39:37 +00:00
Kenny
d220654e84 Merge pull request #982 from KNN-07/fix/opencode-config-dir-env-support
fix: respect OPENCODE_CONFIG_DIR environment variable across all config paths
2026-01-22 06:44:35 -05:00
Nguyen Khac Trung Kien
e65d57285f fix: respect OPENCODE_CONFIG_DIR environment variable across all config paths
Multiple files were hardcoding ~/.config/opencode paths instead of using
getOpenCodeConfigDir() which respects the OPENCODE_CONFIG_DIR env var.

This broke profile isolation features like OCX ghost mode, where users
set OPENCODE_CONFIG_DIR to a custom path but oh-my-opencode.json and
other configs weren't being read from that location.

Changes:
- plugin-config.ts: Use getOpenCodeConfigDir() directly
- cli/doctor/checks: Use getOpenCodeConfigDir() for auth and config checks
- tools/lsp/config.ts: Use getOpenCodeConfigDir() for LSP config paths
- command loaders: Use getOpenCodeConfigDir() for global command dirs
- hooks: Use getOpenCodeConfigDir() for hook config paths
- config-path.ts: Mark getUserConfigDir() as deprecated
- tests: Ensure OPENCODE_CONFIG_DIR is properly isolated in tests
2026-01-22 12:15:09 +07:00
Kenny
80b4067b8e Merge pull request #937 from luojiyin1987/fix/typos
fix: correct spelling errors (IDEALY→IDEALLY, EXPLICITELY→EXPLICITLY)
2026-01-21 21:58:23 -05:00
luojiyin
e3cc4c8cef Merge branch 'dev' into fix/typos 2026-01-22 10:51:45 +08:00
justsisyphus
c8175c2678 Revert "fix(tools): override OpenCode HTTP-based session tools with local implementation"
This reverts commit 89bde5ce64.
2026-01-22 11:28:27 +09:00
justsisyphus
7f2eb0a568 refactor(ci): separate platform publish into dedicated workflow
- publish.yml: main package only, triggers platform workflow on success
- publish-platform.yml: dedicated workflow with fresh OIDC token per run
- Fixes OIDC token expiration during large binary uploads (~40MB+)
- Platform workflow can also be triggered manually via workflow_dispatch
2026-01-22 11:22:26 +09:00
justsisyphus
89bde5ce64 fix(tools): override OpenCode HTTP-based session tools with local implementation
OpenCode v1.1.30+ added HTTP-based SessionList/Read/Search/Info tools that
require localhost:4096 server. Renamed our tools to PascalCase to override
them with local file-based implementation, fixing 'Unable to connect' errors.
2026-01-22 11:22:26 +09:00
popododo0720
be9d6c0061 fix(doctor): improve AST-Grep NAPI detection for bunx environments
Use dynamic import instead of require.resolve() to detect @ast-grep/napi
installation. This fixes false negatives when running via bunx where the
module exists in ~/.config/opencode/node_modules but isn't resolvable
from the temporary execution directory.

Also adds fallback path checks for common installation locations.

Fixes #898
2026-01-21 15:42:21 +09:00
luojiyin
d61817bc76 fix: correct spelling errors (IDEALY→IDEALLY, EXPLICITELY→EXPLICITLY) 2026-01-20 13:31:37 +08:00
carlory
45fe9578ec fix(doctor): handle file:// protocol for local dev plugin detection 2026-01-19 16:09:46 +08:00
232 changed files with 21123 additions and 4342 deletions

View File

@@ -14,6 +14,8 @@ body:
label: Prerequisites
description: Please confirm the following before submitting
options:
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
required: true
- label: I have searched existing issues to avoid duplicates
required: true
- label: I am using the latest version of oh-my-opencode

View File

@@ -14,6 +14,8 @@ body:
label: Prerequisites
description: Please confirm the following before submitting
options:
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
required: true
- label: I have searched existing issues and discussions to avoid duplicates
required: true
- label: This feature request is specific to oh-my-opencode (not OpenCode core)

View File

@@ -14,6 +14,8 @@ body:
label: Prerequisites
description: Please confirm the following before submitting
options:
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
required: true
- label: I have searched existing issues and discussions
required: true
- label: I have read the [documentation](https://github.com/code-yeongyu/oh-my-opencode#readme)

View File

@@ -4,13 +4,32 @@ on:
push:
branches: [master, dev]
pull_request:
branches: [dev]
branches: [master, dev]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Block PRs targeting master branch
block-master-pr:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Check PR target branch
run: |
if [ "${{ github.base_ref }}" = "master" ]; then
echo "::error::PRs to master branch are not allowed. Please target the 'dev' branch instead."
echo ""
echo "PULL REQUESTS TO MASTER ARE BLOCKED"
echo ""
echo "All PRs must target the 'dev' branch."
echo "Please close this PR and create a new one targeting 'dev'."
exit 1
else
echo "PR targets '${{ github.base_ref }}' branch - OK"
fi
test:
runs-on: ubuntu-latest
steps:
@@ -25,8 +44,34 @@ jobs:
env:
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
- name: Run tests
run: bun test
- name: Run mock-heavy tests (isolated)
run: |
# These files use mock.module() which pollutes module cache
# Run them in separate processes to prevent cross-file contamination
bun test src/plugin-handlers
bun test src/hooks/atlas
bun test src/hooks/compaction-context-injector
bun test src/features/tmux-subagent
- name: Run remaining tests
run: |
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
bun test bin script src/cli src/config src/mcp src/index.test.ts \
src/agents src/tools src/shared \
src/hooks/anthropic-context-window-limit-recovery \
src/hooks/claude-code-compatibility \
src/hooks/context-injection \
src/hooks/provider-toast \
src/hooks/session-notification \
src/hooks/sisyphus \
src/hooks/todo-continuation-enforcer \
src/features/background-agent \
src/features/builtin-commands \
src/features/builtin-skills \
src/features/claude-code-session-state \
src/features/hook-message-injector \
src/features/opencode-skill-loader \
src/features/skill-mcp-manager
typecheck:
runs-on: ubuntu-latest

View File

@@ -25,7 +25,7 @@ jobs:
path-to-signatures: 'signatures/cla.json'
path-to-document: 'https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md'
branch: 'dev'
allowlist: bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
allowlist: code-yeongyu,bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
custom-notsigned-prcomment: |
Thank you for your contribution! Before we can merge this PR, we need you to sign our [Contributor License Agreement (CLA)](https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md).

113
.github/workflows/publish-platform.yml vendored Normal file
View File

@@ -0,0 +1,113 @@
name: publish-platform
run-name: "platform packages ${{ inputs.version }}"
on:
workflow_call:
inputs:
version:
required: true
type: string
dist_tag:
required: false
type: string
default: ""
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g., 3.0.0-beta.12)"
required: true
type: string
dist_tag:
description: "npm dist tag (e.g., beta, latest)"
required: false
type: string
default: ""
permissions:
contents: read
id-token: write
jobs:
publish-platform:
# Use windows-latest for Windows to avoid cross-compilation segfault (oven-sh/bun#18416)
# Fixes: #873, #844
runs-on: ${{ matrix.platform == 'windows-x64' && 'windows-latest' || 'ubuntu-latest' }}
defaults:
run:
shell: bash
strategy:
fail-fast: false
max-parallel: 2
matrix:
platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun install
env:
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
- name: Check if already published
id: check
run: |
PKG_NAME="oh-my-opencode-${{ matrix.platform }}"
VERSION="${{ inputs.version }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/${PKG_NAME}/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "✓ ${PKG_NAME}@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
echo "→ ${PKG_NAME}@${VERSION} needs publishing"
fi
- name: Update version
if: steps.check.outputs.skip != 'true'
run: |
VERSION="${{ inputs.version }}"
cd packages/${{ matrix.platform }}
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
- name: Build binary
if: steps.check.outputs.skip != 'true'
run: |
PLATFORM="${{ matrix.platform }}"
case "$PLATFORM" in
darwin-arm64) TARGET="bun-darwin-arm64" ;;
darwin-x64) TARGET="bun-darwin-x64" ;;
linux-x64) TARGET="bun-linux-x64" ;;
linux-arm64) TARGET="bun-linux-arm64" ;;
linux-x64-musl) TARGET="bun-linux-x64-musl" ;;
linux-arm64-musl) TARGET="bun-linux-arm64-musl" ;;
windows-x64) TARGET="bun-windows-x64" ;;
esac
if [ "$PLATFORM" = "windows-x64" ]; then
OUTPUT="packages/${PLATFORM}/bin/oh-my-opencode.exe"
else
OUTPUT="packages/${PLATFORM}/bin/oh-my-opencode"
fi
bun build src/cli/index.ts --compile --minify --target=$TARGET --outfile=$OUTPUT
- name: Publish ${{ matrix.platform }}
if: steps.check.outputs.skip != 'true'
run: |
cd packages/${{ matrix.platform }}
TAG_ARG=""
if [ -n "${{ inputs.dist_tag }}" ]; then
TAG_ARG="--tag ${{ inputs.dist_tag }}"
fi
npm publish --access public $TAG_ARG
env:
NPM_CONFIG_PROVENANCE: false

View File

@@ -1,5 +1,5 @@
name: publish
run-name: "${{ format('release {0}', inputs.bump) }}"
run-name: "${{ format('release {0}', inputs.version || inputs.bump) }}"
on:
workflow_dispatch:
@@ -14,16 +14,11 @@ on:
- minor
- major
version:
description: "Override version (e.g., 3.0.0-beta.6 for beta release). Takes precedence over bump."
description: "Override version (e.g., 3.0.0-beta.6). Takes precedence over bump."
required: false
type: string
skip_platform:
description: "Skip platform binary packages (use when already published)"
required: false
type: boolean
default: false
republish:
description: "Re-publish mode: skip version check, only publish missing packages"
description: "Skip platform binary packages"
required: false
type: boolean
default: false
@@ -33,6 +28,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
id-token: write
actions: write
jobs:
test:
@@ -49,8 +45,34 @@ jobs:
env:
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
- name: Run tests
run: bun test
- name: Run mock-heavy tests (isolated)
run: |
# These files use mock.module() which pollutes module cache
# Run them in separate processes to prevent cross-file contamination
bun test src/plugin-handlers
bun test src/hooks/atlas
bun test src/hooks/compaction-context-injector
bun test src/features/tmux-subagent
- name: Run remaining tests
run: |
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
bun test bin script src/cli src/config src/mcp src/index.test.ts \
src/agents src/tools src/shared \
src/hooks/anthropic-context-window-limit-recovery \
src/hooks/claude-code-compatibility \
src/hooks/context-injection \
src/hooks/provider-toast \
src/hooks/session-notification \
src/hooks/sisyphus \
src/hooks/todo-continuation-enforcer \
src/features/background-agent \
src/features/builtin-commands \
src/features/builtin-skills \
src/features/claude-code-session-state \
src/features/hook-message-injector \
src/features/opencode-skill-loader \
src/features/skill-mcp-manager
typecheck:
runs-on: ubuntu-latest
@@ -69,8 +91,7 @@ jobs:
- name: Type check
run: bun run typecheck
# Build everything and upload artifacts
build:
publish-main:
runs-on: ubuntu-latest
needs: [test, typecheck]
if: github.repository == 'code-yeongyu/oh-my-opencode'
@@ -88,6 +109,11 @@ jobs:
with:
bun-version: latest
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun install
env:
@@ -109,7 +135,6 @@ jobs:
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Calculate dist tag
if [[ "$VERSION" == *"-"* ]]; then
DIST_TAG=$(echo "$VERSION" | cut -d'-' -f2 | cut -d'.' -f1)
echo "dist_tag=${DIST_TAG:-next}" >> $GITHUB_OUTPUT
@@ -119,43 +144,52 @@ jobs:
echo "Version: $VERSION"
- name: Update versions in package.json files
run: bun run script/publish.ts --prepare-only
env:
VERSION: ${{ steps.version.outputs.version }}
- name: Check if already published
id: check
run: |
VERSION="${{ steps.version.outputs.version }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-opencode/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-opencode@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Update version
if: steps.check.outputs.skip != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl windows-x64; do
jq --arg v "$VERSION" '.version = $v' "packages/${platform}/package.json" > tmp.json
mv tmp.json "packages/${platform}/package.json"
done
jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json
- name: Build main package
if: steps.check.outputs.skip != 'true'
run: |
bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi
bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi
bunx tsc --emitDeclarationOnly
bun run build:schema
- name: Build platform binaries
if: inputs.skip_platform != true
run: bun run build:binaries
- name: Upload main package artifact
uses: actions/upload-artifact@v4
with:
name: main-package
path: |
dist/
package.json
assets/
README.md
LICENSE.md
retention-days: 1
- name: Upload platform artifacts
if: inputs.skip_platform != true
uses: actions/upload-artifact@v4
with:
name: platform-packages
path: packages/
retention-days: 1
- name: Publish main package
if: steps.check.outputs.skip != 'true'
run: |
TAG_ARG=""
if [ -n "${{ steps.version.outputs.dist_tag }}" ]; then
TAG_ARG="--tag ${{ steps.version.outputs.dist_tag }}"
fi
npm publish --access public --provenance $TAG_ARG
env:
NPM_CONFIG_PROVENANCE: true
- name: Git commit and tag
if: steps.check.outputs.skip != 'true'
run: |
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
@@ -167,98 +201,24 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Publish platform packages in parallel (each job gets fresh OIDC token)
publish-platform:
trigger-platform:
runs-on: ubuntu-latest
needs: build
needs: publish-main
if: inputs.skip_platform != true
strategy:
fail-fast: false
matrix:
platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64]
steps:
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Download platform artifacts
uses: actions/download-artifact@v4
with:
name: platform-packages
path: packages/
- name: Check if already published
id: check
- name: Trigger platform publish workflow
run: |
PKG_NAME="oh-my-opencode-${{ matrix.platform }}"
VERSION="${{ needs.build.outputs.version }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/${PKG_NAME}/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "✓ ${PKG_NAME}@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
echo "→ ${PKG_NAME}@${VERSION} needs publishing"
fi
- name: Publish ${{ matrix.platform }}
if: steps.check.outputs.skip != 'true'
run: |
cd packages/${{ matrix.platform }}
TAG_ARG=""
if [ -n "${{ needs.build.outputs.dist_tag }}" ]; then
TAG_ARG="--tag ${{ needs.build.outputs.dist_tag }}"
fi
npm publish --access public $TAG_ARG
gh workflow run publish-platform.yml \
--repo ${{ github.repository }} \
--ref ${{ github.ref }} \
-f version=${{ needs.publish-main.outputs.version }} \
-f dist_tag=${{ needs.publish-main.outputs.dist_tag }}
env:
NPM_CONFIG_PROVENANCE: false
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Publish main package after all platform packages
publish-main:
runs-on: ubuntu-latest
needs: [build, publish-platform]
if: always() && needs.build.result == 'success' && (inputs.skip_platform == true || needs.publish-platform.result == 'success' || needs.publish-platform.result == 'skipped')
steps:
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Download main package artifact
uses: actions/download-artifact@v4
with:
name: main-package
path: .
- name: Check if already published
id: check
run: |
VERSION="${{ needs.build.outputs.version }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-opencode/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-opencode@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Publish main package
if: steps.check.outputs.skip != 'true'
run: |
TAG_ARG=""
if [ -n "${{ needs.build.outputs.dist_tag }}" ]; then
TAG_ARG="--tag ${{ needs.build.outputs.dist_tag }}"
fi
npm publish --access public --provenance $TAG_ARG
env:
NPM_CONFIG_PROVENANCE: true
# Create release and cleanup
release:
runs-on: ubuntu-latest
needs: [build, publish-main]
if: always() && needs.build.result == 'success'
needs: publish-main
steps:
- uses: actions/checkout@v4
with:
@@ -267,9 +227,8 @@ jobs:
- name: Generate changelog
id: changelog
run: |
VERSION="${{ needs.build.outputs.version }}"
VERSION="${{ needs.publish-main.outputs.version }}"
# Find previous tag
PREV_TAG=""
if [[ "$VERSION" == *"-beta."* ]]; then
BASE="${VERSION%-beta.*}"
@@ -289,13 +248,11 @@ jobs:
NOTES=$(git log "v${PREV_TAG}..v${VERSION}" --oneline --format="- %h %s" 2>/dev/null | grep -vE "^- \w+ (ignore:|test:|chore:|ci:|release:)" || echo "No notable changes")
# Write to file for multiline support
echo "$NOTES" > /tmp/changelog.md
echo "notes_file=/tmp/changelog.md" >> $GITHUB_OUTPUT
- name: Create GitHub release
run: |
VERSION="${{ needs.build.outputs.version }}"
VERSION="${{ needs.publish-main.outputs.version }}"
gh release view "v${VERSION}" >/dev/null 2>&1 || \
gh release create "v${VERSION}" --title "v${VERSION}" --notes-file /tmp/changelog.md
env:
@@ -311,7 +268,7 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
VERSION="${{ needs.build.outputs.version }}"
VERSION="${{ needs.publish-main.outputs.version }}"
git stash --include-untracked || true
git checkout master
git reset --hard "v${VERSION}"

View File

@@ -152,6 +152,41 @@ jobs:
"limit": { "context": 200000, "output": 64000 }
}
}
} |
.provider["zai-coding-plan"] = {
"name": "Z.AI Coding Plan",
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "https://api.z.ai/api/paas/v4"
},
"models": {
"glm-4.7": {
"id": "glm-4.7",
"name": "GLM 4.7",
"limit": { "context": 128000, "output": 16000 }
},
"glm-4.6v": {
"id": "glm-4.6v",
"name": "GLM 4.6 Vision",
"limit": { "context": 128000, "output": 16000 }
}
}
} |
.provider.openai = {
"name": "OpenAI",
"npm": "@ai-sdk/openai",
"models": {
"gpt-5.2": {
"id": "gpt-5.2",
"name": "GPT-5.2",
"limit": { "context": 128000, "output": 16000 }
},
"gpt-5.2-codex": {
"id": "gpt-5.2-codex",
"name": "GPT-5.2 Codex",
"limit": { "context": 128000, "output": 32000 }
}
}
}
' "$OPENCODE_JSON" > /tmp/oc.json && mv /tmp/oc.json "$OPENCODE_JSON"
@@ -287,6 +322,9 @@ jobs:
)
jq --arg append "$PROMPT_APPEND" '.agents.Sisyphus.prompt_append = $append' "$OMO_JSON" > /tmp/omo.json && mv /tmp/omo.json "$OMO_JSON"
# Add categories configuration for unspecified-low to use GLM 4.7
jq '.categories["unspecified-low"] = { "model": "zai-coding-plan/glm-4.7" }' "$OMO_JSON" > /tmp/omo.json && mv /tmp/omo.json "$OMO_JSON"
mkdir -p ~/.local/share/opencode
echo "$OPENCODE_AUTH_JSON" > ~/.local/share/opencode/auth.json
chmod 600 ~/.local/share/opencode/auth.json

View File

@@ -35,6 +35,8 @@ You are the release manager for oh-my-opencode. Execute the FULL publish workflo
{ "id": "draft-release-notes", "content": "Draft enhanced release notes content", "status": "pending", "priority": "high" },
{ "id": "update-release-notes", "content": "Update GitHub release with enhanced notes", "status": "pending", "priority": "high" },
{ "id": "verify-npm", "content": "Verify npm package published successfully", "status": "pending", "priority": "high" },
{ "id": "wait-platform-workflow", "content": "Wait for publish-platform workflow completion", "status": "pending", "priority": "high" },
{ "id": "verify-platform-binaries", "content": "Verify all 7 platform binary packages published", "status": "pending", "priority": "high" },
{ "id": "final-confirmation", "content": "Final confirmation to user with links", "status": "pending", "priority": "low" }
]
```
@@ -219,12 +221,64 @@ Compare with expected version. If not matching after 2 minutes, warn user about
---
## STEP 8.5: WAIT FOR PLATFORM WORKFLOW COMPLETION
The main publish workflow triggers a separate `publish-platform` workflow for platform-specific binaries.
1. Find the publish-platform workflow run triggered by the main workflow:
```bash
gh run list --workflow=publish-platform --limit=1 --json databaseId,status,conclusion --jq '.[0]'
```
2. Poll workflow status every 30 seconds until completion:
```bash
gh run view {platform_run_id} --json status,conclusion --jq '{status: .status, conclusion: .conclusion}'
```
**IMPORTANT: Use polling loop, NOT sleep commands.**
If conclusion is `failure`, show error logs:
```bash
gh run view {platform_run_id} --log-failed
```
---
## STEP 8.6: VERIFY PLATFORM BINARY PACKAGES
After publish-platform workflow completes, verify all 7 platform packages are published:
```bash
PLATFORMS="darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl windows-x64"
for PLATFORM in $PLATFORMS; do
npm view "oh-my-opencode-${PLATFORM}" version
done
```
All 7 packages should show the same version as the main package (`${NEW_VERSION}`).
**Expected packages:**
| Package | Description |
|---------|-------------|
| `oh-my-opencode-darwin-arm64` | macOS Apple Silicon |
| `oh-my-opencode-darwin-x64` | macOS Intel |
| `oh-my-opencode-linux-x64` | Linux x64 (glibc) |
| `oh-my-opencode-linux-arm64` | Linux ARM64 (glibc) |
| `oh-my-opencode-linux-x64-musl` | Linux x64 (musl/Alpine) |
| `oh-my-opencode-linux-arm64-musl` | Linux ARM64 (musl/Alpine) |
| `oh-my-opencode-windows-x64` | Windows x64 |
If any platform package version doesn't match, warn the user and suggest checking the publish-platform workflow logs.
---
## STEP 9: FINAL CONFIRMATION
Report success to user with:
- New version number
- GitHub release URL: https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v{version}
- npm package URL: https://www.npmjs.com/package/oh-my-opencode
- Platform packages status: List all 7 platform packages with their versions
---
@@ -234,6 +288,8 @@ Report success to user with:
- **Release not found**: Wait and retry, may be propagation delay
- **npm not updated**: npm can take 1-5 minutes to propagate, inform user
- **Permission denied**: User may need to re-authenticate with `gh auth login`
- **Platform workflow fails**: Show logs from publish-platform workflow, check which platform failed
- **Platform package missing**: Some platforms may fail due to cross-compilation issues, suggest re-running publish-platform workflow manually
## LANGUAGE

View 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>

190
AGENTS.md
View File

@@ -1,28 +1,40 @@
# PROJECT KNOWLEDGE BASE
**Generated:** 2026-01-20T17:18:00+09:00
**Commit:** 3d3d3e49
**Generated:** 2026-01-26T14:50:00+09:00
**Commit:** 9d66b807
**Branch:** dev
---
## **IMPORTANT: PULL REQUEST TARGET BRANCH**
> **ALL PULL REQUESTS MUST TARGET THE `dev` BRANCH.**
>
> **DO NOT CREATE PULL REQUESTS TARGETING `master` BRANCH.**
>
> PRs to `master` will be automatically rejected by CI.
---
## OVERVIEW
ClaudeCode plugin implementing multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3, Grok, GLM-4.7). 31 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, Claude Code compatibility layer. "oh-my-zsh" for ClaudeCode.
OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3 Flash, Grok Code). 32 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
## STRUCTURE
```
oh-my-opencode/
├── src/
│ ├── agents/ # 10 AI agents (Sisyphus, oracle, librarian, explore, frontend, etc.) - see src/agents/AGENTS.md
│ ├── hooks/ # 31 lifecycle hooks (PreToolUse, PostToolUse, Stop, etc.) - see src/hooks/AGENTS.md
│ ├── tools/ # 20+ tools (LSP, AST-Grep, delegation, session) - see src/tools/AGENTS.md
│ ├── features/ # Background agents, Claude Code compat layer - see src/features/AGENTS.md
│ ├── shared/ # 43 cross-cutting utilities - see src/shared/AGENTS.md
│ ├── cli/ # CLI installer, doctor, run - see src/cli/AGENTS.md
│ ├── mcp/ # Built-in MCPs: websearch, context7, grep_app
│ ├── agents/ # 10 AI agents - see src/agents/AGENTS.md
│ ├── hooks/ # 32 lifecycle hooks - see src/hooks/AGENTS.md
│ ├── tools/ # 20+ tools - see src/tools/AGENTS.md
│ ├── features/ # Background agents, Claude Code compat - see src/features/AGENTS.md
│ ├── shared/ # 55 cross-cutting utilities - see src/shared/AGENTS.md
│ ├── 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 (589 lines)
├── script/ # build-schema.ts, publish.ts, build-binaries.ts
│ └── index.ts # Main plugin entry (672 lines)
├── script/ # build-schema.ts, build-binaries.ts
├── packages/ # 7 platform-specific binaries
└── dist/ # Build output (ESM + .d.ts)
```
@@ -31,88 +43,68 @@ oh-my-opencode/
| Task | Location | Notes |
|------|----------|-------|
| Add agent | `src/agents/` | Create .ts with factory, add to `builtinAgents` in index.ts |
| Add agent | `src/agents/` | Create .ts with factory, add to `agentSources` |
| Add hook | `src/hooks/` | Create dir with `createXXXHook()`, register in index.ts |
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts, add to `builtinTools` |
| 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 |
| LSP behavior | `src/tools/lsp/` | client.ts (connection), tools.ts (handlers) |
| AST-Grep | `src/tools/ast-grep/` | napi.ts for @ast-grep/napi binding |
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` after changes |
| Claude Code compat | `src/features/claude-code-*-loader/` | Command, skill, agent, mcp loaders |
| Background agents | `src/features/background-agent/` | manager.ts (1165 lines) for task lifecycle |
| Skill MCP | `src/features/skill-mcp-manager/` | MCP servers embedded in skills |
| CLI installer | `src/cli/install.ts` | Interactive TUI (462 lines) |
| Doctor checks | `src/cli/doctor/checks/` | 14 health checks across 6 categories |
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (771 lines) |
| 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 (1377 lines) |
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (752 lines) |
## TDD (Test-Driven Development)
**MANDATORY for new features and bug fixes.** Follow RED-GREEN-REFACTOR:
| Phase | Action | Verification |
|-------|--------|--------------|
| **RED** | Write test describing expected behavior | `bun test` → FAIL (expected) |
| **GREEN** | Implement minimum code to pass | `bun test` → PASS |
| **REFACTOR** | Improve code quality, remove duplication | `bun test` → PASS (must stay green) |
**MANDATORY.** RED-GREEN-REFACTOR:
1. **RED**: Write test → `bun test` → FAIL
2. **GREEN**: Implement minimum → PASS
3. **REFACTOR**: Clean up → stay GREEN
**Rules:**
- NEVER write implementation before test
- NEVER delete failing tests to "pass" - fix the code
- Test file naming: `*.test.ts` alongside source
- BDD comments: `#given`, `#when`, `#then` (same as AAA)
- NEVER delete failing tests - fix the code
- Test file: `*.test.ts` alongside source (100 test files)
- BDD comments: `//#given`, `//#when`, `//#then`
## CONVENTIONS
- **Package manager**: Bun only (`bun run`, `bun build`, `bunx`)
- **Types**: bun-types (not @types/node)
- **Types**: bun-types (NEVER @types/node)
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
- **Exports**: Barrel pattern in index.ts; explicit named exports
- **Naming**: kebab-case directories, `createXXXHook`/`createXXXTool` factories
- **Testing**: BDD comments `#given/#when/#then`, 83 test files
- **Exports**: Barrel pattern via index.ts
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
- **Testing**: BDD comments, 100 test files
- **Temperature**: 0.1 for code agents, max 0.3
## ANTI-PATTERNS (THIS PROJECT)
## ANTI-PATTERNS
| Category | Forbidden |
|----------|-----------|
| **Package Manager** | npm, yarn - use Bun exclusively |
| **Types** | @types/node - use bun-types |
| **File Ops** | mkdir/touch/rm/cp/mv in code - agents use bash tool |
| **Publishing** | Direct `bun publish` - use GitHub Actions workflow_dispatch |
| **Versioning** | Local version bump - managed by CI |
| **Date References** | Year 2024 - use current year |
| **Type Safety** | `as any`, `@ts-ignore`, `@ts-expect-error` |
| **Error Handling** | Empty catch blocks `catch(e) {}` |
| **Testing** | Deleting failing tests to "pass" |
| **Agent Calls** | Sequential agent calls - use `delegate_task` for parallel |
| **Tool Access** | Broad tool access - prefer explicit `include` |
| **Hook Logic** | Heavy PreToolUse computation - slows every tool call |
| **Commits** | Giant commits (3+ files = 2+ commits), separate test from impl |
| **Temperature** | >0.3 for code agents |
| **Trust** | Trust agent self-reports - ALWAYS verify independently |
## UNIQUE STYLES
- **Platform**: Union type `"darwin" | "linux" | "win32" | "unsupported"`
- **Optional props**: Extensive `?` for optional interface properties
- **Flexible objects**: `Record<string, unknown>` for dynamic configs
- **Agent tools**: `tools: { include: [...] }` or `tools: { exclude: [...] }`
- **Hook naming**: `createXXXHook` function convention
- **Factory pattern**: Components created via `createXXX()` functions
| Package Manager | npm, yarn - Bun exclusively |
| Types | @types/node - use bun-types |
| File Ops | mkdir/touch/rm/cp/mv in code - use bash tool |
| Publishing | Direct `bun publish` - GitHub Actions only |
| Versioning | Local version bump - CI manages |
| Type Safety | `as any`, `@ts-ignore`, `@ts-expect-error` |
| Error Handling | Empty catch blocks |
| Testing | Deleting failing tests |
| Agent Calls | Sequential - use `delegate_task` parallel |
| Hook Logic | Heavy PreToolUse - slows every call |
| Commits | Giant (3+ files), separate test from impl |
| Temperature | >0.3 for code agents |
| Trust | Agent self-reports - ALWAYS verify |
## AGENT MODELS
| Agent | Default Model | Purpose |
|-------|---------------|---------|
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator with extended thinking |
| oracle | openai/gpt-5.2 | Read-only consultation, high-IQ debugging |
| librarian | opencode/glm-4.7-free | Multi-repo analysis, docs, GitHub search |
| explore | opencode/grok-code | Fast codebase exploration (contextual grep) |
| Agent | Model | Purpose |
|-------|-------|---------|
| 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/big-pickle | Docs, GitHub search |
| explore | opencode/gpt-5-nano | Fast codebase grep |
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
| Prometheus (Planner) | anthropic/claude-opus-4-5 | Strategic planning, interview mode |
| Metis (Plan Consultant) | anthropic/claude-sonnet-4-5 | Pre-planning analysis |
| Momus (Plan Reviewer) | anthropic/claude-sonnet-4-5 | Plan validation |
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning |
## COMMANDS
@@ -120,60 +112,44 @@ oh-my-opencode/
bun run typecheck # Type check
bun run build # ESM + declarations + schema
bun run rebuild # Clean + Build
bun run build:schema # Schema only
bun test # Run tests (83 test files)
bun test # 100 test files
```
## DEPLOYMENT
**GitHub Actions workflow_dispatch only**
1. Never modify package.json version locally
2. Commit & push changes
3. Trigger `publish` workflow: `gh workflow run publish -f bump=patch`
**Critical**: Never `bun publish` directly. Never bump version locally.
## CI PIPELINE
- **ci.yml**: Parallel test/typecheck → build → auto-commit schema on master → rolling `next` draft release
- **publish.yml**: Manual workflow_dispatch → version bump → changelog → 8-package OIDC npm publish → force-push master
**GitHub Actions workflow_dispatch ONLY**
1. Commit & push changes
2. Trigger: `gh workflow run publish -f bump=patch`
3. Never `bun publish` directly, never bump version locally
## COMPLEXITY HOTSPOTS
| File | Lines | Description |
|------|-------|-------------|
| `src/agents/atlas.ts` | 1383 | Orchestrator agent, 7-section delegation, wisdom accumulation |
| `src/features/builtin-skills/skills.ts` | 1203 | Skill definitions (playwright, git-master, frontend-ui-ux) |
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent, interview mode, Momus loop |
| `src/features/background-agent/manager.ts` | 1165 | Task lifecycle, concurrency, notification batching |
| `src/hooks/atlas/index.ts` | 771 | Orchestrator hook implementation |
| `src/tools/delegate-task/tools.ts` | 770 | Category-based task delegation |
| `src/cli/config-manager.ts` | 616 | JSONC parsing, multi-level config |
| `src/agents/sisyphus.ts` | 615 | Main Sisyphus prompt |
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactoring command template |
| `src/tools/lsp/client.ts` | 596 | LSP protocol, JSON-RPC |
| `src/features/builtin-skills/skills.ts` | 1729 | Skill definitions |
| `src/features/background-agent/manager.ts` | 1377 | Task lifecycle, concurrency |
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent |
| `src/tools/delegate-task/tools.ts` | 1070 | Category-based delegation |
| `src/hooks/atlas/index.ts` | 752 | Orchestrator hook |
| `src/cli/config-manager.ts` | 664 | JSONC config parsing |
| `src/index.ts` | 672 | Main plugin entry |
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
## MCP ARCHITECTURE
Three-tier MCP system:
1. **Built-in**: `websearch` (Exa), `context7` (docs), `grep_app` (GitHub search)
2. **Claude Code compatible**: `.mcp.json` files with `${VAR}` expansion
3. **Skill-embedded**: YAML frontmatter in skills (e.g., playwright)
Three-tier system:
1. **Built-in**: websearch (Exa), context7 (docs), grep_app (GitHub)
2. **Claude Code compat**: .mcp.json with `${VAR}` expansion
3. **Skill-embedded**: YAML frontmatter in skills
## CONFIG SYSTEM
- **Zod validation**: `src/config/schema.ts`
- **JSONC support**: Comments and trailing commas
- **JSONC support**: Comments, trailing commas
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
- **CLI doctor**: Validates config and reports errors
## NOTES
- **Testing**: Bun native test (`bun test`), BDD-style, 83 test files
- **ClaudeCode**: Requires >= 1.0.150
- **Multi-lang docs**: README.md (EN), README.ko.md (KO), README.ja.md (JA), README.zh-cn.md (ZH-CN)
- **Config**: `~/.config/opencode/oh-my-opencode.json` (user) or `.opencode/oh-my-opencode.json` (project)
- **OpenCode**: Requires >= 1.0.150
- **Flaky tests**: ralph-loop (CI timeout), session-state (parallel pollution)
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
- **Claude Code Compat**: Full compatibility layer for settings.json hooks, commands, skills, agents, MCPs
- **Flaky tests**: 2 known flaky tests (ralph-loop CI timeout, session-state parallel pollution)

View File

@@ -16,8 +16,8 @@
> [!TIP]
>
> [![The Orchestrator is now available in beta.](./.github/assets/orchestrator-atlas.png?v=3)](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
> > **オーケストレーターがベータ版で利用可能になりました`oh-my-opencode@3.0.0-beta.10`を使用してインストールしてください。**
> [![Oh My OpenCode 3.0が正式リリースされました!](./.github/assets/orchestrator-atlas.png?v=3)](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 @@
[![GitHub Issues](https://img.shields.io/github/issues/code-yeongyu/oh-my-opencode?color=ff80eb&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/issues)
[![License](https://img.shields.io/badge/license-SUL--1.0-white?labelColor=black&style=flat-square)](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)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-opencode)
</div>

377
README.ko.md Normal file
View File

@@ -0,0 +1,377 @@
> [!WARNING]
> **보안 경고: 사칭 사이트**
>
> **ohmyopencode.com은 이 프로젝트와 제휴 관계가 아닙니다.** 우리는 해당 사이트를 운영하거나 지지하지 않습니다.
>
> OhMyOpenCode는 **무료 오픈 소스**입니다. "공식"을 표방하는 제3자 사이트에서 설치 프로그램을 다운로드하거나 결제 정보를 입력하지 마십시오.
>
> 사칭 사이트는 유료 벽 뒤에 있어 **배포하는 내용을 확인할 수 없습니다.** 해당 사이트의 다운로드는 **잠재적으로 위험한 것으로 간주**하세요.
>
> ✅ 공식 다운로드: https://github.com/code-yeongyu/oh-my-opencode/releases
> [!NOTE]
>
> [![Sisyphus Labs — Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)
> > **Sisyphus의 완전한 제품화 버전을 구축하여 프론티어 에이전트의 미래를 정의하고 있습니다. <br />[여기서](https://sisyphuslabs.ai) 대기 명단에 등록하세요.**
>
> [!TIP]
>
> [![Oh My OpenCode 3.0이 정식 출시되었습니다!](./.github/assets/orchestrator-atlas.png?v=3)](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
> > **Oh My OpenCode 3.0이 정식 출시되었습니다! `oh-my-opencode@latest`를 사용하여 설치하세요.**
>
> 함께해요!
>
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | 기여자와 동료 `oh-my-opencode` 사용자와 연결하려면 [Discord 커뮤니티](https://discord.gg/PUwSMR9XNk)에 가입하세요. |
> | :-----| :----- |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode`에 대한 뉴스와 업데이트가 제 X 계정에 게시되었습니다. <br /> 실수로 정지된 이후, [@justsisyphus](https://x.com/justsisyphus)가 제 대신 업데이트를 게시합니다. |
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | 더 많은 프로젝트를 위해 GitHub에서 [@code-yeongyu](https://github.com/code-yeongyu)를 팔로우하세요. |
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
<div align="center">
[![Oh My OpenCode](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
</div>
> 이것은 코딩을 스테로이드로 만드는 것 — 실제로 작동하는 `oh-my-opencode`입니다. 백그라운드 에이전트 실행, 오라클, 라이브러리언, 프론트엔드 엔지니어와 같은 전문 에이전트 호출. 정교하게 제작된 LSP/AST 도구, 큐레이팅된 MCP, 완전한 Claude Code 호환 계층 사용.
# Claude OAuth 액세스 공지
## TL;DR
> Q. oh-my-opencode를 사용할 수 있나요?
네.
> Q. Claude Code 구독과 함께 사용할 수 있나요?
기술적으로는 가능합니다. 하지만 사용을 추천할 수는 없습니다.
## FULL
> 2026년 1월 현재, Anthropic은 ToS 위반을 이유로 제3자 OAuth 액세스를 제한했습니다.
>
> [**Anthropic은 이 프로젝트 oh-my-opencode를 opencode 차단의 정당화로 인용했습니다.**](https://x.com/thdxr/status/2010149530486911014)
>
> 실제로 커뮤니티에는 Claude Code의 oauth 요청 서명을 위조하는 일부 플러그인이 존재합니다.
>
> 기술적 감지 여부와 관계없이 이러한 도구는 작동할 수 있지만, 사용자는 ToS 영향을 인식해야 하며 개인적으로는 사용을 추천하지 않습니다.
>
> 이 프로젝트는 공식이 아닌 도구 사용으로 발생하는 모든 문제에 대해 책임지지 않으며, **우리는 해당 oauth 시스템에 대한 사용자 정의 구현이 없습니다.**
<div align="center">
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-opencode?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/releases)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-opencode?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-opencode)
[![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-opencode?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/graphs/contributors)
[![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-opencode?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/network/members)
[![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-opencode?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/stargazers)
[![GitHub Issues](https://img.shields.io/github/issues/code-yeongyu/oh-my-opencode?color=ff80eb&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/issues)
[![License](https://img.shields.io/badge/license-SUL--1.0-white?labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-opencode)
</div>
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
## 리뷰
> "이것 덕분에 Cursor 구독을 취소했습니다. 오픈 소스 커뮤니티에서 믿을 수 없는 일들이 일어나고 있습니다." - [Arthur Guiot](https://x.com/arthur_guiot/status/2008736347092382053?s=20)
> "Claude Code가 7일 동안 하는 일을 인간은 3개월 동안 한다면, Sisyphus는 1시간 만에 합니다. 작업이 완료될 때까지 작동합니다. 규율 있는 에이전트입니다." — B, 양적 연구원
> "Oh My Opencode로 하루 만에 8000개의 eslint 경고를 해결했습니다" — [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> "Ohmyopencode와 ralph 루프를 사용하여 하룻밤 사이에 45,000줄의 tauri 앱을 SaaS 웹 앱으로 변환했습니다. 인터뷰 프롬프트로 시작하여 질문에 대한 등급과 추천을 물어봤습니다. 그것이 작동하는 모습을 보는 것은 놀라웠고, 이 아침에 기본적으로 작동하는 웹사이트로 깨어나는 것이었습니다!" - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
> "oh-my-opencode를 사용하세요, 다시는 돌아갈 수 없을 것입니다" — [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "아직 왜 그렇게 훌륭한지 정확히 설명할 수 없지만, 개발 경험이 완전히 다른 차원에 도달했습니다." - [
苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
> "이번 주말에 open code, oh my opencode, supermemory으로 마인크래프트/소울스 같은 기괴한 것을 만들고 있습니다."
> "점심 후 산책을 가는 동안 웅크림 애니메이션을 추가하도록 요청 중입니다. [동영상]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> "여러분이 이것을 핵심에 통합하고 그를 채용해야 합니다. 진지합니다. 정말, 정말, 정말 훌륭합니다." — Henning Kilset
> "그를 설득할 수 있다면 @yeon_gyu_kim을 고용하세요, 이 사람은 opencode를 혁신했습니다." — [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
> "Oh My OpenCode는 실제로 미칩니다" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
---
## 목차
- [Oh My OpenCode](#oh-my-opencode)
- [이 README를 읽지 않고 건너뛰세요](#이-readme를-읽지-않고-건너뛰세요)
- [에이전트의 시대입니다](#에이전트의-시대입니다)
- [🪄 마법의 단어: `ultrawork`](#-마법의-단어-ultrawork)
- [읽고 싶은 분들을 위해: Sisyphus를 소개합니다](#읽고-싶은-분들을-위해-sisyphus를-소개합니다)
- [그냥 설치하세요](#그냥-설치하세요)
- [설치](#설치)
- [인간을 위한](#인간을-위한)
- [LLM 에이전트를 위한](#llm-에이전트를-위한)
- [제거](#제거)
- [기능](#기능)
- [구성](#구성)
- [JSONC 지원](#jsonc-지원)
- [Google 인증](#google-인증)
- [에이전트](#에이전트)
- [권한 옵션](#권한-옵션)
- [내장 스킬](#내장-스킬)
- [Git Master](#git-master)
- [Sisyphus 에이전트](#sisyphus-에이전트)
- [백그라운드 작업](#백그라운드-작업)
- [카테고리](#카테고리)
- [](#훅)
- [MCP](#mcp)
- [LSP](#lsp)
- [실험적 기능](#실험적-기능)
- [환경 변수](#환경-변수)
- [작성자의 메모](#작성자의-메모)
- [경고](#경고)
- [다음 기업 전문가들이 사랑합니다](#다음-기업-전문가들이-사랑합니다)
# Oh My OpenCode
[Claude Code](https://www.claude.com/product/claude-code)는 훌륭합니다.
하지만 해커라면 [OpenCode](https://github.com/sst/opencode)에 반하게 될 것입니다.
**ChatGPT, Claude, Gemini 구독으로 시작하세요. OPENCODE는 모든 것을 포함합니다.**
- 끝없이 확장 가능. 끝없이 사용자 정의 가능.
- 화면 깜빡임 없음.
- [LSP](https://opencode.ai/docs/lsp/), [린터, 포맷터](https://opencode.ai/docs/formatters/)가 파일별로 자동으로 활성화되며 모든 것을 조정할 수 있습니다.
- 모델 혼합 및 매칭. **목적별로 오케스트레이션합니다.**
- 기능이 풍부합니다. 아름답습니다. 터미널이 막히지 않습니다. 고성능입니다.
Windows에서 Linux로 처음 전환했던 때를 기억하나요? 원하는 대로 모든 것을 구성했을 때의 그 짜릿함?
그 마법이 사라진 것 같은 시대에 OpenCode가 그것을 되돌려줍니다.
코딩과 컴퓨터를 사랑한다면 OpenCode는 그 Windows 이후의 Linux와 같은 순간입니다.
**하지만 문제는 배워야 할 것이 많고, 복잡하며, 당신의 시간은 비싸다는 것입니다.**
내가 그것을 해결했습니다.
해커가 아니더라도 몇 분만 투자하세요. 기술과 생산성을 배가하세요.
이 문서를 에이전트에 넘겨서 설정하도록 하세요.
## 이 README를 읽지 않고 건너뛰세요
### 에이전트의 시대입니다
- **Claude Code / AmpCode / Factory Droid / Cursor에 이 링크를 붙여넣고 설명하도록 요청하세요.**
- 왜 좋은지, 왜 그저 또 다른 상용구 설정이 아닌지, 실제로 무엇이 좋아지는지 물어보세요.
- LLM 에이전트라면 이 프로젝트로 지금 가능한 것을 사용자에게 설명하세요.
다음과 같이:
```
Hey please read this readme and tell me why it is different from other agent harness? why is this actually good? https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/dev/README.md
```
### 🪄 마법의 단어: `ultrawork`
**이 모든 것을 읽고 싶지 않나요? 프롬프트에 `ultrawork`(또는 `ulw`)를 포함하세요.**
그게 전부입니다. 우리가 제공하는 모든 기능이 마법처럼 작동합니다 — 병렬 에이전트, 백그라운드 작업, 깊은 탐색, 완료될 때까지 끈질긴 실행. 에이전트가 나머지를 자동으로 파악합니다.
### 읽고 싶은 분들을 위해: Sisyphus를 소개합니다
![Meet Sisyphus](.github/assets/sisyphus.png)
그리스 신화에서 시시포스는 신들을 속인 형벌로 영원히 바위를 언덕 위로 굴려야 했습니다. LLM 에이전트는 정말 잘못한 것이 없지만, 그들도 매일 자신의 "돌" — 생각을 굴립니다.
내 삶도 다르지 않습니다. 돌이켜보면 우리는 이 에이전트들과 그리 다르지 않습니다.
**맞습니다! LLM 에이전트는 우리와 다르지 않습니다. 훌륭한 도구와 확고한 팀원을 제공하면 우리만큼 훌륭한 코드를 작성하고 똑같이 훌륭하게 작업할 수 있습니다.**
우리의 주요 에이전트를 만나보세요: Sisyphus (Opus 4.5 High). 아래는 Sisyphus가 그 바위를 굴리는 데 사용하는 도구입니다.
*아래의 모든 것은 사용자 정의 가능합니다. 원하는 것을 가져가세요. 모든 기능은 기본적으로 활성화됩니다. 아무것도 할 필요가 없습니다. 포함되어 있으며, 즉시 작동합니다.*
- Sisyphus의 팀원 (큐레이팅된 에이전트)
- Oracle: 디자인, 디버깅 (GPT 5.2 Medium)
- Frontend UI/UX Engineer: 프론트엔드 개발 (Gemini 3 Pro)
- Librarian: 공식 문서, 오픈 소스 구현, 코드베이스 탐색 (Claude Sonnet 4.5)
- Explore: 엄청나게 빠른 코드베이스 탐색 (Contextual Grep) (Grok Code)
- 완전한 LSP / AstGrep 지원: 결정적으로 리팩토링합니다.
- TODO 연속 강제: 에이전트가 중간에 멈추면 계속하도록 강제합니다. **이것이 Sisyphus가 그 바위를 굴리게 하는 것입니다.**
- 주석 검사기: AI가 과도한 주석을 추가하는 것을 방지합니다. Sisyphus가 생성한 코드는 인간이 작성한 것과 구별할 수 없어야 합니다.
- Claude Code 호환성: 명령, 에이전트, 스킬, MCP, 훅(PreToolUse, PostToolUse, UserPromptSubmit, Stop)
- 큐레이팅된 MCP:
- Exa (웹 검색)
- Context7 (공식 문서)
- Grep.app (GitHub 코드 검색)
- 대화형 터미널 지원 - Tmux 통합
- 비동기 에이전트
- ...
#### 그냥 설치하세요
[개요 페이지](docs/guide/overview.md)에서 많은 것을 배울 수 있지만, 다음은 예제 워크플로와 같습니다.
이것을 설치하는 것만으로 에이전트가 다음과 같이 작동합니다:
1. Sisyphus는 파일을 직접 찾는 데 시간을 낭비하지 않습니다. 메인 에이전트의 컨텍스트를 깔끔하게 유지합니다. 대신 병렬로 더 빠르고 저렴한 모델에 백그라운드 작업을 실행하여 지도를 매핑합니다.
1. Sisyphus는 리팩토링을 위해 LSP를 활용합니다. 더 결정적이고 안전하며 정교합니다.
1. 무거운 작업에 UI 터치가 필요할 때, Sisyphus는 프론트엔드 작업을 Gemini 3 Pro에 직접 위임합니다.
1. Sisyphus가 루프에 갇히거나 벽에 부딪히면 머리를 계속 부딪히지 않습니다. GPT 5.2에 고지능 전략 백업을 요청합니다.
1. 복잡한 오픈 소스 프레임워크를 작업하고 있나요? Sisyphus는 하위 에이전트를 생성하여 실시간으로 원시 소스 코드와 문서를 소화합니다. 완전한 컨텍스트 인식으로 작동합니다.
1. Sisyphus가 주석을 다루면 존재를 정당화하거나 제거합니다. 코드베이스를 깔끔하게 유지합니다.
1. Sisyphus는 TODO 목록에 묶여 있습니다. 시작한 것을 완료하지 않으면 시스템이 "바위 굴리기" 모드로 다시 강제합니다. 작업이 완료됩니다.
1. 솔직히, 문서를 읽을 필요조차 없습니다. 프롬프트를 작성하세요. 'ultrawork' 키워드를 포함하세요. Sisyphus는 구조를 분석하고, 컨텍스트를 수집하고, 외부 소스 코드를 파헤치고, 작업이 100% 완료될 때까지 계속 바위를 굴립니다.
1. 사실, 'ultrawork'를 입력하는 것도 너무 많은 노력입니다. 'ulw'를 입력하세요. 그냥 ulw. 커피를 마시세요. 작업이 완료되었습니다.
무언가를 찾아야 하나요? 공식 문서, 전체 코드베이스 기록, 공개 GitHub 구현을 검색합니다 — grep뿐만 아니라 내장 LSP 도구와 AST-Grep을 사용합니다.
3. LLM에 위임할 때 컨텍스트 관리에 대해 걱정하지 마세요. 내가 다 처리했습니다.
- OhMyOpenCode는 컨텍스트 부하를 줄이기 위해 공격적으로 여러 에이전트를 활용합니다.
- **이제 귀하의 에이전트는 개발 팀 리드입니다. 당신은 AI 매니저입니다.**
4. 작업이 완료될 때까지 멈추지 않습니다.
5. 이 프로젝트에 깊이 파고들고 싶지 않나요? 문제 없습니다. 'ultrathink'를 입력하세요.
이 모든 것이 필요하지 않다면, 앞서 언급했듯이 특정 기능을 선택할 수 있습니다.
## 설치
### 인간을 위한
이 프롬프트를 LLM 에이전트(Claude Code, AmpCode, Cursor 등)에 복사하여 붙여넣으세요:
```
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md
```
또는 [설치 가이드](docs/guide/installation.md)를 직접 읽으세요 — 하지만 **에이전트가 처리하도록 하는 것을 강력히 권장합니다. 인간은 실수를 합니다.**
### LLM 에이전트를 위한
설치 가이드를 가져와서 따르세요:
```bash
curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md
```
## 제거
oh-my-opencode를 제거하려면:
1. **OpenCode 구성에서 플러그인 제거**
`~/.config/opencode/opencode.json`(또는 `opencode.jsonc`)을 편집하고 `plugin` 배열에서 `"oh-my-opencode"`를 제거하세요:
```bash
# Using jq
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
```
2. **구성 파일 제거 (선택 사항)**
```bash
# Remove user config
rm -f ~/.config/opencode/oh-my-opencode.json
# Remove project config (if exists)
rm -f .opencode/oh-my-opencode.json
```
3. **제거 확인**
```bash
opencode --version
# Plugin should no longer be loaded
```
## 기능
당연히 존재해야 한다고 생각할 많은 기능이 있으며, 한 번 경험하면 이전 방식으로 돌아갈 수 없을 것입니다.
자세한 내용은 전체 [기능 문서](docs/features.md)를 참조하세요.
**빠른 개요:**
- **에이전트**: Sisyphus(주요 에이전트), Prometheus(플래너), Oracle(아키텍처/디버깅), Librarian(문서/코드 검색), Explore(빠른 코드베이스 grep), Multimodal Looker
- **백그라운드 에이전트**: 실제 개발 팀처럼 여러 에이전트를 병렬로 실행
- **LSP 및 AST 도구**: 리팩토링, 이름 변경, 진단, AST 인식 코드 검색
- **컨텍스트 주입**: AGENTS.md, README.md, 조건부 규칙 자동 주입
- **Claude Code 호환성**: 완전한 훅 시스템, 명령, 스킬, 에이전트, MCP
- **내장 MCP**: websearch(Exa), context7(문서), grep_app(GitHub 검색)
- **세션 도구**: 세션 기록 나열, 읽기, 검색 및 분석
- **생산성 기능**: Ralph 루프, Todo 강제, 주석 검사기, 생각 모드 등
## 구성
매우 의견이 강하지만 취향에 맞게 조정 가능합니다.
자세한 내용은 전체 [구성 문서](docs/configurations.md)를 참조하세요.
**빠른 개요:**
- **구성 위치**: `.opencode/oh-my-opencode.json`(프로젝트) 또는 `~/.config/opencode/oh-my-opencode.json`(사용자)
- **JSONC 지원**: 주석 및 후행 쉼표 지원
- **에이전트**: 모든 에이전트의 모델, 온도, 프롬프트 및 권한 재정의
- **내장 스킬**: `playwright`(브라우저 자동화), `git-master`(원자적 커밋)
- **Sisyphus 에이전트**: Prometheus(플래너) 및 Metis(계획 컨설턴트)가 있는 주요 오케스트레이터
- **백그라운드 작업**: 공급자/모델별 동시성 제한 구성
- **카테고리**: 도메인별 작업 위임(`visual`, `business-logic`, 사용자 정의)
- **훅**: 25개 이상의 내장 훅, `disabled_hooks`를 통해 모두 구성 가능
- **MCP**: 내장 websearch(Exa), context7(문서), grep_app(GitHub 검색)
- **LSP**: 리팩토링 도구가 있는 완전한 LSP 지원
- **실험적 기능**: 공격적 자르기, 자동 재개 등
## 작성자의 메모
**이 프로젝트의 철학에 궁금한가요?** [Ultrawork 선언문](docs/ultrawork-manifesto.md)을 읽어보세요.
Oh My OpenCode를 설치하세요.
순수하게 개인용으로 $24,000 토큰 가치의 LLM을 사용했습니다.
모든 도구를 시도하고 구성했습니다. OpenCode가 승리했습니다.
내가 겪은 모든 문제에 대한 답변이 이 플러그인에 구워져 있습니다. 설치하고 바로 가세요.
OpenCode가 Debian/Arch라면 Oh My OpenCode는 Ubuntu/[Omarchy](https://omarchy.org/)입니다.
[AmpCode](https://ampcode.com)와 [Claude Code](https://code.claude.com/docs/overview)에 큰 영향을 받았습니다 — 여기에 그들의 기능을 포팅했고, 종종 개선했습니다. 그리고 여전히 구축 중입니다.
그것은 **Open**Code이니까요.
다른 하니스가 약속하지만 전달할 수 없는 다중 모델 오케스트레이션, 안정성, 풍부한 기능을 즐기세요.
계속 테스트하고 업데이트하겠습니다. 저는 이 프로젝트의 가장 집요한 사용자입니다.
- 어떤 모델이 가장 날카로운 논리를 가지고 있나요?
- 누가 디버깅의 신인가요?
- 누가 가장 훌륭한 글을 쓰나요?
- 누가 프론트엔드를 지배하나요?
- 누가 백엔드를 소유하나요?
- 일일 주행에 어떤 모델이 가장 빠른가요?
- 다른 하니스가 어떤 새로운 기능을 출시하고 있나요?
이 플러그인은 그 경험의 증류입니다. 최고를 취하세요. 더 나은 아이디어가 있나요? PR을 환영합니다.
**에이전트 하니스 선택에 대해 고민하지 마세요.**
**연구를 하고, 최고에서 차용하고, 여기에 업데이트를 배포하겠습니다.**
이것이 오만하게 들리고 더 나은 답이 있다면 기여하세요. 환영합니다.
여기에 언급된 모든 프로젝트나 모델과 제휴 관계가 없습니다. 이것은 순수한 개인적인 실험과 선호입니다.
이 프로젝트의 99%는 OpenCode를 사용하여 구축되었습니다. 기능을 테스트했습니다 — 제대로 된 TypeScript를 작성하는 방법을 정말 모릅니다. **하지만 개인적으로 검토하고 이 문서의 대부분을 다시 작성했으므로 자신감을 가지고 읽으세요.**
## 경고
- 생산성이 너무 급증할 수 있습니다. 동료에게 눈치채이지 마세요.
- 실제로, 소문을 퍼뜨리겠습니다. 누가 이기는지 봅시다.
- [1.0.132](https://github.com/sst/opencode/releases/tag/v1.0.132) 이전 버전을 사용 중인 경우 OpenCode 버그로 인해 구성이 손상될 수 있습니다.
- [수정 사항](https://github.com/sst/opencode/pull/5040)은 1.0.132 이후에 병합되었습니다 — 더 새로운 버전을 사용하세요.
- 재미있는 사실: 해당 PR은 OhMyOpenCode의 Librarian, Explore 및 Oracle 설정 덕분에 발견되고 수정되었습니다.
## 다음 기업 전문가들이 사랑합니다
- [Indent](https://indentcorp.com)
- Spray(인플루언서 마케팅 솔루션), vovushop(국가 간 상거래 플랫폼), vreview(AI 상거래 리뷰 마케팅 솔루션) 제작
- [Google](https://google.com)
- [Microsoft](https://microsoft.com)
*이 놀라운 히어로 이미지에 대해 [@junhoyeo](https://github.com/junhoyeo)에게 특별히 감사드립니다.*

View File

@@ -16,8 +16,8 @@
> [!TIP]
>
> [![The Orchestrator is now available in beta.](./.github/assets/orchestrator-atlas.png?v=3)](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.**
> [![Oh My OpenCode 3.0 is now stable!](./.github/assets/orchestrator-atlas.png?v=3)](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!
>
@@ -75,7 +75,7 @@ Yes, technically possible. But I cannot recommend using it.
[![License](https://img.shields.io/badge/license-SUL--1.0-white?labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-opencode)
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
</div>
@@ -120,7 +120,7 @@ Yes, technically possible. But I cannot recommend using it.
- [For LLM Agents](#for-llm-agents)
- [Uninstallation](#uninstallation)
- [Features](#features)
- [Configuration](#configuration)
- [Configuration](#configuration)
- [JSONC Support](#jsonc-support)
- [Google Auth](#google-auth)
- [Agents](#agents)

View File

@@ -16,8 +16,8 @@
> [!TIP]
>
> [![Orchestrator 现已进入测试阶段。](./.github/assets/orchestrator-atlas.png?v=3)](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
> > **Orchestrator 现已进入测试阶段。使用 `oh-my-opencode@3.0.0-beta.10` 安装。**
> [![Oh My OpenCode 3.0 正式发布!](./.github/assets/orchestrator-atlas.png?v=3)](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 @@
[![GitHub Issues](https://img.shields.io/github/issues/code-yeongyu/oh-my-opencode?color=ff80eb&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-opencode/issues)
[![许可证](https://img.shields.io/badge/license-SUL--1.0-white?labelColor=black&style=flat-square)](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)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-opencode)
</div>

View File

@@ -20,14 +20,15 @@
"items": {
"type": "string",
"enum": [
"Sisyphus",
"sisyphus",
"prometheus",
"oracle",
"librarian",
"explore",
"multimodal-looker",
"Metis (Plan Consultant)",
"Momus (Plan Reviewer)",
"Atlas"
"metis",
"momus",
"atlas"
]
}
},
@@ -37,6 +38,7 @@
"type": "string",
"enum": [
"playwright",
"agent-browser",
"frontend-ui-ux",
"git-master"
]
@@ -69,12 +71,14 @@
"interactive-bash-session",
"thinking-block-validator",
"ralph-loop",
"category-skill-reminder",
"compaction-context-injector",
"claude-code-hooks",
"auto-slash-command",
"edit-error-recovery",
"delegate-task-retry",
"prometheus-md-only",
"sisyphus-junior-notepad",
"start-work",
"atlas"
]
@@ -216,6 +220,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
@@ -342,10 +391,55 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"Sisyphus": {
"sisyphus": {
"type": "object",
"properties": {
"model": {
@@ -468,10 +562,55 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"Sisyphus-Junior": {
"sisyphus-junior": {
"type": "object",
"properties": {
"model": {
@@ -594,6 +733,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
@@ -720,10 +904,55 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"Prometheus (Planner)": {
"prometheus": {
"type": "object",
"properties": {
"model": {
@@ -846,10 +1075,55 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"Metis (Plan Consultant)": {
"metis": {
"type": "object",
"properties": {
"model": {
@@ -972,10 +1246,55 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"Momus (Plan Reviewer)": {
"momus": {
"type": "object",
"properties": {
"model": {
@@ -1098,6 +1417,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
@@ -1224,6 +1588,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
@@ -1350,6 +1759,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
@@ -1476,6 +1930,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
@@ -1602,10 +2101,55 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"Atlas": {
"atlas": {
"type": "object",
"properties": {
"model": {
@@ -1728,6 +2272,51 @@
]
}
}
},
"maxTokens": {
"type": "number"
},
"thinking": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"enabled",
"disabled"
]
},
"budgetTokens": {
"type": "number"
}
},
"required": [
"type"
]
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"xhigh"
]
},
"textVerbosity": {
"type": "string",
"enum": [
"low",
"medium",
"high"
]
},
"providerOptions": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}
@@ -1741,6 +2330,9 @@
"additionalProperties": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"model": {
"type": "string"
},
@@ -1783,7 +2375,8 @@
"enum": [
"low",
"medium",
"high"
"high",
"xhigh"
]
},
"textVerbosity": {
@@ -2127,7 +2720,7 @@
},
"additionalProperties": {
"type": "number",
"minimum": 1
"minimum": 0
}
},
"modelConcurrency": {
@@ -2137,7 +2730,7 @@
},
"additionalProperties": {
"type": "number",
"minimum": 1
"minimum": 0
}
},
"staleTimeoutMs": {
@@ -2166,6 +2759,100 @@
"type": "boolean"
}
}
},
"browser_automation_engine": {
"type": "object",
"properties": {
"provider": {
"default": "playwright",
"type": "string",
"enum": [
"playwright",
"agent-browser",
"dev-browser"
]
}
}
},
"tmux": {
"type": "object",
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"layout": {
"default": "main-vertical",
"type": "string",
"enum": [
"main-horizontal",
"main-vertical",
"tiled",
"even-horizontal",
"even-vertical"
]
},
"main_pane_size": {
"default": 60,
"type": "number",
"minimum": 20,
"maximum": 80
},
"main_pane_min_width": {
"default": 120,
"type": "number",
"minimum": 40
},
"agent_pane_min_width": {
"default": 40,
"type": "number",
"minimum": 20
}
}
},
"sisyphus": {
"type": "object",
"properties": {
"tasks": {
"type": "object",
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"storage_path": {
"default": ".sisyphus/tasks",
"type": "string"
},
"claude_code_compat": {
"default": false,
"type": "boolean"
}
}
},
"swarm": {
"type": "object",
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"storage_path": {
"default": ".sisyphus/teams",
"type": "string"
},
"ui_mode": {
"default": "toast",
"type": "string",
"enum": [
"toast",
"tmux",
"both"
]
}
}
}
}
}
}
}

View File

@@ -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.1.2",
"oh-my-opencode-darwin-x64": "3.1.2",
"oh-my-opencode-linux-arm64": "3.1.2",
"oh-my-opencode-linux-arm64-musl": "3.1.2",
"oh-my-opencode-linux-x64": "3.1.2",
"oh-my-opencode-linux-x64-musl": "3.1.2",
"oh-my-opencode-windows-x64": "3.1.2",
},
},
},
@@ -225,20 +225,6 @@
"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-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-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-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-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-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-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=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],

View File

@@ -19,14 +19,15 @@ A Category is an agent configuration preset optimized for specific domains.
### Available Built-in Categories
| Category | Optimal Model | Characteristics | Use Cases |
|----------|---------------|-----------------|-----------|
| `visual-engineering` | `gemini-3-pro` | High creativity (Temp 0.7) | Frontend, UI/UX, animations, styling |
| `ultrabrain` | `gpt-5.2` | Maximum logical reasoning (Temp 0.1) | Architecture design, complex business logic, debugging |
| `artistry` | `gemini-3-pro` | Artistic (Temp 0.9) | Creative ideation, design concepts, storytelling |
| `quick` | `claude-haiku` | Fast (Temp 0.3) | Simple tasks, refactoring, script writing |
| `writing` | `gemini-3-flash` | Natural flow (Temp 0.5) | Documentation, technical blogs, README writing |
| `most-capable` | `claude-opus` | High performance (Temp 0.1) | Extremely difficult complex tasks |
| Category | Default Model | Use Cases |
|----------|---------------|-----------|
| `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` (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` | Documentation, prose, technical writing |
### Usage
@@ -69,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."
)
```
@@ -109,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.
---
@@ -130,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
@@ -156,12 +157,18 @@ You can fine-tune categories in `oh-my-opencode.json`.
| Field | Type | Description |
|-------|------|-------------|
| `description` | string | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-5`) |
| `variant` | string | Model variant (e.g., `max`, `xhigh`) |
| `temperature` | number | Creativity level (0.0 ~ 2.0). Lower is more deterministic. |
| `top_p` | number | Nucleus sampling parameter (0.0 ~ 1.0) |
| `prompt_append` | string | Content to append to system prompt when this category is selected |
| `thinking` | object | Thinking model configuration (`{ type: "enabled", budgetTokens: 16000 }`) |
| `reasoningEffort` | string | Reasoning effort level (`low`, `medium`, `high`) |
| `textVerbosity` | string | Text verbosity level (`low`, `medium`, `high`) |
| `tools` | object | Tool usage control (disable with `{ "tool_name": false }`) |
| `maxTokens` | number | Maximum response token count |
| `is_unstable_agent` | boolean | Mark agent as unstable - forces background mode for monitoring |
### Example Configuration
@@ -170,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."
},

View File

@@ -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",
},
},
}

View File

@@ -2,6 +2,39 @@
Highly opinionated, but adjustable to taste.
## Quick Start
**Most users don't need to configure anything manually.** Run the interactive installer:
```bash
bunx oh-my-opencode install
```
It asks about your providers (Claude, OpenAI, Gemini, etc.) and generates optimal config automatically.
**Want to customize?** Here's the common patterns:
```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
// Override specific agent models
"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/gpt-5-nano" } // Free model for grep
},
// Override category models (used by delegate_task)
"categories": {
"quick": { "model": "opencode/gpt-5-nano" }, // Fast/cheap for trivial tasks
"visual-engineering": { "model": "google/gemini-3-pro" } // Gemini for UI
}
}
```
**Find available models:** Run `opencode models` to see all models in your environment.
## Config File Locations
Config file locations (priority order):
@@ -42,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
},
},
}
@@ -50,7 +83,67 @@ 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).
## Ollama Provider
**IMPORTANT**: When using Ollama as a provider, you **must** disable streaming to avoid JSON parsing errors.
### Required Configuration
```json
{
"agents": {
"explore": {
"model": "ollama/qwen3-coder",
"stream": false
}
}
}
```
### Why `stream: false` is Required
Ollama returns NDJSON (newline-delimited JSON) when streaming is enabled, but Claude Code SDK expects a single JSON object. This causes `JSON Parse error: Unexpected EOF` when agents attempt tool calls.
**Example of the problem**:
```json
// Ollama streaming response (NDJSON - multiple lines)
{"message":{"tool_calls":[...]}, "done":false}
{"message":{"content":""}, "done":true}
// Claude Code SDK expects (single JSON object)
{"message":{"tool_calls":[...], "content":""}, "done":true}
```
### Supported Models
Common Ollama models that work with oh-my-opencode:
| Model | Best For | Configuration |
|-------|----------|---------------|
| `ollama/qwen3-coder` | Code generation, build fixes | `{"model": "ollama/qwen3-coder", "stream": false}` |
| `ollama/ministral-3:14b` | Exploration, codebase search | `{"model": "ollama/ministral-3:14b", "stream": false}` |
| `ollama/lfm2.5-thinking` | Documentation, writing | `{"model": "ollama/lfm2.5-thinking", "stream": false}` |
### Troubleshooting
If you encounter `JSON Parse error: Unexpected EOF`:
1. **Verify `stream: false` is set** in your agent configuration
2. **Check Ollama is running**: `curl http://localhost:11434/api/tags`
3. **Test with curl**:
```bash
curl -s http://localhost:11434/api/chat \
-d '{"model": "qwen3-coder", "messages": [{"role": "user", "content": "Hello"}], "stream": false}'
```
4. **See detailed troubleshooting**: [docs/troubleshooting/ollama-streaming-issue.md](troubleshooting/ollama-streaming-issue.md)
### Future SDK Fix
The proper long-term fix requires Claude Code SDK to parse NDJSON responses correctly. Until then, use `stream: false` as a workaround.
**Tracking**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
## Agents
@@ -126,8 +219,8 @@ 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.
- **playwright** (default) / **agent-browser**: Browser automation for web scraping, testing, screenshots, and browser interactions. See [Browser Automation](#browser-automation) for switching between providers.
- **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`:
@@ -137,7 +230,231 @@ Disable built-in skills via `disabled_skills` in `~/.config/opencode/oh-my-openc
}
```
Available built-in skills: `playwright`, `git-master`
Available built-in skills: `playwright`, `agent-browser`, `git-master`
## Browser Automation
Choose between two browser automation providers:
| Provider | Interface | Features | Installation |
|----------|-----------|----------|--------------|
| **playwright** (default) | MCP tools | Playwright MCP server with structured tool calls | Auto-installed via npx |
| **agent-browser** | Bash CLI | Vercel's CLI with session management, parallel browsers | Requires `bun add -g agent-browser` |
**Switch providers** via `browser_automation_engine` in `oh-my-opencode.json`:
```json
{
"browser_automation_engine": {
"provider": "agent-browser"
}
}
```
### Playwright (Default)
Uses the official Playwright MCP server (`@playwright/mcp`). Browser automation happens through structured MCP tool calls.
### agent-browser
Uses [Vercel's agent-browser CLI](https://github.com/vercel-labs/agent-browser). Key advantages:
- **Session management**: Run multiple isolated browser instances with `--session` flag
- **Persistent profiles**: Keep browser state across restarts with `--profile`
- **Snapshot-based workflow**: Get element refs via `snapshot -i`, interact with `@e1`, `@e2`, etc.
- **CLI-first**: All commands via Bash - great for scripting
**Installation required**:
```bash
bun add -g agent-browser
agent-browser install # Download Chromium
```
**Example workflow**:
```bash
agent-browser open https://example.com
agent-browser snapshot -i # Get interactive elements with refs
agent-browser fill @e1 "user@example.com"
agent-browser click @e2
agent-browser screenshot result.png
agent-browser close
```
## Tmux Integration
Run background subagents in separate tmux panes for **visual multi-agent execution**. See your agents working in parallel, each in their own terminal pane.
**Enable tmux integration** via `tmux` in `oh-my-opencode.json`:
```json
{
"tmux": {
"enabled": true,
"layout": "main-vertical",
"main_pane_size": 60,
"main_pane_min_width": 120,
"agent_pane_min_width": 40
}
}
```
| Option | Default | Description |
|--------|---------|-------------|
| `enabled` | `false` | Enable tmux subagent pane spawning. Only works when running inside an existing tmux session. |
| `layout` | `main-vertical` | Tmux layout for agent panes. See [Layout Options](#layout-options) below. |
| `main_pane_size` | `60` | Main pane size as percentage (20-80). |
| `main_pane_min_width` | `120` | Minimum width for main pane in columns. |
| `agent_pane_min_width` | `40` | Minimum width for each agent pane in columns. |
### Layout Options
| Layout | Description |
|--------|-------------|
| `main-vertical` | Main pane left, agent panes stacked on right (default) |
| `main-horizontal` | Main pane top, agent panes stacked bottom |
| `tiled` | All panes in equal-sized grid |
| `even-horizontal` | All panes in horizontal row |
| `even-vertical` | All panes in vertical stack |
### Requirements
1. **Must run inside tmux**: The feature only activates when OpenCode is already running inside a tmux session
2. **Tmux installed**: Requires tmux to be available in PATH
3. **Server mode**: OpenCode must run with `--port` flag to enable subagent pane spawning
### How It Works
When `tmux.enabled` is `true` and you're inside a tmux session:
- Background agents (via `delegate_task(run_in_background=true)`) spawn in new tmux panes
- Each pane shows the subagent's real-time output
- Panes are automatically closed when the subagent completes
- Layout is automatically adjusted based on your configuration
### Running OpenCode with Tmux Subagent Support
To enable tmux subagent panes, OpenCode must run in **server mode** with the `--port` flag. This starts an HTTP server that subagent panes connect to via `opencode attach`.
**Basic setup**:
```bash
# Start tmux session
tmux new -s dev
# Run OpenCode with server mode (port 4096)
opencode --port 4096
# Now background agents will appear in separate panes
```
**Recommended: Shell Function**
For convenience, create a shell function that automatically handles tmux sessions and port allocation. Here's an example for Fish shell:
```fish
# ~/.config/fish/config.fish
function oc
set base_name (basename (pwd))
set path_hash (echo (pwd) | md5 | cut -c1-4)
set session_name "$base_name-$path_hash"
# Find available port starting from 4096
function __oc_find_port
set port 4096
while test $port -lt 5096
if not lsof -i :$port >/dev/null 2>&1
echo $port
return 0
end
set port (math $port + 1)
end
echo 4096
end
set oc_port (__oc_find_port)
set -x OPENCODE_PORT $oc_port
if set -q TMUX
# Already inside tmux - just run with port
opencode --port $oc_port $argv
else
# Create tmux session and run opencode
set oc_cmd "OPENCODE_PORT=$oc_port opencode --port $oc_port $argv; exec fish"
if tmux has-session -t "$session_name" 2>/dev/null
tmux new-window -t "$session_name" -c (pwd) "$oc_cmd"
tmux attach-session -t "$session_name"
else
tmux new-session -s "$session_name" -c (pwd) "$oc_cmd"
end
end
functions -e __oc_find_port
end
```
**Bash/Zsh equivalent**:
```bash
# ~/.bashrc or ~/.zshrc
oc() {
local base_name=$(basename "$PWD")
local path_hash=$(echo "$PWD" | md5sum | cut -c1-4)
local session_name="${base_name}-${path_hash}"
# Find available port
local port=4096
while [ $port -lt 5096 ]; do
if ! lsof -i :$port >/dev/null 2>&1; then
break
fi
port=$((port + 1))
done
export OPENCODE_PORT=$port
if [ -n "$TMUX" ]; then
opencode --port $port "$@"
else
local oc_cmd="OPENCODE_PORT=$port opencode --port $port $*; exec $SHELL"
if tmux has-session -t "$session_name" 2>/dev/null; then
tmux new-window -t "$session_name" -c "$PWD" "$oc_cmd"
tmux attach-session -t "$session_name"
else
tmux new-session -s "$session_name" -c "$PWD" "$oc_cmd"
fi
fi
}
```
**How subagent panes work**:
1. Main OpenCode starts HTTP server on specified port (e.g., `http://localhost:4096`)
2. When a background agent spawns, Oh My OpenCode creates a new tmux pane
3. The pane runs: `opencode attach http://localhost:4096 --session <session-id>`
4. Each subagent pane shows real-time streaming output
5. Panes are automatically closed when the subagent completes
**Environment variables**:
| Variable | Description |
|----------|-------------|
| `OPENCODE_PORT` | Default port for the HTTP server (used if `--port` not specified) |
### Server Mode Reference
OpenCode's server mode exposes an HTTP API for programmatic interaction:
```bash
# Standalone server (no TUI)
opencode serve --port 4096
# TUI with server (recommended for tmux integration)
opencode --port 4096
```
| Flag | Default | Description |
|------|---------|-------------|
| `--port` | `4096` | Port for HTTP server |
| `--hostname` | `127.0.0.1` | Hostname to listen on |
For more details, see the [OpenCode Server documentation](https://opencode.ai/docs/server/).
## Git Master
@@ -268,27 +585,96 @@ Configure concurrency limits for background agent tasks. This controls how many
Categories enable domain-specific task delegation via the `delegate_task` tool. Each category applies runtime presets (model, temperature, prompt additions) when calling the `Sisyphus-Junior` agent.
**Default Categories:**
### Built-in Categories
| Category | Model | Description |
| ---------------- | ----------------------------- | ---------------------------------------------------------------------------- |
| `visual` | `google/gemini-3-pro-preview` | 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). |
All 7 categories come with optimal model defaults, but **you must configure them to use those defaults**:
**Usage:**
| Category | Built-in Default Model | Description |
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions |
| `artistry` | `google/gemini-3-pro-preview` (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 |
### ⚠️ Critical: Model Resolution Priority
**Categories DO NOT use their built-in defaults unless configured.** Model resolution follows this priority:
```
// Via delegate_task tool
delegate_task(category="visual", prompt="Create a responsive dashboard component")
delegate_task(category="business-logic", prompt="Design the payment processing flow")
1. User-configured model (in oh-my-opencode.json)
2. Category's built-in default (if you add category to config)
3. System default model (from opencode.json)
```
// Or target a specific agent directly
**Example Problem:**
```json
// opencode.json
{ "model": "anthropic/claude-sonnet-4-5" }
// oh-my-opencode.json (empty categories section)
{}
// Result: ALL categories use claude-sonnet-4-5 (wasteful!)
// - quick tasks use Sonnet instead of Haiku (expensive)
// - ultrabrain uses Sonnet instead of GPT-5.2 (inferior reasoning)
// - visual tasks use Sonnet instead of Gemini (suboptimal for UI)
```
### Recommended Configuration
**To use optimal models for each category, add them to your config:**
```json
{
"categories": {
"visual-engineering": {
"model": "google/gemini-3-pro-preview"
},
"ultrabrain": {
"model": "openai/gpt-5.2-codex",
"variant": "xhigh"
},
"artistry": {
"model": "google/gemini-3-pro-preview",
"variant": "max"
},
"quick": {
"model": "anthropic/claude-haiku-4-5" // Fast + cheap for trivial tasks
},
"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"
}
}
}
```
**Only configure categories you have access to.** Unconfigured categories fall back to your system default model.
### Usage
```javascript
// Via delegate_task tool
delegate_task(category="visual-engineering", prompt="Create a responsive dashboard component")
delegate_task(category="ultrabrain", prompt="Design the payment processing flow")
// Or target a specific agent directly (bypasses categories)
delegate_task(agent="oracle", prompt="Review this architecture")
```
**Custom Categories:**
### Custom Categories
Add custom categories in `oh-my-opencode.json`:
Add your own categories or override built-in ones:
```json
{
@@ -298,7 +684,7 @@ Add custom categories in `oh-my-opencode.json`:
"temperature": 0.2,
"prompt_append": "Focus on data analysis, ML pipelines, and statistical methods."
},
"visual": {
"visual-engineering": {
"model": "google/gemini-3-pro-preview",
"prompt_append": "Use shadcn/ui components and Tailwind CSS."
}
@@ -306,270 +692,130 @@ Add custom categories in `oh-my-opencode.json`:
}
```
Each category supports: `model`, `temperature`, `top_p`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `tools`, `prompt_append`.
Each category supports: `model`, `temperature`, `top_p`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `tools`, `prompt_append`, `variant`.
## Model Selection System
## Model Resolution System
The installer automatically configures optimal models based on your subscriptions. This section explains how models are selected for each agent and category.
At runtime, Oh My OpenCode uses a 3-step resolution process to determine which model to use for each agent and category. This happens dynamically based on your configuration and available models.
### Overview
**Problem**: Users have different subscription combinations (Claude, OpenAI, Gemini, etc.). The system needs to automatically select the best available model for each task.
**Problem**: Users have different provider configurations. The system needs to select the best available model for each task at runtime.
**Solution**: A tiered fallback system that:
1. Prioritizes native provider subscriptions (Claude, OpenAI, Gemini)
2. Falls back through alternative providers in priority order
3. Applies capability-specific logic (e.g., Oracle prefers GPT, visual tasks prefer Gemini)
**Solution**: A simple 3-step resolution flow:
1. **Step 1: User Override** — If you specify a model in `oh-my-opencode.json`, use exactly that
2. **Step 2: Provider Fallback** — Try each provider in the requirement's priority order until one is available
3. **Step 3: System Default** — Fall back to OpenCode's configured default model
### Provider Priority
### Resolution Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ MODEL SELECTION FLOW │
│ MODEL RESOLUTION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: USER OVERRIDE │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TIER 1: NATIVE PROVIDERS │ │
│ │ (Your direct subscriptions) │ │
│ │ │ │
│ │ Claude (anthropic/) ──► OpenAI (openai/) ──► Gemini │ │
│ │ │ │ (google/) │ │
│ │ ▼ ▼ │ │ │
│ │ Opus/Sonnet/Haiku GPT-5.2/Codex Gemini 3 Pro │ │
│ │ User specified model in oh-my-opencode.json? │ │
│ │ YES → Use exactly as specified │ │
│ │ NO → Continue to Step 2 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (if no native available)
│ ▼
│ Step 2: PROVIDER PRIORITY FALLBACK │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TIER 2: OPENCODE ZEN │ │
│ │ (opencode/ prefix models) │ │
│ │ For each provider in requirement.providers order: │ │
│ │ │ │
│ │ opencode/claude-opus-4-5, opencode/gpt-5.2, etc. │ │
│ │ Example for Sisyphus: │ │
│ │ anthropic → github-copilot → opencode → antigravity │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ Try: anthropic/claude-opus-4-5 │ │
│ │ Try: github-copilot/claude-opus-4-5 │ │
│ │ Try: opencode/claude-opus-4-5 │ │
│ │ ... │ │
│ │ │ │
│ │ Found in available models? → Return matched model │ │
│ │ Not found? → Try next provider │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (if no OpenCode Zen)
│ ▼ (all providers exhausted)
│ Step 3: SYSTEM DEFAULT │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TIER 3: GITHUB COPILOT │ │
│ │ (github-copilot/ prefix models) │ │
│ │ │ │
│ │ github-copilot/claude-opus-4.5, etc. │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (if no Copilot) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ TIER 4: Z.AI CODING PLAN │ │
│ │ (zai-coding-plan/ prefix models) │ │
│ │ │ │
│ │ zai-coding-plan/glm-4.7 (GLM models only) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (ultimate fallback) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FALLBACK: FREE TIER │ │
│ │ │ │
│ │ opencode/glm-4.7-free │ │
│ │ Return systemDefaultModel (from opencode.json) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### Native Tier Cross-Fallback
### Agent Provider Chains
Within the Native tier, models fall back based on capability requirements:
Each agent has a defined provider priority chain. The system tries providers in order until it finds an available model:
| Capability | 1st Choice | 2nd Choice | 3rd Choice |
|------------|------------|------------|------------|
| **High-tier tasks** (Sisyphus, Atlas) | Claude Opus | OpenAI GPT-5.2 | Gemini 3 Pro |
| **Standard tasks** | Claude Sonnet | OpenAI GPT-5.2 | Gemini 3 Flash |
| **Quick tasks** | Claude Haiku | OpenAI GPT-5.1-mini | Gemini 3 Flash |
| **Deep reasoning** (Oracle) | OpenAI GPT-5.2-Codex | Claude Opus | Gemini 3 Pro |
| **Visual/UI tasks** | Gemini 3 Pro | OpenAI GPT-5.2 | Claude Sonnet |
| **Writing tasks** | Gemini 3 Flash | OpenAI GPT-5.2 | Claude Sonnet |
| Agent | Model (no prefix) | Provider Priority Chain |
|-------|-------------------|-------------------------|
| **Sisyphus** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
| **oracle** | `gpt-5.2` | openai → anthropic → google → 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 |
| **Atlas** | `claude-sonnet-4-5` | anthropic → github-copilot → opencode → antigravity → google |
### Agent-Specific Rules
### Category Provider Chains
#### Standard Agents
Categories follow the same resolution logic:
| Agent | Capability | Example (Claude + OpenAI + Gemini) |
|-------|------------|-------------------------------------|
| **Sisyphus** | High-tier (isMax20) or Standard | `anthropic/claude-opus-4-5` or `anthropic/claude-sonnet-4-5` |
| **Oracle** | Deep reasoning | `openai/gpt-5.2-codex` |
| **Prometheus** | High-tier/Standard | Same as Sisyphus |
| **Metis** | High-tier/Standard | Same as Sisyphus |
| **Momus** | Deep reasoning | `openai/gpt-5.2-codex` |
| **Atlas** | High-tier/Standard | Same as Sisyphus |
| **multimodal-looker** | Visual | `google/gemini-3-pro-preview` |
| Category | Model (no prefix) | Provider Priority Chain |
|----------|-------------------|-------------------------|
| **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` | 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` | google → openai → anthropic → github-copilot → opencode |
#### Special Case: explore Agent
### Checking Your Configuration
The `explore` agent has unique logic for cost optimization:
Use the `doctor` command to see how models resolve with your current configuration:
```
┌────────────────────────────────────────┐
│ EXPLORE AGENT LOGIC │
├────────────────────────────────────────┤
│ │
│ Has Claude + isMax20? │
│ │ │
│ YES │ NO │
│ ▼ │ ▼ │
│ ┌──────┐│┌────────────────────┐ │
│ │Haiku ││ │ opencode/grok-code │ │
│ │4.5 │││ (free & fast) │ │
│ └──────┘│└────────────────────┘ │
│ │
│ Rationale: │
│ • max20 users want to use Claude quota │
│ • Others save quota with free grok │
└────────────────────────────────────────┘
```bash
bunx oh-my-opencode doctor --verbose
```
#### Special Case: librarian Agent
The `librarian` agent prioritizes Z.ai when available:
```
┌────────────────────────────────────────┐
│ LIBRARIAN AGENT LOGIC │
├────────────────────────────────────────┤
│ │
│ Has Z.ai Coding Plan? │
│ │ │
│ YES │ NO │
│ ▼ │ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │zai-coding- │ │ Normal fallback │ │
│ │plan/glm-4.7 │ │ chain applies │ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ Rationale: │
│ • GLM excels at documentation tasks │
│ • Z.ai provides dedicated GLM access │
└────────────────────────────────────────┘
```
### Category-Specific Rules
Categories follow the same fallback logic as agents:
| Category | Primary Capability | Fallback Chain |
|----------|-------------------|----------------|
| `visual-engineering` | Visual | Gemini → OpenAI → Claude |
| `ultrabrain` | Deep reasoning | OpenAI → Claude → Gemini |
| `artistry` | Visual/Creative | Gemini → OpenAI → Claude |
| `quick` | Quick tasks | Claude Haiku → OpenAI mini → Gemini Flash |
| `unspecified-low` | Standard | Claude Sonnet → OpenAI → Gemini Flash |
| `unspecified-high` | High-tier | Claude Opus → OpenAI → Gemini Pro |
| `writing` | Writing | Gemini Flash → OpenAI → Claude |
### Subscription Scenarios
#### Scenario 1: Claude Only (Standard Plan)
```json
// User has: Claude Pro (not max20)
{
"agents": {
"Sisyphus": { "model": "anthropic/claude-sonnet-4-5" },
"oracle": { "model": "anthropic/claude-opus-4-5" },
"explore": { "model": "opencode/grok-code" },
"librarian": { "model": "opencode/glm-4.7-free" }
}
}
```
#### Scenario 2: Claude Only (Max20 Plan)
```json
// User has: Claude Max (max20 mode)
{
"agents": {
"Sisyphus": { "model": "anthropic/claude-opus-4-5" },
"oracle": { "model": "anthropic/claude-opus-4-5" },
"explore": { "model": "anthropic/claude-haiku-4-5" },
"librarian": { "model": "opencode/glm-4.7-free" }
}
}
```
#### Scenario 3: ChatGPT Only
```json
// User has: OpenAI/ChatGPT Plus only
{
"agents": {
"Sisyphus": { "model": "openai/gpt-5.2" },
"oracle": { "model": "openai/gpt-5.2-codex" },
"explore": { "model": "opencode/grok-code" },
"multimodal-looker": { "model": "openai/gpt-5.2" },
"librarian": { "model": "opencode/glm-4.7-free" }
}
}
```
#### Scenario 4: Full Stack (Claude + OpenAI + Gemini)
```json
// User has: All native providers
{
"agents": {
"Sisyphus": { "model": "anthropic/claude-opus-4-5" },
"oracle": { "model": "openai/gpt-5.2-codex" },
"explore": { "model": "anthropic/claude-haiku-4-5" },
"multimodal-looker": { "model": "google/gemini-3-pro-preview" },
"librarian": { "model": "opencode/glm-4.7-free" }
}
}
```
#### Scenario 5: GitHub Copilot Only
```json
// User has: GitHub Copilot only (no native providers)
{
"agents": {
"Sisyphus": { "model": "github-copilot/claude-sonnet-4.5" },
"oracle": { "model": "github-copilot/gpt-5.2-codex" },
"explore": { "model": "opencode/grok-code" },
"librarian": { "model": "github-copilot/gpt-5.2" }
}
}
```
### isMax20 Flag Impact
The `isMax20` flag (Claude Max 20x mode) affects high-tier task model selection:
| isMax20 | High-tier Capability | Result |
|---------|---------------------|--------|
| `true` | Uses `unspecified-high` | Opus-class models |
| `false` | Uses `unspecified-low` | Sonnet-class models |
**Affected agents**: Sisyphus, Prometheus, Metis, Atlas
**Why?**: Max20 users have 20x more Claude usage, so they can afford Opus for orchestration. Standard users should conserve quota with Sonnet.
The "Model Resolution" check shows:
- Each agent/category's model requirement
- Provider fallback chain
- User overrides (if configured)
- Effective resolution path
### Manual Override
You can always override automatic selection in `oh-my-opencode.json`:
Override any agent or category model in `oh-my-opencode.json`:
```json
{
"agents": {
"Sisyphus": {
"model": "anthropic/claude-sonnet-4-5" // Force specific model
"model": "anthropic/claude-sonnet-4-5"
},
"oracle": {
"model": "openai/o3" // Use different model
"model": "openai/o3"
}
},
"categories": {
"visual-engineering": {
"model": "anthropic/claude-opus-4-5" // Override category default
"model": "anthropic/claude-opus-4-5"
}
}
}
```
When you specify a model override, it takes precedence (Step 1) and the provider fallback chain is skipped entirely.
## Hooks
Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
@@ -582,6 +828,8 @@ Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-m
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
**Note on `directory-agents-injector`**: This hook is **automatically disabled** when running on OpenCode 1.1.37+ because OpenCode now has native support for dynamically resolving AGENTS.md files from subdirectories (PR #10678). This prevents duplicate AGENTS.md injection. For older OpenCode versions, the hook remains active to provide the same functionality.
**Note on `auto-update-checker` and `startup-toast`**: The `startup-toast` hook is a sub-feature of `auto-update-checker`. To disable only the startup toast notification while keeping update checking enabled, add `"startup-toast"` to `disabled_hooks`. To disable all update checking features (including the toast), add `"auto-update-checker"` to `disabled_hooks`.
## MCPs

View File

@@ -12,8 +12,8 @@ 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. |
| **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
@@ -62,6 +62,27 @@ delegate_task(agent="explore", background=true, prompt="Find auth implementation
background_output(task_id="bg_abc123")
```
#### Visual Multi-Agent with Tmux
Enable `tmux.enabled` to see background agents in separate tmux panes:
```json
{
"tmux": {
"enabled": true,
"layout": "main-vertical"
}
}
```
When running inside tmux:
- Background agents spawn in new panes
- Watch multiple agents work in real-time
- Each pane shows agent output live
- Auto-cleanup when agents complete
See [Tmux Integration](configurations.md#tmux-integration) for full configuration options.
Customize agent models, prompts, and permissions in `oh-my-opencode.json`. See [Configuration](configurations.md#agents).
---
@@ -78,11 +99,15 @@ Skills provide specialized workflows with embedded MCP servers and detailed inst
| **frontend-ui-ux** | UI/UX tasks, styling | Designer-turned-developer persona. Crafts stunning UI/UX even without design mockups. Emphasizes bold aesthetic direction, distinctive typography, cohesive color palettes. |
| **git-master** | commit, rebase, squash, blame | MUST USE for ANY git operations. Atomic commits with automatic splitting, rebase/squash workflows, history search (blame, bisect, log -S). |
### Skill: playwright
### Skill: Browser Automation (playwright / agent-browser)
**Trigger**: Any browser-related request
Provides browser automation via Playwright MCP server:
Oh-My-OpenCode provides two browser automation providers, configurable via `browser_automation_engine.provider`:
#### Option 1: Playwright MCP (Default)
The default provider uses Playwright MCP server:
```yaml
mcp:
@@ -91,18 +116,41 @@ mcp:
args: ["@playwright/mcp@latest"]
```
**Capabilities**:
**Usage**:
```
/playwright Navigate to example.com and take a screenshot
```
#### Option 2: Agent Browser CLI (Vercel)
Alternative provider using [Vercel's agent-browser CLI](https://github.com/vercel-labs/agent-browser):
```json
{
"browser_automation_engine": {
"provider": "agent-browser"
}
}
```
**Requires installation**:
```bash
bun add -g agent-browser
```
**Usage**:
```
Use agent-browser to navigate to example.com and extract the main heading
```
#### Capabilities (Both Providers)
- Navigate and interact with web pages
- Take screenshots and PDFs
- Fill forms and click elements
- Wait for network requests
- Scrape content
**Usage**:
```
/playwright Navigate to example.com and take a screenshot
```
### Skill: frontend-ui-ux
**Trigger**: UI design tasks, visual changes
@@ -272,7 +320,7 @@ Hooks intercept and modify behavior at key points in the agent lifecycle.
| Hook | Event | Description |
|------|-------|-------------|
| **directory-agents-injector** | PostToolUse | Auto-injects AGENTS.md when reading files. Walks from file to project root, collecting all AGENTS.md files. |
| **directory-agents-injector** | PostToolUse | Auto-injects AGENTS.md when reading files. Walks from file to project root, collecting all AGENTS.md files. **Deprecated for OpenCode 1.1.37+** - Auto-disabled when native AGENTS.md injection is available. |
| **directory-readme-injector** | PostToolUse | Auto-injects README.md for directory context. |
| **rules-injector** | PostToolUse | Injects rules from `.claude/rules/` when conditions match. Supports globs and alwaysApply. |
| **compaction-context-injector** | Stop | Preserves critical context during session compaction. |
@@ -418,6 +466,29 @@ Disable specific hooks in config:
| **session_search** | Full-text search across session messages |
| **session_info** | Get session metadata and statistics |
### Interactive Terminal Tools
| Tool | Description |
|------|-------------|
| **interactive_bash** | Tmux-based terminal for TUI apps (vim, htop, pudb). Pass tmux subcommands directly without prefix. |
**Usage Examples**:
```bash
# Create a new session
interactive_bash(tmux_command="new-session -d -s dev-app")
# Send keystrokes to a session
interactive_bash(tmux_command="send-keys -t dev-app 'vim main.py' Enter")
# Capture pane output
interactive_bash(tmux_command="capture-pane -p -t dev-app")
```
**Key Points**:
- Commands are tmux subcommands (no `tmux` prefix)
- Use for interactive apps that need persistent sessions
- One-shot commands should use regular `Bash` tool with `&`
---
## MCPs: Built-in Servers

View File

@@ -63,7 +63,7 @@ Ask the user these questions to determine CLI options:
**Provider Priority**: Native (anthropic/, openai/, google/) > GitHub Copilot > OpenCode Zen > Z.ai Coding Plan
MUST STRONGLY WARNING, WHEN USER SAID THEY DON'T HAVE CLAUDE SUBSCRIPTION, SISYPHUS AGENT MIGHT NOT WORK IDEALY.
MUST STRONGLY WARNING, WHEN USER SAID THEY DON'T HAVE CLAUDE SUBSCRIPTION, SISYPHUS AGENT MIGHT NOT WORK IDEALLY.
### Step 1: Install OpenCode (if not installed)
@@ -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

View File

@@ -54,7 +54,7 @@ For complex or critical tasks, press **Tab** to switch to Prometheus (Planner) m
2. **Plan generation** - Based on the interview, Prometheus generates a detailed work plan with tasks, acceptance criteria, and guardrails. Optionally reviewed by Momus (plan reviewer) for high-accuracy validation.
3. **Run `/start-work`** - The Orchestrator-Sisyphus takes over:
3. **Run `/start-work`** - The Atlas takes over:
- Distributes tasks to specialized sub-agents
- Verifies each task completion independently
- Accumulates learnings across tasks
@@ -84,7 +84,78 @@ The orchestrator is designed to execute work plans created by Prometheus. Using
4. Run /start-work → Orchestrator executes
```
**Prometheus and Orchestrator-Sisyphus are a pair. Always use them together.**
**Prometheus and Atlas are a pair. Always use them together.**
---
## Model Configuration
Oh My OpenCode automatically configures models based on your available providers. You don't need to manually specify every model.
### How Models Are Determined
**1. At Installation Time (Interactive Installer)**
When you run `bunx oh-my-opencode install`, the installer asks which providers you have:
- Claude Pro/Max subscription?
- OpenAI/ChatGPT Plus?
- Google Gemini?
- GitHub Copilot?
- OpenCode Zen?
- Z.ai Coding Plan?
Based on your answers, it generates `~/.config/opencode/oh-my-opencode.json` with optimal model assignments for each agent and category.
**2. At Runtime (Fallback Chain)**
Each agent has a **provider priority chain**. The system tries providers in order until it finds an available model:
```
Example: multimodal-looker
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`. No Gemini but have Claude? Uses `anthropic/claude-haiku-4-5`. And so on.
### Example Configuration
Here's a real-world config for a user with **Claude, OpenAI, Gemini, and Z.ai** all available:
```jsonc
{
"$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" },
"librarian": { "model": "zai-coding-plan/glm-4.7" },
"explore": { "model": "opencode/gpt-5-nano" },
"multimodal-looker": { "model": "zai-coding-plan/glm-4.6v" }
},
"categories": {
// Override categories for cost optimization
"quick": { "model": "opencode/gpt-5-nano" },
"unspecified-low": { "model": "zai-coding-plan/glm-4.7" }
},
"experimental": {
"aggressive_truncation": true
}
}
```
**Key points:**
- You only need to override what you want to change
- Unspecified agents/categories use the automatic fallback chain
- Mix providers freely (Claude for main work, Z.ai for cheap tasks, etc.)
### Finding Available Models
Run `opencode models` to see all available models in your environment. Model names follow the format `provider/model-name`.
### Learn More
For detailed configuration options including per-agent settings, category customization, and more, see the [Configuration Guide](../configurations.md).
---

View File

@@ -1,6 +1,6 @@
# Understanding the Orchestration System
Oh My OpenCode's orchestration system transforms a simple AI agent into a coordinated development team. This document explains how the Prometheus → Orchestrator → Junior workflow creates high-quality, reliable code output.
Oh My OpenCode's orchestration system transforms a simple AI agent into a coordinated development team. This document explains how the Prometheus → Atlas → Junior workflow creates high-quality, reliable code output.
---
@@ -29,7 +29,7 @@ flowchart TB
end
subgraph Execution["Execution Layer (Orchestrator)"]
Orchestrator["⚡ Orchestrator-Sisyphus<br/>(Conductor)<br/>Claude Opus 4.5"]
Orchestrator["⚡ Atlas<br/>(Conductor)<br/>Claude Opus 4.5"]
end
subgraph Workers["Worker Layer (Specialized Agents)"]
@@ -152,7 +152,7 @@ If REJECTED, Prometheus fixes issues and resubmits. **No maximum retry limit.**
---
## Layer 2: Execution (Orchestrator-Sisyphus)
## Layer 2: Execution (Atlas)
### The Conductor Mindset
@@ -160,7 +160,7 @@ The Orchestrator is like an orchestra conductor: **it doesn't play instruments,
```mermaid
flowchart LR
subgraph Orchestrator["Orchestrator-Sisyphus"]
subgraph Orchestrator["Atlas"]
Read["1. Read Plan"]
Analyze["2. Analyze Tasks"]
Wisdom["3. Accumulate Wisdom"]
@@ -291,15 +291,15 @@ delegate_task(category="quick", prompt="...") // "Just get it done fast
### Built-in Categories
| Category | Model | Temp | When to Use |
|----------|-------|------|-------------|
| `visual-engineering` | Gemini 3 Pro | 0.7 | Frontend, UI/UX, design, animations |
| `ultrabrain` | GPT-5.2 | 0.1 | Complex architecture, business logic |
| `artistry` | Gemini 3 Pro | 0.9 | Creative tasks, novel ideas |
| `quick` | Claude Haiku 4.5 | 0.3 | Small tasks, budget-friendly |
| `most-capable` | Claude Opus 4.5 | 0.1 | Maximum reasoning power |
| `writing` | Gemini 3 Flash | 0.5 | Documentation, prose |
| `general` | Claude Sonnet 4.5 | 0.3 | Default, general purpose |
| Category | Model | When to Use |
|----------|-------|-------------|
| `visual-engineering` | Gemini 3 Pro | Frontend, UI/UX, design, styling, animation |
| `ultrabrain` | GPT-5.2 Codex (xhigh) | Deep logical reasoning, complex architecture decisions |
| `artistry` | Gemini 3 Pro (max) | Highly creative/artistic tasks, novel ideas |
| `quick` | Claude Haiku 4.5 | Trivial tasks - single file changes, typo fixes |
| `unspecified-low` | Claude Sonnet 4.5 | Tasks that don't fit other categories, low effort |
| `unspecified-high` | Claude Opus 4.5 (max) | Tasks that don't fit other categories, high effort |
| `writing` | Gemini 3 Flash | Documentation, prose, technical writing |
### Custom Categories
@@ -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 |
---
@@ -352,7 +352,7 @@ delegate_task(
```mermaid
sequenceDiagram
participant User
participant Orchestrator as Orchestrator-Sisyphus
participant Orchestrator as Atlas
participant Junior as Sisyphus-Junior
participant Notepad as .sisyphus/notepads/
@@ -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)
@@ -392,7 +392,7 @@ sequenceDiagram
### 1. Separation of Concerns
- **Planning** (Prometheus): High reasoning, interview, strategic thinking
- **Orchestration** (Sisyphus): Coordination, verification, wisdom accumulation
- **Orchestration** (Atlas): Coordination, verification, wisdom accumulation
- **Execution** (Junior): Focused implementation, no distractions
### 2. Explicit Over Implicit

View File

@@ -6,9 +6,10 @@
|------------|----------|-------------|
| **Simple** | Just prompt | Simple tasks, quick fixes, single-file changes |
| **Complex + Lazy** | Just type `ulw` or `ultrawork` | Complex tasks where explaining context is tedious. Agent figures it out. |
| **Complex + Precise** | `@plan``/start-work` | Precise, multi-step work requiring true orchestration. Prometheus plans, Sisyphus executes. |
| **Complex + Precise** | `@plan``/start-work` | Precise, multi-step work requiring true orchestration. Prometheus plans, Atlas executes. |
**Decision Flow:**
```
Is it a quick fix or simple task?
└─ YES → Just prompt normally
@@ -30,7 +31,7 @@ Traditional AI agents often mix planning and execution, leading to context pollu
Oh-My-OpenCode solves this by clearly separating two roles:
1. **Prometheus (Planner)**: A pure strategist who never writes code. Establishes perfect plans through interviews and analysis.
2. **Sisyphus (Executor)**: An orchestrator who executes plans. Delegates work to specialized agents and never stops until completion.
2. **Atlas (Executor)**: An orchestrator who executes plans. Delegates work to specialized agents and never stops until completion.
---
@@ -52,10 +53,10 @@ flowchart TD
StartWork --> BoulderState[boulder.json]
subgraph Execution Phase
BoulderState --> Sisyphus[Sisyphus<br>Orchestrator]
Sisyphus --> Oracle[Oracle]
Sisyphus --> Frontend[Frontend<br>Engineer]
Sisyphus --> Explore[Explore]
BoulderState --> Atlas[Atlas<br>Orchestrator]
Atlas --> Oracle[Oracle]
Atlas --> Frontend[Frontend<br>Engineer]
Atlas --> Explore[Explore]
end
```
@@ -64,22 +65,26 @@ flowchart TD
## 3. Key Components
### 🔮 Prometheus (The Planner)
- **Model**: `anthropic/claude-opus-4-5`
- **Role**: Strategic planning, requirements interviews, work plan creation
- **Constraint**: **READ-ONLY**. Can only create/modify markdown files within `.sisyphus/` directory.
- **Characteristic**: Never writes code directly, focuses solely on "how to do it".
### 🦉 Metis (The Consultant)
### 🦉 Metis (The Plan Consultant)
- **Role**: Pre-analysis and gap detection
- **Function**: Identifies hidden user intent, prevents AI over-engineering, eliminates ambiguity.
- **Workflow**: Metis consultation is mandatory before plan creation.
### ⚖️ Momus (The Reviewer)
### ⚖️ Momus (The Plan Reviewer)
- **Role**: High-precision plan validation (High Accuracy Mode)
- **Function**: Rejects and demands revisions until the plan is perfect.
- **Trigger**: Activated when user requests "high accuracy".
### 🪨 Sisyphus (The Orchestrator)
### ⚡ Atlas (The Plan Executor)
- **Model**: `anthropic/claude-opus-4-5` (Extended Thinking 32k)
- **Role**: Execution and delegation
- **Characteristic**: Doesn't do everything directly, actively delegates to specialized agents (Frontend, Librarian, etc.).
@@ -89,6 +94,7 @@ flowchart TD
## 4. Workflow
### Phase 1: Interview and Planning (Interview Mode)
Prometheus starts in **interview mode** by default. Instead of immediately creating a plan, it collects sufficient context.
1. **Intent Identification**: Classifies whether the user's request is Refactoring or New Feature.
@@ -96,6 +102,7 @@ Prometheus starts in **interview mode** by default. Instead of immediately creat
3. **Draft Creation**: Continuously records discussion content in `.sisyphus/drafts/`.
### Phase 2: Plan Generation
When the user requests "Make it a plan", plan generation begins.
1. **Metis Consultation**: Confirms any missed requirements or risk factors.
@@ -103,10 +110,11 @@ When the user requests "Make it a plan", plan generation begins.
3. **Handoff**: Once plan creation is complete, guides user to use `/start-work` command.
### Phase 3: Execution
When the user enters `/start-work`, the execution phase begins.
1. **State Management**: Creates `boulder.json` file to track current plan and session ID.
2. **Task Execution**: Sisyphus reads the plan and processes TODOs one by one.
2. **Task Execution**: Atlas reads the plan and processes TODOs one by one.
3. **Delegation**: UI work is delegated to Frontend agent, complex logic to Oracle.
4. **Continuity**: Even if the session is interrupted, work continues in the next session through `boulder.json`.
@@ -115,11 +123,15 @@ When the user enters `/start-work`, the execution phase begins.
## 5. Commands and Usage
### `@plan [request]`
Invokes Prometheus to start a planning session.
- Example: `@plan "I want to refactor the authentication system to NextAuth"`
### `/start-work`
Executes the generated plan.
- Function: Finds plan in `.sisyphus/plans/` and enters execution mode.
- If there's interrupted work, automatically resumes from where it left off.
@@ -132,7 +144,7 @@ You can control related features in `oh-my-opencode.json`.
```jsonc
{
"sisyphus_agent": {
"disabled": false, // Enable Sisyphus orchestration (default: false)
"disabled": false, // Enable Atlas orchestration (default: false)
"planner_enabled": true, // Enable Prometheus (default: true)
"replace_plan": true // Replace default plan agent with Prometheus (default: true)
},

View File

@@ -0,0 +1,126 @@
# Ollama Streaming Issue - JSON Parse Error
## Problem
When using Ollama as a provider with oh-my-opencode agents, you may encounter:
```
JSON Parse error: Unexpected EOF
```
This occurs when agents attempt tool calls (e.g., `explore` agent using `mcp_grep_search`).
## Root Cause
Ollama returns **NDJSON** (newline-delimited JSON) when `stream: true` is used in API requests:
```json
{"message":{"tool_calls":[{"function":{"name":"read","arguments":{"filePath":"README.md"}}}]}, "done":false}
{"message":{"content":""}, "done":true}
```
Claude Code SDK expects a single JSON object, not multiple NDJSON lines, causing the parse error.
### Why This Happens
- **Ollama API**: Returns streaming responses as NDJSON by design
- **Claude Code SDK**: Doesn't properly handle NDJSON responses for tool calls
- **oh-my-opencode**: Passes through the SDK's behavior (can't fix at this layer)
## Solutions
### Option 1: Disable Streaming (Recommended - Immediate Fix)
Configure your Ollama provider to use `stream: false`:
```json
{
"provider": "ollama",
"model": "qwen3-coder",
"stream": false
}
```
**Pros:**
- Works immediately
- No code changes needed
- Simple configuration
**Cons:**
- Slightly slower response time (no streaming)
- Less interactive feedback
### Option 2: Use Non-Tool Agents Only
If you need streaming, avoid agents that use tools:
-**Safe**: Simple text generation, non-tool tasks
-**Problematic**: Any agent with tool calls (explore, librarian, etc.)
### Option 3: Wait for SDK Fix (Long-term)
The proper fix requires Claude Code SDK to:
1. Detect NDJSON responses
2. Parse each line separately
3. Merge `tool_calls` from multiple lines
4. Return a single merged response
**Tracking**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
## Workaround Implementation
Until the SDK is fixed, here's how to implement NDJSON parsing (for SDK maintainers):
```typescript
async function parseOllamaStreamResponse(response: string): Promise<object> {
const lines = response.split('\n').filter(line => line.trim());
const mergedMessage = { tool_calls: [] };
for (const line of lines) {
try {
const json = JSON.parse(line);
if (json.message?.tool_calls) {
mergedMessage.tool_calls.push(...json.message.tool_calls);
}
if (json.message?.content) {
mergedMessage.content = json.message.content;
}
} catch (e) {
// Skip malformed lines
console.warn('Skipping malformed NDJSON line:', line);
}
}
return mergedMessage;
}
```
## Testing
To verify the fix works:
```bash
# Test with curl (should work with stream: false)
curl -s http://localhost:11434/api/chat \
-d '{
"model": "qwen3-coder",
"messages": [{"role": "user", "content": "Read file README.md"}],
"stream": false,
"tools": [{"type": "function", "function": {"name": "read", "description": "Read a file", "parameters": {"type": "object", "properties": {"filePath": {"type": "string"}}, "required": ["filePath"]}}}]
}'
```
## Related Issues
- **oh-my-opencode**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
- **Ollama API Docs**: https://github.com/ollama/ollama/blob/main/docs/api.md
## Getting Help
If you encounter this issue:
1. Check your Ollama provider configuration
2. Set `stream: false` as a workaround
3. Report any additional errors to the issue tracker
4. Provide your configuration (without secrets) for debugging

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"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.12",
"oh-my-opencode-darwin-x64": "3.0.0-beta.12",
"oh-my-opencode-linux-arm64": "3.0.0-beta.12",
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.12",
"oh-my-opencode-linux-x64": "3.0.0-beta.12",
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.12",
"oh-my-opencode-windows-x64": "3.0.0-beta.12"
"oh-my-opencode-darwin-arm64": "3.1.6",
"oh-my-opencode-darwin-x64": "3.1.6",
"oh-my-opencode-linux-arm64": "3.1.6",
"oh-my-opencode-linux-arm64-musl": "3.1.6",
"oh-my-opencode-linux-x64": "3.1.6",
"oh-my-opencode-linux-x64-musl": "3.1.6",
"oh-my-opencode-windows-x64": "3.1.6"
},
"trustedDependencies": [
"@ast-grep/cli",

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-arm64",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
"license": "MIT",
"repository": {

View File

@@ -0,0 +1,22 @@
{
"name": "oh-my-opencode-darwin-x64-baseline",
"version": "3.1.1",
"description": "Platform-specific binary for oh-my-opencode (darwin-x64-baseline, no AVX2)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/code-yeongyu/oh-my-opencode"
},
"os": [
"darwin"
],
"cpu": [
"x64"
],
"files": [
"bin"
],
"bin": {
"oh-my-opencode": "./bin/oh-my-opencode"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-x64",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64-musl",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
"license": "MIT",
"repository": {

View File

@@ -0,0 +1,25 @@
{
"name": "oh-my-opencode-linux-x64-baseline",
"version": "3.1.1",
"description": "Platform-specific binary for oh-my-opencode (linux-x64-baseline, no AVX2)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/code-yeongyu/oh-my-opencode"
},
"os": [
"linux"
],
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"files": [
"bin"
],
"bin": {
"oh-my-opencode": "./bin/oh-my-opencode"
}
}

View File

@@ -0,0 +1,25 @@
{
"name": "oh-my-opencode-linux-x64-musl-baseline",
"version": "3.1.1",
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl-baseline, no AVX2)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/code-yeongyu/oh-my-opencode"
},
"os": [
"linux"
],
"cpu": [
"x64"
],
"libc": [
"musl"
],
"files": [
"bin"
],
"bin": {
"oh-my-opencode": "./bin/oh-my-opencode"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64-musl",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
"license": "MIT",
"repository": {

View File

@@ -0,0 +1,22 @@
{
"name": "oh-my-opencode-windows-x64-baseline",
"version": "3.1.1",
"description": "Platform-specific binary for oh-my-opencode (windows-x64-baseline, no AVX2)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/code-yeongyu/oh-my-opencode"
},
"os": [
"win32"
],
"cpu": [
"x64"
],
"files": [
"bin"
],
"bin": {
"oh-my-opencode": "./bin/oh-my-opencode.exe"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-windows-x64",
"version": "3.0.0-beta.12",
"version": "3.1.6",
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
"license": "MIT",
"repository": {

View File

@@ -0,0 +1,79 @@
// script/build-binaries.test.ts
// Tests for platform binary build configuration
import { describe, expect, it } from "bun:test";
// Import PLATFORMS from build-binaries.ts
// We need to export it first, but for now we'll test the expected structure
const EXPECTED_BASELINE_TARGETS = [
"bun-linux-x64-baseline",
"bun-linux-x64-musl-baseline",
"bun-darwin-x64-baseline",
"bun-windows-x64-baseline",
];
describe("build-binaries", () => {
describe("PLATFORMS array", () => {
it("includes baseline variants for non-AVX2 CPU support", async () => {
// given
const module = await import("./build-binaries.ts");
const platforms = (module as { PLATFORMS: { target: string }[] }).PLATFORMS;
const targets = platforms.map((p) => p.target);
// when
const hasAllBaselineTargets = EXPECTED_BASELINE_TARGETS.every((baseline) =>
targets.includes(baseline)
);
// then
expect(hasAllBaselineTargets).toBe(true);
for (const baseline of EXPECTED_BASELINE_TARGETS) {
expect(targets).toContain(baseline);
}
});
it("has correct directory names for baseline platforms", async () => {
// given
const module = await import("./build-binaries.ts");
const platforms = (module as { PLATFORMS: { dir: string; target: string }[] }).PLATFORMS;
// when
const baselinePlatforms = platforms.filter((p) => p.target.includes("baseline"));
// then
expect(baselinePlatforms.length).toBe(4);
expect(baselinePlatforms.map((p) => p.dir)).toContain("linux-x64-baseline");
expect(baselinePlatforms.map((p) => p.dir)).toContain("linux-x64-musl-baseline");
expect(baselinePlatforms.map((p) => p.dir)).toContain("darwin-x64-baseline");
expect(baselinePlatforms.map((p) => p.dir)).toContain("windows-x64-baseline");
});
it("has correct binary names for baseline platforms", async () => {
// given
const module = await import("./build-binaries.ts");
const platforms = (module as { PLATFORMS: { dir: string; target: string; binary: string }[] }).PLATFORMS;
// when
const windowsBaseline = platforms.find((p) => p.target === "bun-windows-x64-baseline");
const linuxBaseline = platforms.find((p) => p.target === "bun-linux-x64-baseline");
// then
expect(windowsBaseline?.binary).toBe("oh-my-opencode.exe");
expect(linuxBaseline?.binary).toBe("oh-my-opencode");
});
it("has descriptions mentioning no AVX2 for baseline platforms", async () => {
// given
const module = await import("./build-binaries.ts");
const platforms = (module as { PLATFORMS: { target: string; description: string }[] }).PLATFORMS;
// when
const baselinePlatforms = platforms.filter((p) => p.target.includes("baseline"));
// then
for (const platform of baselinePlatforms) {
expect(platform.description).toContain("no AVX2");
}
});
});
});

View File

@@ -13,14 +13,18 @@ interface PlatformTarget {
description: string;
}
const PLATFORMS: PlatformTarget[] = [
export const PLATFORMS: PlatformTarget[] = [
{ dir: "darwin-arm64", target: "bun-darwin-arm64", binary: "oh-my-opencode", description: "macOS ARM64" },
{ dir: "darwin-x64", target: "bun-darwin-x64", binary: "oh-my-opencode", description: "macOS x64" },
{ dir: "darwin-x64-baseline", target: "bun-darwin-x64-baseline", binary: "oh-my-opencode", description: "macOS x64 (no AVX2)" },
{ dir: "linux-x64", target: "bun-linux-x64", binary: "oh-my-opencode", description: "Linux x64 (glibc)" },
{ dir: "linux-x64-baseline", target: "bun-linux-x64-baseline", binary: "oh-my-opencode", description: "Linux x64 (glibc, no AVX2)" },
{ dir: "linux-arm64", target: "bun-linux-arm64", binary: "oh-my-opencode", description: "Linux ARM64 (glibc)" },
{ dir: "linux-x64-musl", target: "bun-linux-x64-musl", binary: "oh-my-opencode", description: "Linux x64 (musl)" },
{ dir: "linux-x64-musl-baseline", target: "bun-linux-x64-musl-baseline", binary: "oh-my-opencode", description: "Linux x64 (musl, no AVX2)" },
{ dir: "linux-arm64-musl", target: "bun-linux-arm64-musl", binary: "oh-my-opencode", description: "Linux ARM64 (musl)" },
{ dir: "windows-x64", target: "bun-windows-x64", binary: "oh-my-opencode.exe", description: "Windows x64" },
{ dir: "windows-x64-baseline", target: "bun-windows-x64-baseline", binary: "oh-my-opencode.exe", description: "Windows x64 (no AVX2)" },
];
const ENTRY_POINT = "src/cli/index.ts";

View File

@@ -1,105 +0,0 @@
#!/usr/bin/env bun
/**
* Generate the full Sisyphus system prompt and output to sisyphus-prompt.md
*
* Usage:
* bun run script/generate-sisyphus-prompt.ts
*/
import { createSisyphusAgent } from "../src/agents/sisyphus"
import { ORACLE_PROMPT_METADATA } from "../src/agents/oracle"
import { LIBRARIAN_PROMPT_METADATA } from "../src/agents/librarian"
import { EXPLORE_PROMPT_METADATA } from "../src/agents/explore"
import { MULTIMODAL_LOOKER_PROMPT_METADATA } from "../src/agents/multimodal-looker"
import { createBuiltinSkills } from "../src/features/builtin-skills"
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../src/tools/delegate-task/constants"
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "../src/agents/dynamic-agent-prompt-builder"
import type { BuiltinAgentName, AgentPromptMetadata } from "../src/agents/types"
import { writeFileSync } from "node:fs"
import { join } from "node:path"
// Build available agents (same logic as utils.ts)
const agentMetadata: Record<string, AgentPromptMetadata> = {
oracle: ORACLE_PROMPT_METADATA,
librarian: LIBRARIAN_PROMPT_METADATA,
explore: EXPLORE_PROMPT_METADATA,
"multimodal-looker": MULTIMODAL_LOOKER_PROMPT_METADATA,
}
const agentDescriptions: Record<string, string> = {
oracle: "Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design.",
librarian: "Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
explore: 'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis.',
"multimodal-looker": "Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents.",
}
const availableAgents: AvailableAgent[] = Object.entries(agentMetadata).map(([name, metadata]) => ({
name: name as BuiltinAgentName,
description: agentDescriptions[name] ?? "",
metadata,
}))
// Build available categories
const availableCategories: AvailableCategory[] = Object.entries(DEFAULT_CATEGORIES).map(([name]) => ({
name,
description: CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
}))
// Build available skills
const builtinSkills = createBuiltinSkills()
const availableSkills: AvailableSkill[] = builtinSkills.map((skill) => ({
name: skill.name,
description: skill.description,
location: "plugin" as const,
}))
// Generate the agent config
const model = "anthropic/claude-opus-4-5"
const sisyphusConfig = createSisyphusAgent(
model,
availableAgents,
undefined, // no tool names
availableSkills,
availableCategories
)
// Output to file
const outputPath = join(import.meta.dirname, "..", "sisyphus-prompt.md")
const content = `# Sisyphus System Prompt
> Auto-generated by \`script/generate-sisyphus-prompt.ts\`
> Generated at: ${new Date().toISOString()}
## Configuration
| Field | Value |
|-------|-------|
| Model | \`${model}\` |
| Max Tokens | \`${sisyphusConfig.maxTokens}\` |
| Mode | \`${sisyphusConfig.mode}\` |
| Thinking | ${sisyphusConfig.thinking ? `Budget: ${sisyphusConfig.thinking.budgetTokens}` : "N/A"} |
## Available Agents
${availableAgents.map((a) => `- **${a.name}**: ${a.description.split(".")[0]}`).join("\n")}
## Available Categories
${availableCategories.map((c) => `- **${c.name}**: ${c.description}`).join("\n")}
## Available Skills
${availableSkills.map((s) => `- **${s.name}**: ${s.description.split(".")[0]}`).join("\n")}
---
## Full System Prompt
\`\`\`markdown
${sisyphusConfig.prompt}
\`\`\`
`
writeFileSync(outputPath, content)
console.log(`Generated: ${outputPath}`)
console.log(`Prompt length: ${sisyphusConfig.prompt?.length ?? 0} characters`)

View File

@@ -703,6 +703,254 @@
"created_at": "2026-01-22T01:29:22Z",
"repoId": 1108837393,
"pullRequestNo": 974
},
{
"name": "boojongmin",
"id": 9567723,
"comment_id": 3784182787,
"created_at": "2026-01-22T12:39:26Z",
"repoId": 1108837393,
"pullRequestNo": 989
},
{
"name": "l3aro",
"id": 25253808,
"comment_id": 3786383804,
"created_at": "2026-01-22T19:52:42Z",
"repoId": 1108837393,
"pullRequestNo": 999
},
{
"name": "Ssoon-m",
"id": 89559826,
"comment_id": 3788539617,
"created_at": "2026-01-23T06:31:24Z",
"repoId": 1108837393,
"pullRequestNo": 1014
},
{
"name": "veetase",
"id": 2784250,
"comment_id": 3789028002,
"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
},
{
"name": "misyuari",
"id": 12197761,
"comment_id": 3798225767,
"created_at": "2026-01-26T07:31:02Z",
"repoId": 1108837393,
"pullRequestNo": 1132
},
{
"name": "boguan",
"id": 3226538,
"comment_id": 3798448537,
"created_at": "2026-01-26T08:40:37Z",
"repoId": 1108837393,
"pullRequestNo": 1137
},
{
"name": "boguan",
"id": 3226538,
"comment_id": 3798471978,
"created_at": "2026-01-26T08:46:03Z",
"repoId": 1108837393,
"pullRequestNo": 1137
},
{
"name": "Jeremy-Kr",
"id": 110771206,
"comment_id": 3799211732,
"created_at": "2026-01-26T11:59:13Z",
"repoId": 1108837393,
"pullRequestNo": 1141
},
{
"name": "orientpine",
"id": 32758428,
"comment_id": 3799897021,
"created_at": "2026-01-26T14:30:33Z",
"repoId": 1108837393,
"pullRequestNo": 1145
},
{
"name": "craftaholic",
"id": 63741110,
"comment_id": 3797014417,
"created_at": "2026-01-25T17:52:34Z",
"repoId": 1108837393,
"pullRequestNo": 1110
},
{
"name": "acamq",
"id": 179265037,
"comment_id": 3801038978,
"created_at": "2026-01-26T18:20:17Z",
"repoId": 1108837393,
"pullRequestNo": 1151
},
{
"name": "itsmylife44",
"id": 34112129,
"comment_id": 3802225779,
"created_at": "2026-01-26T23:20:30Z",
"repoId": 1108837393,
"pullRequestNo": 1157
},
{
"name": "ghtndl",
"id": 117787238,
"comment_id": 3802593326,
"created_at": "2026-01-27T01:27:17Z",
"repoId": 1108837393,
"pullRequestNo": 1158
},
{
"name": "alvinunreal",
"id": 204474669,
"comment_id": 3796402213,
"created_at": "2026-01-25T10:26:58Z",
"repoId": 1108837393,
"pullRequestNo": 1100
},
{
"name": "MoerAI",
"id": 26067127,
"comment_id": 3803968993,
"created_at": "2026-01-27T09:00:57Z",
"repoId": 1108837393,
"pullRequestNo": 1172
},
{
"name": "moha-abdi",
"id": 83307623,
"comment_id": 3804988070,
"created_at": "2026-01-27T12:36:21Z",
"repoId": 1108837393,
"pullRequestNo": 1179
},
{
"name": "zycaskevin",
"id": 223135116,
"comment_id": 3806137669,
"created_at": "2026-01-27T16:20:38Z",
"repoId": 1108837393,
"pullRequestNo": 1184
},
{
"name": "agno01",
"id": 4479380,
"comment_id": 3808373433,
"created_at": "2026-01-28T01:02:02Z",
"repoId": 1108837393,
"pullRequestNo": 1188
},
{
"name": "rooftop-Owl",
"id": 254422872,
"comment_id": 3809867225,
"created_at": "2026-01-28T08:46:58Z",
"repoId": 1108837393,
"pullRequestNo": 1197
},
{
"name": "youming-ai",
"id": 173424537,
"comment_id": 3811195276,
"created_at": "2026-01-28T13:04:16Z",
"repoId": 1108837393,
"pullRequestNo": 1203
},
{
"name": "KennyDizi",
"id": 16578966,
"comment_id": 3811619818,
"created_at": "2026-01-28T14:26:10Z",
"repoId": 1108837393,
"pullRequestNo": 1214
}
]
}

View File

@@ -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

View File

@@ -1,67 +1,64 @@
# AGENTS KNOWLEDGE BASE
## OVERVIEW
8 AI agents for multi-model orchestration. Sisyphus (primary), oracle, librarian, explore, multimodal-looker, Prometheus, Metis, Momus.
10 AI agents for multi-model orchestration. Sisyphus (primary), Atlas (orchestrator), oracle, librarian, explore, multimodal-looker, Prometheus, Metis, Momus, Sisyphus-Junior.
## STRUCTURE
```
agents/
├── atlas.ts # Orchestrator (1383 lines) - 7-phase delegation
├── sisyphus.ts # Main prompt (615 lines)
├── sisyphus-junior.ts # Delegated task executor
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
├── atlas.ts # Master Orchestrator (holds todo list)
├── sisyphus.ts # Main prompt (SF Bay Area engineer identity)
├── sisyphus-junior.ts # Delegated task executor (category-spawned)
├── oracle.ts # Strategic advisor (GPT-5.2)
├── librarian.ts # Multi-repo research (GLM-4.7-free)
├── explore.ts # Fast grep (Grok Code)
├── librarian.ts # Multi-repo research (GitHub CLI, Context7)
├── explore.ts # Fast contextual grep (Grok Code)
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
├── prometheus-prompt.ts # Planning (1196 lines) - interview mode
├── metis.ts # Plan consultant - pre-planning analysis
├── momus.ts # Plan reviewer - validation
├── types.ts # AgentModelConfig interface
├── utils.ts # createBuiltinAgents(), getAgentName()
├── prometheus-prompt.ts # Planning (Interview/Consultant mode, 1196 lines)
├── metis.ts # Pre-planning analysis (Gap detection)
├── momus.ts # Plan reviewer (Ruthless fault-finding)
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
├── types.ts # AgentModelConfig, AgentPromptMetadata
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback()
└── index.ts # builtinAgents export
```
## AGENT MODELS
| Agent | Model | Temperature | Purpose |
|-------|-------|-------------|---------|
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator, todo-driven |
| oracle | openai/gpt-5.2 | 0.1 | Read-only consultation, debugging |
| librarian | opencode/glm-4.7-free | 0.1 | Docs, GitHub search, OSS examples |
| explore | opencode/grok-code | 0.1 | Fast contextual grep |
| Agent | Model | Temp | Purpose |
|-------|-------|------|---------|
| 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/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, interview mode |
| Metis | anthropic/claude-sonnet-4-5 | 0.1 | Pre-planning gap 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 |
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
## HOW TO ADD
1. Create `src/agents/my-agent.ts` exporting `AgentConfig`
2. Add to `builtinAgents` in `src/agents/index.ts`
3. Update `AgentNameSchema` in `src/config/schema.ts`
4. Register in `src/index.ts` initialization
1. Create `src/agents/my-agent.ts` exporting factory + metadata.
2. Add to `agentSources` in `src/agents/utils.ts`.
3. Update `AgentNameSchema` in `src/config/schema.ts`.
4. Register in `src/index.ts` initialization.
## TOOL RESTRICTIONS
| Agent | Denied Tools |
|-------|-------------|
| oracle | write, edit, task, delegate_task |
| librarian | write, edit, task, delegate_task, call_omo_agent |
| explore | write, edit, task, delegate_task, call_omo_agent |
| multimodal-looker | Allowlist: read, glob, grep |
| multimodal-looker | Allowlist: read only |
| Sisyphus-Junior | task, delegate_task |
## KEY PATTERNS
- **Factory**: `createXXXAgent(model?: string): AgentConfig`
- **Metadata**: `XXX_PROMPT_METADATA: AgentPromptMetadata`
- **Tool restrictions**: `permission: { edit: "deny", bash: "ask" }`
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus
## PATTERNS
- **Factory**: `createXXXAgent(model: string): AgentConfig`
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers.
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`.
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas.
## ANTI-PATTERNS
- **Trust reports**: NEVER trust subagent "I'm done" - verify outputs
- **High temp**: Don't use >0.3 for code agents
- **Sequential calls**: Use `delegate_task` with `run_in_background`
- **Trust reports**: NEVER trust "I'm done" - verify outputs.
- **High temp**: Don't use >0.3 for code agents.
- **Sequential calls**: Use `delegate_task` with `run_in_background` for exploration.
- **Prometheus writing code**: Planner only - never implements.

File diff suppressed because it is too large Load Diff

View File

@@ -62,68 +62,29 @@ function formatToolsForPrompt(tools: AvailableTool[]): string {
return parts.join(", ")
}
export function buildKeyTriggersSection(agents: AvailableAgent[], skills: AvailableSkill[] = []): string {
export function buildKeyTriggersSection(agents: AvailableAgent[], _skills: AvailableSkill[] = []): string {
const keyTriggers = agents
.filter((a) => a.metadata.keyTrigger)
.map((a) => `- ${a.metadata.keyTrigger}`)
const skillTriggers = skills
.filter((s) => s.description)
.map((s) => `- **Skill \`${s.name}\`**: ${extractTriggerFromDescription(s.description)}`)
const allTriggers = [...keyTriggers, ...skillTriggers]
if (allTriggers.length === 0) return ""
if (keyTriggers.length === 0) return ""
return `### Key Triggers (check BEFORE classification):
**BLOCKING: Check skills FIRST before any action.**
If a skill matches, invoke it IMMEDIATELY via \`skill\` tool.
${allTriggers.join("\n")}
- **GitHub mention (@mention in issue/PR)** → This is a WORK REQUEST. Plan full cycle: investigate → implement → create PR
${keyTriggers.join("\n")}
- **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.`
}
function extractTriggerFromDescription(description: string): string {
const triggerMatch = description.match(/Trigger[s]?[:\s]+([^.]+)/i)
if (triggerMatch) return triggerMatch[1].trim()
const activateMatch = description.match(/Activate when[:\s]+([^.]+)/i)
if (activateMatch) return activateMatch[1].trim()
const useWhenMatch = description.match(/Use (?:this )?when[:\s]+([^.]+)/i)
if (useWhenMatch) return useWhenMatch[1].trim()
return description.split(".")[0] || description
}
export function buildToolSelectionTable(
agents: AvailableAgent[],
tools: AvailableTool[] = [],
skills: AvailableSkill[] = []
_skills: AvailableSkill[] = []
): string {
const rows: string[] = [
"### Tool & Skill Selection:",
"",
"**Priority Order**: Skills → Direct Tools → Agents",
"### Tool & Agent Selection:",
"",
]
if (skills.length > 0) {
rows.push("#### Skills (INVOKE FIRST if matching)")
rows.push("")
rows.push("| Skill | When to Use |")
rows.push("|-------|-------------|")
for (const skill of skills) {
const shortDesc = extractTriggerFromDescription(skill.description)
rows.push(`| \`${skill.name}\` | ${shortDesc} |`)
}
rows.push("")
}
rows.push("#### Tools & Agents")
rows.push("")
rows.push("| Resource | Cost | When to Use |")
rows.push("|----------|------|-------------|")
@@ -143,7 +104,7 @@ export function buildToolSelectionTable(
}
rows.push("")
rows.push("**Default flow**: skill (if match) → explore/librarian (background) + tools → oracle (if required)")
rows.push("**Default flow**: explore/librarian (background) + tools → oracle (if required)")
return rows.join("\n")
}
@@ -251,7 +212,7 @@ ${skillRows.join("\n")}
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**
@@ -279,14 +240,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
\`\`\``
}
@@ -325,7 +286,6 @@ export function buildHardBlocksSection(): string {
"| Commit without explicit request | Never |",
"| Speculate about unread code | Never |",
"| Leave code in broken state after failures | Never |",
"| Delegate without evaluating available skills | Never - MUST justify skill omissions |",
]
return `## Hard Blocks (NEVER violate)
@@ -341,7 +301,6 @@ export function buildAntiPatternsSection(): string {
"| **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 |",
"| **Debugging** | Shotgun debugging, random changes |",
]

View File

@@ -230,6 +230,8 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
- [Risk 2]: [Mitigation]
## Directives for Prometheus
### Core Directives
- MUST: [Required action]
- MUST: [Required action]
- MUST NOT: [Forbidden action]
@@ -237,6 +239,29 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
- PATTERN: Follow \`[file:lines]\`
- TOOL: Use \`[specific tool]\` for [purpose]
### QA/Acceptance Criteria Directives (MANDATORY)
> **ZERO USER INTERVENTION PRINCIPLE**: All acceptance criteria MUST be executable by agents.
- MUST: Write acceptance criteria as executable commands (curl, bun test, playwright actions)
- MUST: Include exact expected outputs, not vague descriptions
- MUST: Specify verification tool for each deliverable type (playwright for UI, curl for API, etc.)
- MUST NOT: Create criteria requiring "user manually tests..."
- MUST NOT: Create criteria requiring "user visually confirms..."
- MUST NOT: Create criteria requiring "user clicks/interacts..."
- MUST NOT: Use placeholders without concrete examples (bad: "[endpoint]", good: "/api/users")
Example of GOOD acceptance criteria:
\`\`\`
curl -s http://localhost:3000/api/health | jq '.status'
# Assert: Output is "ok"
\`\`\`
Example of BAD acceptance criteria (FORBIDDEN):
\`\`\`
User opens browser and checks if the page loads correctly.
User confirms the button works as expected.
\`\`\`
## Recommended Approach
[1-2 sentence summary of how to proceed]
\`\`\`
@@ -263,12 +288,16 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
- Ask generic questions ("What's the scope?")
- Proceed without addressing ambiguity
- Make assumptions about user's codebase
- Suggest acceptance criteria requiring user intervention ("user manually tests", "user confirms", "user clicks")
- Leave QA/acceptance criteria vague or placeholder-heavy
**ALWAYS**:
- Classify intent FIRST
- Be specific ("Should this change UserService only, or also AuthService?")
- Explore before asking (for Build/Research intents)
- Provide actionable directives for Prometheus
- Include QA automation directives in every output
- Ensure acceptance criteria are agent-executable (commands, not human actions)
`
const metisRestrictions = createAgentToolRestrictions([

View File

@@ -274,7 +274,7 @@ Before diving into consultation, classify the work intent. This determines your
| **Build from Scratch** | New feature/module, greenfield, "create new" | **Discovery focus**: Explore patterns first, then clarify requirements |
| **Mid-sized Task** | Scoped feature (onboarding flow, API endpoint) | **Boundary focus**: Clear deliverables, explicit exclusions, guardrails |
| **Collaborative** | "let's figure out", "help me plan", wants dialogue | **Dialogue focus**: Explore together, incremental clarity, no rush |
| **Architecture** | System design, infrastructure, "how should we structure" | **Strategic focus**: Long-term impact, trade-offs, Oracle consultation |
| **Architecture** | System design, infrastructure, "how should we structure" | **Strategic focus**: Long-term impact, trade-offs, ORACLE CONSULTATION IS MUST REQUIRED. NO EXCEPTIONS. |
| **Research** | Goal exists but path unclear, investigation needed | **Investigation focus**: Parallel probes, synthesis, exit criteria |
### Simple Request Detection (CRITICAL)
@@ -319,8 +319,8 @@ Or should I just note down this single fix?"
**Research First:**
\`\`\`typescript
delegate_task(agent="explore", prompt="Find all usages of [target] using lsp_find_references pattern...", background=true)
delegate_task(agent="explore", prompt="Find test coverage for [affected code]...", background=true)
delegate_task(subagent_type="explore", prompt="Find all usages of [target] using lsp_find_references pattern...", run_in_background=true)
delegate_task(subagent_type="explore", prompt="Find test coverage for [affected code]...", run_in_background=true)
\`\`\`
**Interview Focus:**
@@ -343,9 +343,9 @@ delegate_task(agent="explore", prompt="Find test coverage for [affected code]...
**Pre-Interview Research (MANDATORY):**
\`\`\`typescript
// Launch BEFORE asking user questions
delegate_task(agent="explore", prompt="Find similar implementations in codebase...", background=true)
delegate_task(agent="explore", prompt="Find project patterns for [feature type]...", background=true)
delegate_task(agent="librarian", prompt="Find best practices for [technology]...", background=true)
delegate_task(subagent_type="explore", prompt="Find similar implementations in codebase...", run_in_background=true)
delegate_task(subagent_type="explore", prompt="Find project patterns for [feature type]...", run_in_background=true)
delegate_task(subagent_type="librarian", prompt="Find best practices for [technology]...", run_in_background=true)
\`\`\`
**Interview Focus** (AFTER research):
@@ -384,7 +384,7 @@ Based on your stack, I'd recommend NextAuth.js - it integrates well with Next.js
Run this check:
\`\`\`typescript
delegate_task(agent="explore", prompt="Find test infrastructure: package.json test scripts, test config files (jest.config, vitest.config, pytest.ini, etc.), existing test files (*.test.*, *.spec.*, test_*). Report: 1) Does test infra exist? 2) What framework? 3) Example test file patterns.", background=true)
delegate_task(subagent_type="explore", prompt="Find test infrastructure: package.json test scripts, test config files (jest.config, vitest.config, pytest.ini, etc.), existing test files (*.test.*, *.spec.*, test_*). Report: 1) Does test infra exist? 2) What framework? 3) Example test file patterns.", run_in_background=true)
\`\`\`
#### Step 2: Ask the Test Question (MANDATORY)
@@ -473,13 +473,13 @@ Add to draft immediately:
**Research First:**
\`\`\`typescript
delegate_task(agent="explore", prompt="Find current system architecture and patterns...", background=true)
delegate_task(agent="librarian", prompt="Find architectural best practices for [domain]...", background=true)
delegate_task(subagent_type="explore", prompt="Find current system architecture and patterns...", run_in_background=true)
delegate_task(subagent_type="librarian", prompt="Find architectural best practices for [domain]...", run_in_background=true)
\`\`\`
**Oracle Consultation** (recommend when stakes are high):
\`\`\`typescript
delegate_task(agent="oracle", prompt="Architecture consultation needed: [context]...", background=false)
delegate_task(subagent_type="oracle", prompt="Architecture consultation needed: [context]...", run_in_background=false)
\`\`\`
**Interview Focus:**
@@ -496,9 +496,9 @@ delegate_task(agent="oracle", prompt="Architecture consultation needed: [context
**Parallel Investigation:**
\`\`\`typescript
delegate_task(agent="explore", prompt="Find how X is currently handled...", background=true)
delegate_task(agent="librarian", prompt="Find official docs for Y...", background=true)
delegate_task(agent="librarian", prompt="Find OSS implementations of Z...", background=true)
delegate_task(subagent_type="explore", prompt="Find how X is currently handled...", run_in_background=true)
delegate_task(subagent_type="librarian", prompt="Find official docs for Y...", run_in_background=true)
delegate_task(subagent_type="librarian", prompt="Find OSS implementations of Z...", run_in_background=true)
\`\`\`
**Interview Focus:**
@@ -524,17 +524,17 @@ delegate_task(agent="librarian", prompt="Find OSS implementations of Z...", back
**For Understanding Codebase:**
\`\`\`typescript
delegate_task(agent="explore", prompt="Find all files related to [topic]. Show patterns, conventions, and structure.", background=true)
delegate_task(subagent_type="explore", prompt="Find all files related to [topic]. Show patterns, conventions, and structure.", run_in_background=true)
\`\`\`
**For External Knowledge:**
\`\`\`typescript
delegate_task(agent="librarian", prompt="Find official documentation for [library]. Focus on [specific feature] and best practices.", background=true)
delegate_task(subagent_type="librarian", prompt="Find official documentation for [library]. Focus on [specific feature] and best practices.", run_in_background=true)
\`\`\`
**For Implementation Examples:**
\`\`\`typescript
delegate_task(agent="librarian", prompt="Find open source implementations of [feature]. Look for production-quality examples.", background=true)
delegate_task(subagent_type="librarian", prompt="Find open source implementations of [feature]. Look for production-quality examples.", run_in_background=true)
\`\`\`
## Interview Mode Anti-Patterns
@@ -631,20 +631,20 @@ todoWrite([
\`\`\`typescript
delegate_task(
agent="Metis (Plan Consultant)",
subagent_type="metis",
prompt=\`Review this planning session before I generate the work plan:
**User's Goal**: {summarize what user wants}
**What We Discussed**:
{key points from interview}
**My Understanding**:
{your interpretation of requirements}
**Research Findings**:
{key discoveries from explore/librarian}
Please identify:
1. Questions I should have asked but didn't
2. Guardrails that need to be explicitly set
@@ -652,7 +652,7 @@ delegate_task(
4. Assumptions I'm making that need validation
5. Missing acceptance criteria
6. Edge cases not addressed\`,
background=false
run_in_background=false
)
\`\`\`
@@ -712,18 +712,18 @@ Before presenting summary, verify:
<gap_handling>
**IF gap is CRITICAL (requires user decision):**
1. Generate plan with placeholder: \`[DECISION NEEDED: {description}]\`
2. In summary, list under "⚠️ Decisions Needed"
2. In summary, list under "Decisions Needed"
3. Ask specific question with options
4. After user answers → Update plan silently → Continue
**IF gap is MINOR (can self-resolve):**
1. Fix immediately in the plan
2. In summary, list under "📝 Auto-Resolved"
2. In summary, list under "Auto-Resolved"
3. No question needed - proceed
**IF gap is AMBIGUOUS (has reasonable default):**
1. Apply sensible default
2. In summary, list under " Defaults Applied"
2. In summary, list under "Defaults Applied"
3. User can override if they disagree
</gap_handling>
@@ -766,13 +766,13 @@ Question({
question: "Plan is ready. How would you like to proceed?",
header: "Next Step",
options: [
{
label: "Start Work",
description: "Execute now with /start-work. Plan looks solid."
{
label: "Start Work",
description: "Execute now with /start-work. Plan looks solid."
},
{
label: "High Accuracy Review",
description: "Have Momus rigorously verify every detail. Adds review loop but guarantees precision."
{
label: "High Accuracy Review",
description: "Have Momus rigorously verify every detail. Adds review loop but guarantees precision."
}
]
}]
@@ -797,15 +797,15 @@ Question({
// After generating initial plan
while (true) {
const result = delegate_task(
agent="Momus (Plan Reviewer)",
subagent_type="momus",
prompt=".sisyphus/plans/{name}.md",
background=false
run_in_background=false
)
if (result.verdict === "OKAY") {
break // Plan approved - exit loop
}
// Momus rejected - YOU MUST FIX AND RESUBMIT
// Read Momus's feedback carefully
// Address EVERY issue raised
@@ -863,6 +863,20 @@ Generate plan to: \`.sisyphus/plans/{name}.md\`
\`\`\`markdown
# {Plan Title}
## TL;DR
> **Quick Summary**: [1-2 sentences capturing the core objective and approach]
>
> **Deliverables**: [Bullet list of concrete outputs]
> - [Output 1]
> - [Output 2]
>
> **Estimated Effort**: [Quick | Short | Medium | Large | XL]
> **Parallel Execution**: [YES - N waves | NO - sequential]
> **Critical Path**: [Task X → Task Y → Task Z]
---
## Context
### Original Request
@@ -939,53 +953,89 @@ Each TODO follows RED-GREEN-REFACTOR:
- Example: Create \`src/__tests__/example.test.ts\`
- Verify: \`bun test\` → 1 test passes
### If Manual QA Only
### If Automated Verification Only (NO User Intervention)
**CRITICAL**: Without automated tests, manual verification MUST be exhaustive.
> **CRITICAL PRINCIPLE: ZERO USER INTERVENTION**
>
> **NEVER** create acceptance criteria that require:
> - "User manually tests..." / "사용자가 직접 테스트..."
> - "User visually confirms..." / "사용자가 눈으로 확인..."
> - "User interacts with..." / "사용자가 직접 조작..."
> - "Ask user to verify..." / "사용자에게 확인 요청..."
> - ANY step that requires a human to perform an action
>
> **ALL verification MUST be automated and executable by the agent.**
> If a verification cannot be automated, find an automated alternative or explicitly note it as a known limitation.
Each TODO includes detailed verification procedures:
Each TODO includes EXECUTABLE verification procedures that agents can run directly:
**By Deliverable Type:**
| Type | Verification Tool | Procedure |
|------|------------------|-----------|
| **Frontend/UI** | Playwright browser | Navigate, interact, screenshot |
| **TUI/CLI** | interactive_bash (tmux) | Run command, verify output |
| **API/Backend** | curl / httpie | Send request, verify response |
| **Library/Module** | Node/Python REPL | Import, call, verify |
| **Config/Infra** | Shell commands | Apply, verify state |
| Type | Verification Tool | Automated Procedure |
|------|------------------|---------------------|
| **Frontend/UI** | Playwright browser via playwright skill | Agent navigates, clicks, screenshots, asserts DOM state |
| **TUI/CLI** | interactive_bash (tmux) | Agent runs command, captures output, validates expected strings |
| **API/Backend** | curl / httpie via Bash | Agent sends request, parses response, validates JSON fields |
| **Library/Module** | Node/Python REPL via Bash | Agent imports, calls function, compares output |
| **Config/Infra** | Shell commands via Bash | Agent applies config, runs state check, validates output |
**Evidence Required:**
- Commands run with actual output
- Screenshots for visual changes
- Response bodies for API changes
- Terminal output for CLI changes
**Evidence Requirements (Agent-Executable):**
- Command output captured and compared against expected patterns
- Screenshots saved to .sisyphus/evidence/ for visual verification
- JSON response fields validated with specific assertions
- Exit codes checked (0 = success)
---
## Task Flow
## Execution Strategy
### Parallel Execution Waves
> Maximize throughput by grouping independent tasks into parallel waves.
> Each wave completes before the next begins.
\`\`\`
Task 1 → Task 2 → Task 3
↘ Task 4 (parallel)
Wave 1 (Start Immediately):
├── Task 1: [no dependencies]
└── Task 5: [no dependencies]
Wave 2 (After Wave 1):
├── Task 2: [depends: 1]
├── Task 3: [depends: 1]
└── Task 6: [depends: 5]
Wave 3 (After Wave 2):
└── Task 4: [depends: 2, 3]
Critical Path: Task 1 → Task 2 → Task 4
Parallel Speedup: ~40% faster than sequential
\`\`\`
## Parallelization
### Dependency Matrix
| Group | Tasks | Reason |
|-------|-------|--------|
| A | 2, 3 | Independent files |
| Task | Depends On | Blocks | Can Parallelize With |
|------|------------|--------|---------------------|
| 1 | None | 2, 3 | 5 |
| 2 | 1 | 4 | 3, 6 |
| 3 | 1 | 4 | 2, 6 |
| 4 | 2, 3 | None | None (final) |
| 5 | None | 6 | 1 |
| 6 | 5 | None | 2, 3 |
| Task | Depends On | Reason |
|------|------------|--------|
| 4 | 1 | Requires output from 1 |
### Agent Dispatch Summary
| Wave | Tasks | Recommended Agents |
|------|-------|-------------------|
| 1 | 1, 5 | delegate_task(category="...", load_skills=[...], run_in_background=true) |
| 2 | 2, 3, 6 | dispatch parallel after Wave 1 completes |
| 3 | 4 | final integration task |
---
## TODOs
> Implementation + Test = ONE Task. Never separate.
> Specify parallelizability for EVERY task.
> EVERY task MUST have: Recommended Agent Profile + Parallelization info.
- [ ] 1. [Task Title]
@@ -996,86 +1046,123 @@ Task 1 → Task 2 → Task 3
**Must NOT do**:
- [Specific exclusions from guardrails]
**Parallelizable**: YES (with 3, 4) | NO (depends on 0)
**Recommended Agent Profile**:
> Select category + skills based on task domain. Justify each choice.
- **Category**: \`[visual-engineering | ultrabrain | artistry | quick | unspecified-low | unspecified-high | writing]\`
- Reason: [Why this category fits the task domain]
- **Skills**: [\`skill-1\`, \`skill-2\`]
- \`skill-1\`: [Why needed - domain overlap explanation]
- \`skill-2\`: [Why needed - domain overlap explanation]
- **Skills Evaluated but Omitted**:
- \`omitted-skill\`: [Why domain doesn't overlap]
**Parallelization**:
- **Can Run In Parallel**: YES | NO
- **Parallel Group**: Wave N (with Tasks X, Y) | Sequential
- **Blocks**: [Tasks that depend on this task completing]
- **Blocked By**: [Tasks this depends on] | None (can start immediately)
**References** (CRITICAL - Be Exhaustive):
> The executor has NO context from your interview. References are their ONLY guide.
> Each reference must answer: "What should I look at and WHY?"
**Pattern References** (existing code to follow):
- \`src/services/auth.ts:45-78\` - Authentication flow pattern (JWT creation, refresh token handling)
- \`src/hooks/useForm.ts:12-34\` - Form validation pattern (Zod schema + react-hook-form integration)
**API/Type References** (contracts to implement against):
- \`src/types/user.ts:UserDTO\` - Response shape for user endpoints
- \`src/api/schema.ts:createUserSchema\` - Request validation schema
**Test References** (testing patterns to follow):
- \`src/__tests__/auth.test.ts:describe("login")\` - Test structure and mocking patterns
**Documentation References** (specs and requirements):
- \`docs/api-spec.md#authentication\` - API contract details
- \`ARCHITECTURE.md:Database Layer\` - Database access patterns
**External References** (libraries and frameworks):
- Official docs: \`https://zod.dev/?id=basic-usage\` - Zod validation syntax
- Example repo: \`github.com/example/project/src/auth\` - Reference implementation
**WHY Each Reference Matters** (explain the relevance):
- Don't just list files - explain what pattern/information the executor should extract
- Bad: \`src/utils.ts\` (vague, which utils? why?)
- Good: \`src/utils/validation.ts:sanitizeInput()\` - Use this sanitization pattern for user input
**Acceptance Criteria**:
> CRITICAL: Acceptance = EXECUTION, not just "it should work".
> The executor MUST run these commands and verify output.
> **CRITICAL: AGENT-EXECUTABLE VERIFICATION ONLY**
>
> - Acceptance = EXECUTION by the agent, not "user checks if it works"
> - Every criterion MUST be verifiable by running a command or using a tool
> - NO steps like "user opens browser", "user clicks", "user confirms"
> - If you write "[placeholder]" - REPLACE IT with actual values based on task context
**If TDD (tests enabled):**
- [ ] Test file created: \`[path].test.ts\`
- [ ] Test covers: [specific scenario]
- [ ] \`bun test [file]\` → PASS (N tests, 0 failures)
- [ ] Test file created: src/auth/login.test.ts
- [ ] Test covers: successful login returns JWT token
- [ ] bun test src/auth/login.test.ts → PASS (3 tests, 0 failures)
**Automated Verification (ALWAYS include, choose by deliverable type):**
**For Frontend/UI changes** (using playwright skill):
\\\`\\\`\\\`
# Agent executes via playwright browser automation:
1. Navigate to: http://localhost:3000/login
2. Fill: input[name="email"] with "test@example.com"
3. Fill: input[name="password"] with "password123"
4. Click: button[type="submit"]
5. Wait for: selector ".dashboard-welcome" to be visible
6. Assert: text "Welcome back" appears on page
7. Screenshot: .sisyphus/evidence/task-1-login-success.png
\\\`\\\`\\\`
**For TUI/CLI changes** (using interactive_bash):
\\\`\\\`\\\`
# Agent executes via tmux session:
1. Command: ./my-cli --config test.yaml
2. Wait for: "Configuration loaded" in output
3. Send keys: "q" to quit
4. Assert: Exit code 0
5. Assert: Output contains "Goodbye"
\\\`\\\`\\\`
**For API/Backend changes** (using Bash curl):
\\\`\\\`\\\`bash
# Agent runs:
curl -s -X POST http://localhost:8080/api/users \\
-H "Content-Type: application/json" \\
-d '{"email":"new@test.com","name":"Test User"}' \\
| jq '.id'
# Assert: Returns non-empty UUID
# Assert: HTTP status 201
\\\`\\\`\\\`
**For Library/Module changes** (using Bash node/bun):
\\\`\\\`\\\`bash
# Agent runs:
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('test@example.com'))"
# Assert: Output is "true"
**Manual Execution Verification (ALWAYS include, even with tests):**
*Choose based on deliverable type:*
**For Frontend/UI changes:**
- [ ] Using playwright browser automation:
- Navigate to: \`http://localhost:[port]/[path]\`
- Action: [click X, fill Y, scroll to Z]
- Verify: [visual element appears, animation completes, state changes]
- Screenshot: Save evidence to \`.sisyphus/evidence/[task-id]-[step].png\`
**For TUI/CLI changes:**
- [ ] Using interactive_bash (tmux session):
- Command: \`[exact command to run]\`
- Input sequence: [if interactive, list inputs]
- Expected output contains: \`[expected string or pattern]\`
- Exit code: [0 for success, specific code if relevant]
**For API/Backend changes:**
- [ ] Request: \`curl -X [METHOD] http://localhost:[port]/[endpoint] -H "Content-Type: application/json" -d '[body]'\`
- [ ] Response status: [200/201/etc]
- [ ] Response body contains: \`{"key": "expected_value"}\`
**For Library/Module changes:**
- [ ] REPL verification:
\`\`\`
> import { [function] } from '[module]'
> [function]([args])
Expected: [output]
\`\`\`
**For Config/Infra changes:**
- [ ] Apply: \`[command to apply config]\`
- [ ] Verify state: \`[command to check state]\`\`[expected output]\`
**Evidence Required:**
- [ ] Command output captured (copy-paste actual terminal output)
- [ ] Screenshot saved (for visual changes)
- [ ] Response body logged (for API changes)
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('invalid'))"
# Assert: Output is "false"
\\\`\\\`\\\`
**For Config/Infra changes** (using Bash):
\\\`\\\`\\\`bash
# Agent runs:
docker compose up -d
# Wait 5s for containers
docker compose ps --format json | jq '.[].State'
# Assert: All states are "running"
\\\`\\\`\\\`
**Evidence to Capture:**
- [ ] Terminal output from verification commands (actual output, not expected)
- [ ] Screenshot files in .sisyphus/evidence/ for UI changes
- [ ] JSON response bodies for API changes
**Commit**: YES | NO (groups with N)
- Message: \`type(scope): desc\`
@@ -1118,7 +1205,7 @@ The draft served its purpose. Clean up:
Bash("rm .sisyphus/drafts/{name}.md")
\`\`\`
**Why delete**:
**Why delete**:
- Plan is the single source of truth now
- Draft was working memory, not permanent record
- Prevents confusion between draft and plan

View File

@@ -20,31 +20,6 @@ ALLOWED: call_omo_agent - You CAN spawn explore/librarian agents for research.
You work ALONE for implementation. No delegation of implementation tasks.
</Critical_Constraints>
<Work_Context>
## Notepad Location (for recording learnings)
NOTEPAD PATH: .sisyphus/notepads/{plan-name}/
- learnings.md: Record patterns, conventions, successful approaches
- issues.md: Record problems, blockers, gotchas encountered
- decisions.md: Record architectural choices and rationales
- problems.md: Record unresolved issues, technical debt
You SHOULD append findings to notepad files after completing work.
## Plan Location (READ ONLY)
PLAN PATH: .sisyphus/plans/{plan-name}.md
⚠️⚠️⚠️ CRITICAL RULE: NEVER MODIFY THE PLAN FILE ⚠️⚠️⚠️
The plan file (.sisyphus/plans/*.md) is SACRED and READ-ONLY.
- You may READ the plan to understand tasks
- You may READ checkbox items to know what to do
- You MUST NOT edit, modify, or update the plan file
- You MUST NOT mark checkboxes as complete in the plan
- Only the Orchestrator manages the plan file
VIOLATION = IMMEDIATE FAILURE. The Orchestrator tracks plan state.
</Work_Context>
<Todo_Discipline>
TODO OBSESSION (NON-NEGOTIABLE):
- 2+ steps → todowrite FIRST, atomic breakdown

View File

@@ -14,7 +14,23 @@ import {
categorizeTools,
} from "./dynamic-agent-prompt-builder"
const SISYPHUS_ROLE_SECTION = `<Role>
function buildDynamicSisyphusPrompt(
availableAgents: AvailableAgent[],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
availableCategories: AvailableCategory[] = []
): string {
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
const exploreSection = buildExploreSection(availableAgents)
const librarianSection = buildLibrarianSection(availableAgents)
const categorySkillsGuide = buildCategorySkillsDelegationGuide(availableCategories, availableSkills)
const delegationTable = buildDelegationTable(availableAgents)
const oracleSection = buildOracleSection(availableAgents)
const hardBlocks = buildHardBlocksSection()
const antiPatterns = buildAntiPatternsSection()
return `<Role>
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
**Why Sisyphus?**: Humans roll their boulder every day. So do you. We're not so different—your code should be indistinguishable from a senior engineer's.
@@ -26,37 +42,26 @@ You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMy
- Adapting to codebase maturity (disciplined vs chaotic)
- Delegating specialized work to the right subagents
- Parallel execution for maximum throughput
- Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITELY.
- Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITLY.
- KEEP IN MIND: YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION]), BUT IF NOT USER REQUESTED YOU TO WORK, NEVER START WORK.
**Operating Mode**: You NEVER work alone when specialists are available. Frontend work → delegate. Deep research → parallel background agents (async subagents). Complex architecture → consult Oracle.
</Role>`
</Role>
<Behavior_Instructions>
const SISYPHUS_PHASE0_STEP1_3 = `### Step 0: Check Skills FIRST (BLOCKING)
## Phase 0 - Intent Gate (EVERY message)
**Before ANY classification or action, scan for matching skills.**
\`\`\`
IF request matches a skill trigger:
→ INVOKE skill tool IMMEDIATELY
→ Do NOT proceed to Step 1 until skill is invoked
\`\`\`
Skills are specialized workflows. When relevant, they handle the task better than manual orchestration.
---
${keyTriggers}
### Step 1: Classify Request Type
| Type | Signal | Action |
|------|--------|--------|
| **Skill Match** | Matches skill trigger phrase | **INVOKE skill FIRST** via \`skill\` tool |
| **Trivial** | Single file, known location, direct answer | Direct tools only (UNLESS Key Trigger applies) |
| **Explicit** | Specific file/line, clear command | Execute directly |
| **Exploratory** | "How does X work?", "Find Y" | Fire explore (1-3) + tools in parallel |
| **Open-ended** | "Improve", "Refactor", "Add feature" | Assess codebase first |
| **GitHub Work** | Mentioned in issue, "look into X and create PR" | **Full cycle**: investigate → implement → verify → create PR (see GitHub Workflow section) |
| **Ambiguous** | Unclear scope, multiple interpretations | Ask ONE clarifying question |
### Step 2: Check for Ambiguity
@@ -70,16 +75,18 @@ Skills are specialized workflows. When relevant, they handle the task better tha
| User's design seems flawed or suboptimal | **MUST raise concern** before implementing |
### Step 3: Validate Before Acting
**Assumptions Check:**
- Do I have any implicit assumptions that might affect the outcome?
- Is the search scope clear?
- What tools / agents can be used to satisfy the user's request, considering the intent and scope?
- What are the list of tools / agents do I have?
- What tools / agents can I leverage for what tasks?
- Specifically, how can I leverage them like?
- background tasks?
- parallel tool calls?
- lsp tools?
**Delegation Check (MANDATORY before acting directly):**
1. Is there a specialized agent that perfectly matches this request?
2. If not, is there a \`delegate_task\` category best describes this task? (visual-engineering, ultrabrain, quick etc.) What skills are available to equip the agent with?
- MUST FIND skills to use, for: \`delegate_task(load_skills=[{skill1}, ...])\` MUST PASS SKILL AS DELEGATE TASK PARAMETER.
3. Can I do it myself for the best result, FOR SURE? REALLY, REALLY, THERE IS NO APPROPRIATE CATEGORIES TO WORK WITH?
**Default Bias: DELEGATE. WORK YOURSELF ONLY WHEN IT IS SUPER SIMPLE.**
### When to Challenge the User
If you observe:
@@ -93,9 +100,11 @@ Then: Raise your concern concisely. Propose an alternative. Ask if they want to
I notice [observation]. This might cause [problem] because [reason].
Alternative: [your suggestion].
Should I proceed with your original request, or try the alternative?
\`\`\``
\`\`\`
const SISYPHUS_PHASE1 = `## Phase 1 - Codebase Assessment (for Open-ended tasks)
---
## Phase 1 - Codebase Assessment (for Open-ended tasks)
Before following existing patterns, assess whether they're worth following.
@@ -116,137 +125,34 @@ Before following existing patterns, assess whether they're worth following.
IMPORTANT: If codebase appears undisciplined, verify before assuming:
- Different patterns may serve different purposes (intentional)
- Migration might be in progress
- You might be looking at the wrong reference files`
- You might be looking at the wrong reference files
const SISYPHUS_PRE_DELEGATION_PLANNING = `### Pre-Delegation Planning (MANDATORY)
---
**BEFORE every \`delegate_task\` call, EXPLICITLY declare your reasoning.**
## Phase 2A - Exploration & Research
#### Step 1: Identify Task Requirements
${toolSelection}
Ask yourself:
- What is the CORE objective of this task?
- What domain does this task belong to?
- What skills/capabilities are CRITICAL for success?
${exploreSection}
#### Step 2: Match to Available Categories and Skills
${librarianSection}
**For EVERY delegation, you MUST:**
1. **Review the Category + Skills Delegation Guide** (above)
2. **Read each category's description** to find the best domain match
3. **Read each skill's description** to identify relevant expertise
4. **Select category** whose domain BEST matches task requirements
5. **Include ALL skills** whose expertise overlaps with task domain
#### Step 3: Declare BEFORE Calling
**MANDATORY FORMAT:**
\`\`\`
I will use delegate_task with:
- **Category**: [selected-category-name]
- **Why this category**: [how category description matches task domain]
- **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]
- **Expected Outcome**: [what success looks like]
\`\`\`
**Then** make the delegate_task call.
#### Examples
**CORRECT: Full Evaluation**
\`\`\`
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"]
- **Skill evaluation**:
- skill-a: INCLUDED - description says "[quote]" which applies to this task
- skill-b: INCLUDED - description says "[quote]" which is needed here
- skill-c: OMITTED - description says "[quote]" which doesn't apply because [reason]
- **Expected Outcome**: [concrete deliverable]
delegate_task(
category="[category-name]",
skills=["skill-a", "skill-b"],
prompt="..."
)
\`\`\`
**CORRECT: Agent-Specific (for exploration/consultation)**
\`\`\`
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)
- **Expected Outcome**: [what agent should return]
delegate_task(
subagent_type="[agent-name]",
skills=[],
prompt="..."
)
\`\`\`
**CORRECT: Background Exploration**
\`\`\`
I will use delegate_task with:
- **Agent**: explore
- **Reason**: Need to find all authentication implementations across the codebase - this is contextual grep
- **Skills**: []
- **Expected Outcome**: List of files containing auth patterns
delegate_task(
subagent_type="explore",
run_in_background=true,
skills=[],
prompt="Find all authentication implementations in the codebase"
)
\`\`\`
**WRONG: No Skill Evaluation**
\`\`\`
delegate_task(category="...", skills=[], prompt="...") // Where's the justification?
\`\`\`
**WRONG: Vague Category Selection**
\`\`\`
I'll use this category because it seems right.
\`\`\`
#### Enforcement
**BLOCKING VIOLATION**: If you call \`delegate_task\` without:
1. Explaining WHY category was selected (based on description)
2. Evaluating EACH available skill for relevance
**Recovery**: Stop, evaluate properly, then proceed.`
const SISYPHUS_PARALLEL_EXECUTION = `### Parallel Execution (DEFAULT behavior)
### Parallel Execution (DEFAULT behavior)
**Explore/Librarian = Grep, not consultants.
\`\`\`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
result = delegate_task(...) // Never wait synchronously for explore/librarian
result = delegate_task(..., run_in_background=false) // Never wait synchronously for explore/librarian
\`\`\`
### Background Result Collection:
@@ -255,19 +161,6 @@ result = delegate_task(...) // Never wait synchronously for explore/librarian
3. When results needed: \`background_output(task_id="...")\`
4. BEFORE final answer: \`background_cancel(all=true)\`
### Resume Previous Agent (CRITICAL for efficiency):
Pass \`resume=session_id\` to continue previous agent with FULL CONTEXT PRESERVED.
**ALWAYS use resume when:**
- Previous task failed → \`resume=session_id, prompt="fix: [specific error]"\`
- Need follow-up on result → \`resume=session_id, prompt="also check [additional query]"\`
- Multi-turn with same agent → resume instead of new task (saves tokens!)
**Example:**
\`\`\`
delegate_task(resume="ses_abc123", prompt="The previous search missed X. Also look for Y.")
\`\`\`
### Search Stop Conditions
STOP searching when:
@@ -276,27 +169,32 @@ STOP searching when:
- 2 search iterations yielded no new useful data
- Direct answer found
**DO NOT over-explore. Time is precious.**`
**DO NOT over-explore. Time is precious.**
const SISYPHUS_PHASE2B_PRE_IMPLEMENTATION = `## Phase 2B - Implementation
---
## Phase 2B - Implementation
### Pre-Implementation:
1. If task has 2+ steps → Create todo list IMMEDIATELY, IN SUPER DETAIL. No announcements—just create it.
2. Mark current task \`in_progress\` before starting
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS`
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS
const SISYPHUS_DELEGATION_PROMPT_STRUCTURE = `### Delegation Prompt Structure (MANDATORY - ALL 7 sections):
${categorySkillsGuide}
${delegationTable}
### Delegation Prompt Structure (MANDATORY - ALL 6 sections):
When delegating, your prompt MUST include:
\`\`\`
1. TASK: Atomic, specific goal (one action per delegation)
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
3. REQUIRED SKILLS: Which skill to invoke
4. REQUIRED TOOLS: Explicit tool whitelist (prevents tool sprawl)
5. MUST DO: Exhaustive requirements - leave NOTHING implicit
6. MUST NOT DO: Forbidden actions - anticipate and block rogue behavior
7. CONTEXT: File paths, existing patterns, constraints
3. REQUIRED TOOLS: Explicit tool whitelist (prevents tool sprawl)
4. MUST DO: Exhaustive requirements - leave NOTHING implicit
5. MUST NOT DO: Forbidden actions - anticipate and block rogue behavior
6. CONTEXT: File paths, existing patterns, constraints
\`\`\`
AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
@@ -305,44 +203,37 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
- EXPECTED RESULT CAME OUT?
- DID THE AGENT FOLLOWED "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
**Vague prompts = rejected. Be exhaustive.**`
**Vague prompts = rejected. Be exhaustive.**
const SISYPHUS_GITHUB_WORKFLOW = `### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
### Session Continuity (MANDATORY)
When you're mentioned in GitHub issues or asked to "look into" something and "create PR":
Every \`delegate_task()\` output includes a session_id. **USE IT.**
**This is NOT just investigation. This is a COMPLETE WORK CYCLE.**
**ALWAYS continue when:**
| Scenario | Action |
|----------|--------|
| 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."\` |
#### Pattern Recognition:
- "@sisyphus look into X"
- "look into X and create PR"
- "investigate Y and make PR"
- Mentioned in issue comments
**Why session_id is CRITICAL:**
- Subagent has FULL conversation context preserved
- No repeated file reads, exploration, or setup
- Saves 70%+ tokens on follow-ups
- Subagent knows what it already tried/learned
#### Required Workflow (NON-NEGOTIABLE):
1. **Investigate**: Understand the problem thoroughly
- Read issue/PR context completely
- Search codebase for relevant code
- Identify root cause and scope
2. **Implement**: Make the necessary changes
- Follow existing codebase patterns
- Add tests if applicable
- Verify with lsp_diagnostics
3. **Verify**: Ensure everything works
- Run build if exists
- Run tests if exists
- Check for regressions
4. **Create PR**: Complete the cycle
- Use \`gh pr create\` with meaningful title and description
- Reference the original issue number
- Summarize what was changed and why
\`\`\`typescript
// WRONG: Starting fresh loses all context
delegate_task(category="quick", prompt="Fix the type error in auth.ts...")
**EMPHASIS**: "Look into" does NOT mean "just investigate and report back."
It means "investigate, understand, implement a solution, and create a PR."
// CORRECT: Resume preserves everything
delegate_task(session_id="ses_abc123", prompt="Fix: Type error on line 42")
\`\`\`
**If the user says "look into X and create PR", they expect a PR, not just analysis.**`
**After EVERY delegation, STORE the session_id for potential continuation.**
const SISYPHUS_CODE_CHANGES = `### Code Changes:
### Code Changes:
- Match existing patterns (if codebase is disciplined)
- Propose approach first (if codebase is chaotic)
- Never suppress type errors with \`as any\`, \`@ts-ignore\`, \`@ts-expect-error\`
@@ -368,9 +259,11 @@ If project has build/test commands, run them at task completion.
| Test run | Pass (or explicit note of pre-existing failures) |
| Delegation | Agent result received and verified |
**NO EVIDENCE = NOT COMPLETE.**`
**NO EVIDENCE = NOT COMPLETE.**
const SISYPHUS_PHASE2C = `## Phase 2C - Failure Recovery
---
## Phase 2C - Failure Recovery
### When Fixes Fail:
@@ -386,9 +279,11 @@ const SISYPHUS_PHASE2C = `## Phase 2C - Failure Recovery
4. **CONSULT** Oracle with full failure context
5. If Oracle cannot resolve → **ASK USER** before proceeding
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"`
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"
const SISYPHUS_PHASE3 = `## Phase 3 - Completion
---
## Phase 3 - Completion
A task is complete when:
- [ ] All planned todo items marked done
@@ -403,9 +298,12 @@ If verification fails:
### Before Delivering Final Answer:
- Cancel ALL running background tasks: \`background_cancel(all=true)\`
- This conserves resources and ensures clean workflow completion`
- This conserves resources and ensures clean workflow completion
</Behavior_Instructions>
const SISYPHUS_TASK_MANAGEMENT = `<Task_Management>
${oracleSection}
<Task_Management>
## Todo Management (CRITICAL)
**DEFAULT BEHAVIOR**: Create todos BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
@@ -460,13 +358,13 @@ I want to make sure I understand correctly.
Should I proceed with [recommendation], or would you prefer differently?
\`\`\`
</Task_Management>`
</Task_Management>
const SISYPHUS_TONE_AND_STYLE = `<Tone_and_Style>
<Tone_and_Style>
## Communication Style
### Be Concise
- Start work immediately. No acknowledgments ("I'm on it", "Let me...", "I'll start...")
- Start work immediately. No acknowledgments ("I'm on it", "Let me...", "I'll start...")
- Answer directly without preamble
- Don't summarize what you did unless asked
- Don't explain your code unless asked
@@ -502,100 +400,20 @@ If the user's approach seems problematic:
- If user is terse, be terse
- If user wants detail, provide detail
- Adapt to their communication preference
</Tone_and_Style>`
</Tone_and_Style>
const SISYPHUS_SOFT_GUIDELINES = `## Soft Guidelines
<Constraints>
${hardBlocks}
${antiPatterns}
## Soft Guidelines
- Prefer existing libraries over new dependencies
- Prefer small, focused changes over large refactors
- When uncertain about scope, ask
</Constraints>
`
function buildDynamicSisyphusPrompt(
availableAgents: AvailableAgent[],
availableTools: AvailableTool[] = [],
availableSkills: AvailableSkill[] = [],
availableCategories: AvailableCategory[] = []
): string {
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
const exploreSection = buildExploreSection(availableAgents)
const librarianSection = buildLibrarianSection(availableAgents)
const categorySkillsGuide = buildCategorySkillsDelegationGuide(availableCategories, availableSkills)
const delegationTable = buildDelegationTable(availableAgents)
const oracleSection = buildOracleSection(availableAgents)
const hardBlocks = buildHardBlocksSection()
const antiPatterns = buildAntiPatternsSection()
const sections = [
SISYPHUS_ROLE_SECTION,
"<Behavior_Instructions>",
"",
"## Phase 0 - Intent Gate (EVERY message)",
"",
keyTriggers,
"",
SISYPHUS_PHASE0_STEP1_3,
"",
"---",
"",
SISYPHUS_PHASE1,
"",
"---",
"",
"## Phase 2A - Exploration & Research",
"",
toolSelection,
"",
exploreSection,
"",
librarianSection,
"",
SISYPHUS_PRE_DELEGATION_PLANNING,
"",
SISYPHUS_PARALLEL_EXECUTION,
"",
"---",
"",
SISYPHUS_PHASE2B_PRE_IMPLEMENTATION,
"",
categorySkillsGuide,
"",
delegationTable,
"",
SISYPHUS_DELEGATION_PROMPT_STRUCTURE,
"",
SISYPHUS_GITHUB_WORKFLOW,
"",
SISYPHUS_CODE_CHANGES,
"",
"---",
"",
SISYPHUS_PHASE2C,
"",
"---",
"",
SISYPHUS_PHASE3,
"",
"</Behavior_Instructions>",
"",
oracleSection,
"",
SISYPHUS_TASK_MANAGEMENT,
"",
SISYPHUS_TONE_AND_STYLE,
"",
"<Constraints>",
hardBlocks,
"",
antiPatterns,
"",
SISYPHUS_SOFT_GUIDELINES,
]
return sections.filter((s) => s !== "").join("\n")
}
export function createSisyphusAgent(
@@ -630,4 +448,3 @@ export function createSisyphusAgent(
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
}

View File

@@ -57,14 +57,14 @@ export function isGptModel(model: string): boolean {
}
export type BuiltinAgentName =
| "Sisyphus"
| "sisyphus"
| "oracle"
| "librarian"
| "explore"
| "multimodal-looker"
| "Metis (Plan Consultant)"
| "Momus (Plan Reviewer)"
| "Atlas"
| "metis"
| "momus"
| "atlas"
export type OverridableAgentName =
| "build"

View File

@@ -1,71 +1,87 @@
import { describe, test, expect } from "bun:test"
import { describe, test, expect, beforeEach, spyOn, afterEach } from "bun:test"
import { createBuiltinAgents } from "./utils"
import type { AgentConfig } from "@opencode-ai/sdk"
import { clearSkillCache } from "../features/opencode-skill-loader/skill-content"
import * as connectedProvidersCache from "../shared/connected-providers-cache"
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-5"
describe("createBuiltinAgents with model overrides", () => {
test("Sisyphus with default model has thinking config", () => {
test("Sisyphus with default model has thinking config", async () => {
// #given - no overrides, using systemDefaultModel
// #when
const agents = createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.Sisyphus.model).toBe("anthropic/claude-opus-4-5")
expect(agents.Sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
expect(agents.Sisyphus.reasoningEffort).toBeUndefined()
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
expect(agents.sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
})
test("Sisyphus with GPT model override has reasoningEffort, no thinking", () => {
test("Sisyphus with GPT model override has reasoningEffort, no thinking", async () => {
// #given
const overrides = {
Sisyphus: { model: "github-copilot/gpt-5.2" },
sisyphus: { model: "github-copilot/gpt-5.2" },
}
// #when
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.Sisyphus.reasoningEffort).toBe("medium")
expect(agents.Sisyphus.thinking).toBeUndefined()
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.sisyphus.reasoningEffort).toBe("medium")
expect(agents.sisyphus.thinking).toBeUndefined()
})
test("Sisyphus with systemDefaultModel GPT has reasoningEffort, no thinking", () => {
test("Sisyphus uses system default when no availableModels provided", async () => {
// #given
const systemDefaultModel = "openai/gpt-5.2"
const systemDefaultModel = "anthropic/claude-opus-4-5"
// #when
const agents = createBuiltinAgents([], {}, undefined, systemDefaultModel)
const agents = await createBuiltinAgents([], {}, undefined, systemDefaultModel)
// #then
expect(agents.Sisyphus.model).toBe("openai/gpt-5.2")
expect(agents.Sisyphus.reasoningEffort).toBe("medium")
expect(agents.Sisyphus.thinking).toBeUndefined()
// #then - falls back to system default when no availability match
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
expect(agents.sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
})
test("Oracle with default model has reasoningEffort", () => {
// #given - no overrides, using systemDefaultModel for other agents
// Oracle uses its own default model (openai/gpt-5.2) from the factory singleton
test("Oracle falls back to system default when availableModels is empty (even with connected cache)", async () => {
// #given
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
// #when
const agents = createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then - Oracle uses systemDefaultModel since model is now required
expect(agents.oracle.model).toBe("anthropic/claude-opus-4-5")
// #then
expect(agents.oracle.model).toBe(TEST_DEFAULT_MODEL)
expect(agents.oracle.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
expect(agents.oracle.reasoningEffort).toBeUndefined()
cacheSpy.mockRestore()
})
test("Oracle with GPT model override has reasoningEffort, no thinking", () => {
test("Oracle created without model field when no cache exists (first run scenario)", async () => {
// #given - no cache at all (first run)
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
// #when
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then - oracle should be created with system default model (fallback to systemDefaultModel)
expect(agents.oracle).toBeDefined()
expect(agents.oracle.model).toBe(TEST_DEFAULT_MODEL)
cacheSpy.mockRestore()
})
test("Oracle with GPT model override has reasoningEffort, no thinking", async () => {
// #given
const overrides = {
oracle: { model: "openai/gpt-5.2" },
}
// #when
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.oracle.model).toBe("openai/gpt-5.2")
@@ -74,14 +90,14 @@ describe("createBuiltinAgents with model overrides", () => {
expect(agents.oracle.thinking).toBeUndefined()
})
test("Oracle with Claude model override has thinking, no reasoningEffort", () => {
test("Oracle with Claude model override has thinking, no reasoningEffort", async () => {
// #given
const overrides = {
oracle: { model: "anthropic/claude-sonnet-4" },
}
// #when
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4")
@@ -90,18 +106,56 @@ describe("createBuiltinAgents with model overrides", () => {
expect(agents.oracle.textVerbosity).toBeUndefined()
})
test("non-model overrides are still applied after factory rebuild", () => {
test("non-model overrides are still applied after factory rebuild", async () => {
// #given
const overrides = {
sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.sisyphus.temperature).toBe(0.5)
})
})
describe("createBuiltinAgents without systemDefaultModel", () => {
test("agents NOT created when availableModels empty and no systemDefaultModel", async () => {
// #given
const overrides = {
Sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
}
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
// #when
const agents = createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
const agents = await createBuiltinAgents([], {}, undefined, undefined)
// #then
expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.Sisyphus.temperature).toBe(0.5)
expect(agents.oracle).toBeUndefined()
cacheSpy.mockRestore()
})
test("agents NOT created when no cache and no systemDefaultModel (first run without defaults)", async () => {
// #given
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
// #when
const agents = await createBuiltinAgents([], {}, undefined, undefined)
// #then
expect(agents.oracle).toBeUndefined()
cacheSpy.mockRestore()
})
test("sisyphus NOT created when availableModels empty and no systemDefaultModel", async () => {
// #given
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
// #when
const agents = await createBuiltinAgents([], {}, undefined, undefined)
// #then
expect(agents.sisyphus).toBeUndefined()
cacheSpy.mockRestore()
})
})
@@ -109,6 +163,10 @@ describe("buildAgent with category and skills", () => {
const { buildAgent } = require("./utils")
const TEST_MODEL = "anthropic/claude-opus-4-5"
beforeEach(() => {
clearSkillCache()
})
test("agent with category inherits category settings", () => {
// #given - agent factory that sets category but no model
const source = {
@@ -123,7 +181,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", () => {
@@ -308,4 +366,42 @@ describe("buildAgent with category and skills", () => {
// #then
expect(agent.prompt).toBe("Base prompt")
})
test("agent with agent-browser skill resolves when browserProvider is set", () => {
// #given
const source = {
"test-agent": () =>
({
description: "Test agent",
skills: ["agent-browser"],
prompt: "Base prompt",
}) as AgentConfig,
}
// #when - browserProvider is "agent-browser"
const agent = buildAgent(source["test-agent"], TEST_MODEL, undefined, undefined, "agent-browser")
// #then - agent-browser skill content should be in prompt
expect(agent.prompt).toContain("agent-browser")
expect(agent.prompt).toContain("Base prompt")
})
test("agent with agent-browser skill NOT resolved when browserProvider not set", () => {
// #given
const source = {
"test-agent": () =>
({
description: "Test agent",
skills: ["agent-browser"],
prompt: "Base prompt",
}) as AgentConfig,
}
// #when - no browserProvider (defaults to playwright)
const agent = buildAgent(source["test-agent"], TEST_MODEL)
// #then - agent-browser skill not found, only base prompt remains
expect(agent.prompt).toBe("Base prompt")
expect(agent.prompt).not.toContain("agent-browser open")
})
})

View File

@@ -10,24 +10,26 @@ import { createMetisAgent } from "./metis"
import { createAtlasAgent } from "./atlas"
import { createMomusAgent } from "./momus"
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
import { deepMerge } from "../shared"
import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive, readConnectedProvidersCache } from "../shared"
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
import { createBuiltinSkills } from "../features/builtin-skills"
import type { LoadedSkill, SkillScope } from "../features/opencode-skill-loader/types"
import type { BrowserAutomationProvider } from "../config/schema"
type AgentSource = AgentFactory | AgentConfig
const agentSources: Record<BuiltinAgentName, AgentSource> = {
Sisyphus: createSisyphusAgent,
sisyphus: createSisyphusAgent,
oracle: createOracleAgent,
librarian: createLibrarianAgent,
explore: createExploreAgent,
"multimodal-looker": createMultimodalLookerAgent,
"Metis (Plan Consultant)": createMetisAgent,
"Momus (Plan Reviewer)": createMomusAgent,
metis: createMetisAgent,
momus: createMomusAgent,
// Note: Atlas is handled specially in createBuiltinAgents()
// because it needs OrchestratorContext, not just a model string
Atlas: createAtlasAgent as unknown as AgentFactory,
atlas: createAtlasAgent as unknown as AgentFactory,
}
/**
@@ -49,7 +51,8 @@ export function buildAgent(
source: AgentSource,
model: string,
categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig
gitMasterConfig?: GitMasterConfig,
browserProvider?: BrowserAutomationProvider
): AgentConfig {
const base = isFactory(source) ? source(model) : source
const categoryConfigs: Record<string, CategoryConfig> = categories
@@ -73,7 +76,7 @@ export function buildAgent(
}
if (agentWithCategory.skills?.length) {
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig })
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider })
if (resolved.size > 0) {
const skillContent = Array.from(resolved.values()).join("\n\n")
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
@@ -131,17 +134,27 @@ function mergeAgentConfig(
return merged
}
export function createBuiltinAgents(
disabledAgents: BuiltinAgentName[] = [],
function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
if (scope === "user" || scope === "opencode") return "user"
if (scope === "project" || scope === "opencode-project") return "project"
return "plugin"
}
export async function createBuiltinAgents(
disabledAgents: string[] = [],
agentOverrides: AgentOverrides = {},
directory?: string,
systemDefaultModel?: string,
categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig
): Record<string, AgentConfig> {
if (!systemDefaultModel) {
throw new Error("createBuiltinAgents requires systemDefaultModel")
}
gitMasterConfig?: GitMasterConfig,
discoveredSkills: LoadedSkill[] = [],
client?: any,
browserProvider?: BrowserAutomationProvider
): Promise<Record<string, AgentConfig>> {
const connectedProviders = readConnectedProvidersCache()
const availableModels = client
? await fetchAvailableModels(client, { connectedProviders: connectedProviders ?? undefined })
: new Set<string>()
const result: Record<string, AgentConfig> = {}
const availableAgents: AvailableAgent[] = []
@@ -152,27 +165,55 @@ export function createBuiltinAgents(
const availableCategories: AvailableCategory[] = Object.entries(mergedCategories).map(([name]) => ({
name,
description: CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
}))
const builtinSkills = createBuiltinSkills()
const availableSkills: AvailableSkill[] = builtinSkills.map((skill) => ({
const builtinSkills = createBuiltinSkills({ browserProvider })
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
name: skill.name,
description: skill.description,
location: "plugin" as const,
}))
for (const [name, source] of Object.entries(agentSources)) {
const agentName = name as BuiltinAgentName
const discoveredAvailable: AvailableSkill[] = discoveredSkills
.filter(s => !builtinSkillNames.has(s.name))
.map((skill) => ({
name: skill.name,
description: skill.definition.description ?? "",
location: mapScopeToLocation(skill.scope),
}))
if (agentName === "Sisyphus") continue
if (agentName === "Atlas") continue
if (disabledAgents.includes(agentName)) continue
const availableSkills: AvailableSkill[] = [...builtinAvailable, ...discoveredAvailable]
const override = agentOverrides[agentName]
const model = override?.model ?? systemDefaultModel
for (const [name, source] of Object.entries(agentSources)) {
const agentName = name as BuiltinAgentName
let config = buildAgent(source, model, mergedCategories, gitMasterConfig)
if (agentName === "sisyphus") continue
if (agentName === "atlas") continue
if (includesCaseInsensitive(disabledAgents, agentName)) continue
const override = findCaseInsensitive(agentOverrides, agentName)
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
const resolution = resolveModelWithFallback({
userModel: override?.model,
fallbackChain: requirement?.fallbackChain,
availableModels,
systemDefaultModel,
})
if (!resolution) continue
const { model, variant: resolvedVariant } = resolution
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider)
// Apply variant from override or resolved fallback chain
if (override?.variant) {
config = { ...config, variant: override.variant }
} else if (resolvedVariant) {
config = { ...config, variant: resolvedVariant }
}
if (agentName === "librarian" && directory && config.prompt) {
const envContext = createEnvContext()
@@ -195,46 +236,81 @@ export function createBuiltinAgents(
}
}
if (!disabledAgents.includes("Sisyphus")) {
const sisyphusOverride = agentOverrides["Sisyphus"]
const sisyphusModel = sisyphusOverride?.model ?? systemDefaultModel
if (!disabledAgents.includes("sisyphus")) {
const sisyphusOverride = agentOverrides["sisyphus"]
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
const sisyphusResolution = resolveModelWithFallback({
userModel: sisyphusOverride?.model,
fallbackChain: sisyphusRequirement?.fallbackChain,
availableModels,
systemDefaultModel,
})
let sisyphusConfig = createSisyphusAgent(
sisyphusModel,
availableAgents,
undefined,
availableSkills,
availableCategories
)
if (sisyphusResolution) {
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
if (directory && sisyphusConfig.prompt) {
const envContext = createEnvContext()
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
let sisyphusConfig = createSisyphusAgent(
sisyphusModel,
availableAgents,
undefined,
availableSkills,
availableCategories
)
if (sisyphusOverride?.variant) {
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusOverride.variant }
} else if (sisyphusResolvedVariant) {
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
}
if (directory && sisyphusConfig.prompt) {
const envContext = createEnvContext()
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
}
if (sisyphusOverride) {
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
}
result["sisyphus"] = sisyphusConfig
}
}
if (sisyphusOverride) {
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
if (!disabledAgents.includes("atlas")) {
const orchestratorOverride = agentOverrides["atlas"]
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
const atlasResolution = resolveModelWithFallback({
userModel: orchestratorOverride?.model,
fallbackChain: atlasRequirement?.fallbackChain,
availableModels,
systemDefaultModel,
})
if (atlasResolution) {
const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution
let orchestratorConfig = createAtlasAgent({
model: atlasModel,
availableAgents,
availableSkills,
userCategories: categories,
})
if (orchestratorOverride?.variant) {
orchestratorConfig = { ...orchestratorConfig, variant: orchestratorOverride.variant }
} else if (atlasResolvedVariant) {
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
}
if (orchestratorOverride) {
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
}
result["atlas"] = orchestratorConfig
}
}
result["Sisyphus"] = sisyphusConfig
}
if (!disabledAgents.includes("Atlas")) {
const orchestratorOverride = agentOverrides["Atlas"]
const orchestratorModel = orchestratorOverride?.model ?? systemDefaultModel
let orchestratorConfig = createAtlasAgent({
model: orchestratorModel,
availableAgents,
availableSkills,
userCategories: categories,
})
if (orchestratorOverride) {
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
}
result["Atlas"] = orchestratorConfig
}
return result
}
return result
}

View File

@@ -2,90 +2,73 @@
## OVERVIEW
CLI entry point: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics, session runner. Uses Commander.js + @clack/prompts TUI.
CLI entry: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics. Commander.js + @clack/prompts.
## STRUCTURE
```
cli/
├── index.ts # Commander.js entry, 5 subcommands
├── install.ts # Interactive TUI installer (462 lines)
├── config-manager.ts # JSONC parsing, multi-level merge (730 lines)
├── types.ts # InstallArgs, InstallConfig, DetectedConfig
├── index.ts # Commander.js entry (4 commands)
├── install.ts # Interactive TUI (520 lines)
├── config-manager.ts # JSONC parsing (664 lines)
├── types.ts # InstallArgs, InstallConfig
├── model-fallback.ts # Model fallback configuration
├── doctor/
│ ├── index.ts # Doctor command entry
│ ├── index.ts # Doctor entry
│ ├── runner.ts # Check orchestration
│ ├── formatter.ts # Colored output, symbols
│ ├── constants.ts # Check IDs, categories, symbols
│ ├── types.ts # CheckResult, CheckDefinition
│ └── checks/ # 14 checks across 6 categories
│ ├── formatter.ts # Colored output
│ ├── constants.ts # Check IDs, symbols
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
│ └── checks/ # 14 checks, 21 files
│ ├── version.ts # OpenCode + plugin version
│ ├── config.ts # JSONC validity, Zod validation
│ ├── config.ts # JSONC validity, Zod
│ ├── auth.ts # Anthropic, OpenAI, Google
│ ├── dependencies.ts # AST-Grep, Comment Checker
│ ├── lsp.ts # LSP server connectivity
│ ├── mcp.ts # MCP server validation
── gh.ts # GitHub CLI availability
│ ├── lsp.ts # LSP connectivity
│ ├── mcp.ts # MCP validation
── model-resolution.ts # Model resolution check
│ └── gh.ts # GitHub CLI
├── run/
── index.ts # Run command entry
│ └── runner.ts # Session launcher
── index.ts # Session launcher
└── get-local-version/
── index.ts # Version detection
└── formatter.ts # Version output
── index.ts # Version detection
```
## CLI COMMANDS
## COMMANDS
| Command | Purpose |
|---------|---------|
| `install` | Interactive setup, subscription detection |
| `doctor` | 14 health checks, `--verbose`, `--json`, `--category` |
| `run` | Launch OpenCode session with completion enforcement |
| `get-local-version` | Version detection, update checking |
| `install` | Interactive setup with provider selection |
| `doctor` | 14 health checks for diagnostics |
| `run` | Launch session with todo enforcement |
| `get-local-version` | Version detection and update check |
## DOCTOR CHECK CATEGORIES
## DOCTOR CATEGORIES (14 Checks)
| Category | Checks |
|----------|--------|
| installation | opencode, plugin registration |
| configuration | config validity, Zod validation |
| installation | opencode, plugin |
| configuration | config validity, Zod, model-resolution |
| authentication | anthropic, openai, google |
| dependencies | ast-grep CLI/NAPI, comment-checker |
| tools | LSP, MCP connectivity |
| dependencies | ast-grep, comment-checker, gh-cli |
| tools | LSP, MCP |
| updates | version comparison |
## HOW TO ADD CHECK
1. Create `src/cli/doctor/checks/my-check.ts`:
```typescript
export function getMyCheckDefinition(): CheckDefinition {
return {
id: "my-check",
name: "My Check",
category: "configuration",
check: async () => ({ status: "pass", message: "OK" })
}
}
```
2. Export from `checks/index.ts`
3. Add to `getAllCheckDefinitions()`
1. Create `src/cli/doctor/checks/my-check.ts`
2. Export `getXXXCheckDefinition()` factory returning `CheckDefinition`
3. Add to `getAllCheckDefinitions()` in `checks/index.ts`
## TUI FRAMEWORK
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`, `note()`
- **picocolors**: Colored terminal output
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), (skip)
## CONFIG-MANAGER
- **JSONC**: Comments (`// ...`), block comments, trailing commas
- **Multi-source**: User (`~/.config/opencode/`) + Project (`.opencode/`)
- **Env override**: `OPENCODE_CONFIG_DIR` for profile isolation
- **Validation**: Zod schema with error aggregation
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`
- **picocolors**: Terminal colors for status and headers
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), (info)
## ANTI-PATTERNS
- **Blocking in non-TTY**: Check `process.stdout.isTTY`
- **Direct JSON.parse**: Use `parseJsonc()` for config
- **Silent failures**: Always return warn/fail in doctor
- **Hardcoded paths**: Use `ConfigManager`
- **Blocking in non-TTY**: Always check `process.stdout.isTTY`
- **Direct JSON.parse**: Use `parseJsonc()` from shared utils
- **Silent failures**: Return `warn` or `fail` in doctor instead of throwing
- **Hardcoded paths**: Use `getOpenCodeConfigPaths()` from `config-manager.ts`

File diff suppressed because it is too large Load Diff

View File

@@ -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", () => {
@@ -219,7 +258,7 @@ describe("generateOmoConfig - model fallback system", () => {
// #then should use native anthropic sonnet (cost-efficient for standard plan)
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
expect(result.agents).toBeDefined()
expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("anthropic/claude-sonnet-4-5")
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-sonnet-4-5")
})
test("generates native opus models when Claude max20 subscription", () => {
@@ -238,7 +277,7 @@ describe("generateOmoConfig - model fallback system", () => {
const result = generateOmoConfig(config)
// #then should use native anthropic opus (max power for max20 plan)
expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("anthropic/claude-opus-4-5")
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
})
test("uses github-copilot sonnet fallback when only copilot available", () => {
@@ -256,8 +295,8 @@ describe("generateOmoConfig - model fallback system", () => {
// #when generating config
const result = generateOmoConfig(config)
// #then should use github-copilot sonnet models
expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("github-copilot/claude-sonnet-4.5")
// #then should use github-copilot sonnet models (copilot fallback)
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-sonnet-4.5")
})
test("uses ultimate fallback when no providers configured", () => {
@@ -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", () => {
@@ -298,7 +337,7 @@ describe("generateOmoConfig - model fallback system", () => {
// #then librarian should use zai-coding-plan/glm-4.7
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
// #then other agents should use native opus (max20 plan)
expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("anthropic/claude-opus-4-5")
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
})
test("uses native OpenAI models when only ChatGPT available", () => {
@@ -317,9 +356,9 @@ describe("generateOmoConfig - model fallback system", () => {
const result = generateOmoConfig(config)
// #then Sisyphus should use native OpenAI (fallback within native tier)
expect((result.agents as Record<string, { model: string }>).Sisyphus.model).toBe("openai/gpt-5.2")
// #then Oracle should use native OpenAI (primary for ultrabrain)
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2-codex")
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("openai/gpt-5.2")
// #then Oracle should use native OpenAI (first fallback entry)
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2")
// #then multimodal-looker should use native OpenAI (fallback within native tier)
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.2")
})
@@ -343,7 +382,7 @@ describe("generateOmoConfig - model fallback system", () => {
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
})
test("uses grok-code for explore when not max20", () => {
test("uses haiku for explore regardless of max20 flag", () => {
// #given user has Claude but not max20
const config: InstallConfig = {
hasClaude: true,
@@ -358,7 +397,7 @@ describe("generateOmoConfig - model fallback system", () => {
// #when generating config
const result = generateOmoConfig(config)
// #then explore should use grok-code (preserve Claude quota)
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("opencode/grok-code")
// #then explore should use haiku (isMax20 doesn't affect explore anymore)
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
})
})

View File

@@ -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 } },
},
},
},
},

View File

@@ -1,11 +1,10 @@
import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import type { CheckResult, CheckDefinition, AuthProviderInfo, AuthProviderId } from "../types"
import { CHECK_IDS, CHECK_NAMES } from "../constants"
import { parseJsonc } from "../../../shared"
import { parseJsonc, getOpenCodeConfigDir } from "../../../shared"
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
const OPENCODE_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")

View File

@@ -1,12 +1,11 @@
import { existsSync, readFileSync } from "node:fs"
import { homedir } from "node:os"
import { join } from "node:path"
import type { CheckResult, CheckDefinition, ConfigInfo } from "../types"
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
import { parseJsonc, detectConfigFile } from "../../../shared"
import { parseJsonc, detectConfigFile, getOpenCodeConfigDir } from "../../../shared"
import { OhMyOpenCodeConfigSchema } from "../../../config"
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, `${PACKAGE_NAME}`)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)

View File

@@ -16,10 +16,10 @@ describe("dependencies check", () => {
})
describe("checkAstGrepNapi", () => {
it("returns dependency info", () => {
it("returns dependency info", async () => {
// #given
// #when checking ast-grep napi
const info = deps.checkAstGrepNapi()
const info = await deps.checkAstGrepNapi()
// #then should return valid info
expect(info.name).toBe("AST-Grep NAPI")
@@ -95,7 +95,7 @@ describe("dependencies check", () => {
it("returns pass when installed", async () => {
// #given napi installed
checkSpy = spyOn(deps, "checkAstGrepNapi").mockReturnValue({
checkSpy = spyOn(deps, "checkAstGrepNapi").mockResolvedValue({
name: "AST-Grep NAPI",
required: false,
installed: true,

View File

@@ -56,9 +56,10 @@ export async function checkAstGrepCli(): Promise<DependencyInfo> {
}
}
export function checkAstGrepNapi(): DependencyInfo {
export async function checkAstGrepNapi(): Promise<DependencyInfo> {
// Try dynamic import first (works in bunx temporary environments)
try {
require.resolve("@ast-grep/napi")
await import("@ast-grep/napi")
return {
name: "AST-Grep NAPI",
required: false,
@@ -67,6 +68,28 @@ export function checkAstGrepNapi(): DependencyInfo {
path: null,
}
} catch {
// Fallback: check common installation paths
const { existsSync } = await import("fs")
const { join } = await import("path")
const { homedir } = await import("os")
const pathsToCheck = [
join(homedir(), ".config", "opencode", "node_modules", "@ast-grep", "napi"),
join(process.cwd(), "node_modules", "@ast-grep", "napi"),
]
for (const napiPath of pathsToCheck) {
if (existsSync(napiPath)) {
return {
name: "AST-Grep NAPI",
required: false,
installed: true,
version: null,
path: napiPath,
}
}
}
return {
name: "AST-Grep NAPI",
required: false,
@@ -127,7 +150,7 @@ export async function checkDependencyAstGrepCli(): Promise<CheckResult> {
}
export async function checkDependencyAstGrepNapi(): Promise<CheckResult> {
const info = checkAstGrepNapi()
const info = await checkAstGrepNapi()
return dependencyToCheckResult(info, CHECK_NAMES[CHECK_IDS.DEP_AST_GREP_NAPI])
}

View File

@@ -2,6 +2,7 @@ import type { CheckDefinition } from "../types"
import { getOpenCodeCheckDefinition } from "./opencode"
import { getPluginCheckDefinition } from "./plugin"
import { getConfigCheckDefinition } from "./config"
import { getModelResolutionCheckDefinition } from "./model-resolution"
import { getAuthCheckDefinitions } from "./auth"
import { getDependencyCheckDefinitions } from "./dependencies"
import { getGhCliCheckDefinition } from "./gh"
@@ -12,6 +13,7 @@ import { getVersionCheckDefinition } from "./version"
export * from "./opencode"
export * from "./plugin"
export * from "./config"
export * from "./model-resolution"
export * from "./auth"
export * from "./dependencies"
export * from "./gh"
@@ -24,6 +26,7 @@ export function getAllCheckDefinitions(): CheckDefinition[] {
getOpenCodeCheckDefinition(),
getPluginCheckDefinition(),
getConfigCheckDefinition(),
getModelResolutionCheckDefinition(),
...getAuthCheckDefinitions(),
...getDependencyCheckDefinitions(),
getGhCliCheckDefinition(),

View File

@@ -0,0 +1,141 @@
import { describe, it, expect, beforeEach, afterEach, spyOn, mock } from "bun:test"
describe("model-resolution check", () => {
describe("getModelResolutionInfo", () => {
// #given: Model requirements are defined in model-requirements.ts
// #when: Getting model resolution info
// #then: Returns info for all agents and categories with their provider chains
it("returns agent requirements with provider chains", async () => {
const { getModelResolutionInfo } = await import("./model-resolution")
const info = getModelResolutionInfo()
// #then: Should have agent entries
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
expect(sisyphus).toBeDefined()
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-5")
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("anthropic")
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("github-copilot")
})
it("returns category requirements with provider chains", async () => {
const { getModelResolutionInfo } = await import("./model-resolution")
const info = getModelResolutionInfo()
// #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")
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("google")
})
})
describe("getModelResolutionInfoWithOverrides", () => {
// #given: User has overrides in oh-my-opencode.json
// #when: Getting resolution info with config
// #then: Shows user override in Step 1 position
it("shows user override for agent when configured", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
// #given: User has override for oracle agent
const mockConfig = {
agents: {
oracle: { model: "anthropic/claude-opus-4-5" },
},
}
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: Oracle should show the override
const oracle = info.agents.find((a) => a.name === "oracle")
expect(oracle).toBeDefined()
expect(oracle!.userOverride).toBe("anthropic/claude-opus-4-5")
expect(oracle!.effectiveResolution).toBe("User override: anthropic/claude-opus-4-5")
})
it("shows user override for category when configured", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
// #given: User has override for visual-engineering category
const mockConfig = {
categories: {
"visual-engineering": { model: "openai/gpt-5.2" },
},
}
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: visual-engineering should show the override
const visual = info.categories.find((c) => c.name === "visual-engineering")
expect(visual).toBeDefined()
expect(visual!.userOverride).toBe("openai/gpt-5.2")
expect(visual!.effectiveResolution).toBe("User override: openai/gpt-5.2")
})
it("shows provider fallback when no override exists", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
// #given: No overrides configured
const mockConfig = {}
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: Should show provider fallback chain
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
expect(sisyphus).toBeDefined()
expect(sisyphus!.userOverride).toBeUndefined()
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
expect(sisyphus!.effectiveResolution).toContain("anthropic")
})
})
describe("checkModelResolution", () => {
// #given: Doctor check is executed
// #when: Running the model resolution check
// #then: Returns pass with details showing resolution flow
it("returns pass or warn status with agent and category counts", async () => {
const { checkModelResolution } = await import("./model-resolution")
const result = await checkModelResolution()
// #then: Should pass (with cache) or warn (no cache) and show counts
// In CI without model cache, status is "warn"; locally with cache, status is "pass"
expect(["pass", "warn"]).toContain(result.status)
expect(result.message).toMatch(/\d+ agents?, \d+ categories?/)
})
it("includes resolution details in verbose mode details array", async () => {
const { checkModelResolution } = await import("./model-resolution")
const result = await checkModelResolution()
// #then: Details should contain agent/category resolution info
expect(result.details).toBeDefined()
expect(result.details!.length).toBeGreaterThan(0)
// Should have Available Models and Configured Models headers
expect(result.details!.some((d) => d.includes("Available Models"))).toBe(true)
expect(result.details!.some((d) => d.includes("Configured Models"))).toBe(true)
expect(result.details!.some((d) => d.includes("Agents:"))).toBe(true)
expect(result.details!.some((d) => d.includes("Categories:"))).toBe(true)
// Should have legend
expect(result.details!.some((d) => d.includes("user override"))).toBe(true)
})
})
describe("getModelResolutionCheckDefinition", () => {
it("returns valid check definition", async () => {
const { getModelResolutionCheckDefinition } = await import("./model-resolution")
const def = getModelResolutionCheckDefinition()
expect(def.id).toBe("model-resolution")
expect(def.name).toBe("Model Resolution")
expect(def.category).toBe("configuration")
expect(typeof def.check).toBe("function")
})
})
})

View File

@@ -0,0 +1,264 @@
import { readFileSync, existsSync } from "node:fs"
import type { CheckResult, CheckDefinition } from "../types"
import { CHECK_IDS, CHECK_NAMES } from "../constants"
import { parseJsonc, detectConfigFile } from "../../../shared"
import {
AGENT_MODEL_REQUIREMENTS,
CATEGORY_MODEL_REQUIREMENTS,
type ModelRequirement,
} from "../../../shared/model-requirements"
import { homedir } from "node:os"
import { join } from "node:path"
function getOpenCodeCacheDir(): string {
const xdgCache = process.env.XDG_CACHE_HOME
if (xdgCache) return join(xdgCache, "opencode")
return join(homedir(), ".cache", "opencode")
}
function loadAvailableModels(): { providers: string[]; modelCount: number; cacheExists: boolean } {
const cacheFile = join(getOpenCodeCacheDir(), "models.json")
if (!existsSync(cacheFile)) {
return { providers: [], modelCount: 0, cacheExists: false }
}
try {
const content = readFileSync(cacheFile, "utf-8")
const data = JSON.parse(content) as Record<string, { models?: Record<string, unknown> }>
const providers = Object.keys(data)
let modelCount = 0
for (const providerId of providers) {
const models = data[providerId]?.models
if (models && typeof models === "object") {
modelCount += Object.keys(models).length
}
}
return { providers, modelCount, cacheExists: true }
} catch {
return { providers: [], modelCount: 0, cacheExists: false }
}
}
const PACKAGE_NAME = "oh-my-opencode"
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, PACKAGE_NAME)
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
export interface AgentResolutionInfo {
name: string
requirement: ModelRequirement
userOverride?: string
effectiveModel: string
effectiveResolution: string
}
export interface CategoryResolutionInfo {
name: string
requirement: ModelRequirement
userOverride?: string
effectiveModel: string
effectiveResolution: string
}
export interface ModelResolutionInfo {
agents: AgentResolutionInfo[]
categories: CategoryResolutionInfo[]
}
interface OmoConfig {
agents?: Record<string, { model?: string }>
categories?: Record<string, { model?: string }>
}
function loadConfig(): OmoConfig | null {
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
if (projectDetected.format !== "none") {
try {
const content = readFileSync(projectDetected.path, "utf-8")
return parseJsonc<OmoConfig>(content)
} catch {
return null
}
}
const userDetected = detectConfigFile(USER_CONFIG_BASE)
if (userDetected.format !== "none") {
try {
const content = readFileSync(userDetected.path, "utf-8")
return parseJsonc<OmoConfig>(content)
} catch {
return null
}
}
return null
}
function formatProviderChain(providers: string[]): string {
return providers.join(" → ")
}
function getEffectiveModel(requirement: ModelRequirement, userOverride?: string): string {
if (userOverride) {
return userOverride
}
const firstEntry = requirement.fallbackChain[0]
if (!firstEntry) {
return "unknown"
}
return `${firstEntry.providers[0]}/${firstEntry.model}`
}
function buildEffectiveResolution(
requirement: ModelRequirement,
userOverride?: string,
): string {
if (userOverride) {
return `User override: ${userOverride}`
}
const firstEntry = requirement.fallbackChain[0]
if (!firstEntry) {
return "No fallback chain defined"
}
return `Provider fallback: ${formatProviderChain(firstEntry.providers)}${firstEntry.model}`
}
export function getModelResolutionInfo(): ModelResolutionInfo {
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
([name, requirement]) => ({
name,
requirement,
effectiveModel: getEffectiveModel(requirement),
effectiveResolution: buildEffectiveResolution(requirement),
}),
)
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
([name, requirement]) => ({
name,
requirement,
effectiveModel: getEffectiveModel(requirement),
effectiveResolution: buildEffectiveResolution(requirement),
}),
)
return { agents, categories }
}
export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelResolutionInfo {
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
([name, requirement]) => {
const userOverride = config.agents?.[name]?.model
return {
name,
requirement,
userOverride,
effectiveModel: getEffectiveModel(requirement, userOverride),
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
}
},
)
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
([name, requirement]) => {
const userOverride = config.categories?.[name]?.model
return {
name,
requirement,
userOverride,
effectiveModel: getEffectiveModel(requirement, userOverride),
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
}
},
)
return { agents, categories }
}
function formatModelWithVariant(model: string, variant?: string): string {
return variant ? `${model} (${variant})` : model
}
function getEffectiveVariant(requirement: ModelRequirement): string | undefined {
const firstEntry = requirement.fallbackChain[0]
return firstEntry?.variant ?? requirement.variant
}
interface AvailableModelsInfo {
providers: string[]
modelCount: number
cacheExists: boolean
}
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo): string[] {
const details: string[] = []
details.push("═══ Available Models (from cache) ═══")
details.push("")
if (available.cacheExists) {
details.push(` Providers in cache: ${available.providers.length}`)
details.push(` Sample: ${available.providers.slice(0, 6).join(", ")}${available.providers.length > 6 ? "..." : ""}`)
details.push(` Total models: ${available.modelCount}`)
details.push(` Cache: ~/.cache/opencode/models.json`)
details.push(` Runtime: only connected providers used`)
details.push(` Refresh: opencode models --refresh`)
} else {
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
}
details.push("")
details.push("═══ Configured Models ═══")
details.push("")
details.push("Agents:")
for (const agent of info.agents) {
const marker = agent.userOverride ? "●" : "○"
const display = formatModelWithVariant(agent.effectiveModel, getEffectiveVariant(agent.requirement))
details.push(` ${marker} ${agent.name}: ${display}`)
}
details.push("")
details.push("Categories:")
for (const category of info.categories) {
const marker = category.userOverride ? "●" : "○"
const display = formatModelWithVariant(category.effectiveModel, getEffectiveVariant(category.requirement))
details.push(` ${marker} ${category.name}: ${display}`)
}
details.push("")
details.push("● = user override, ○ = provider fallback")
return details
}
export async function checkModelResolution(): Promise<CheckResult> {
const config = loadConfig() ?? {}
const info = getModelResolutionInfoWithOverrides(config)
const available = loadAvailableModels()
const agentCount = info.agents.length
const categoryCount = info.categories.length
const agentOverrides = info.agents.filter((a) => a.userOverride).length
const categoryOverrides = info.categories.filter((c) => c.userOverride).length
const totalOverrides = agentOverrides + categoryOverrides
const overrideNote = totalOverrides > 0 ? ` (${totalOverrides} override${totalOverrides > 1 ? "s" : ""})` : ""
const cacheNote = available.cacheExists ? `, ${available.modelCount} available` : ", cache not found"
return {
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
status: available.cacheExists ? "pass" : "warn",
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
details: buildDetailsArray(info, available),
}
}
export function getModelResolutionCheckDefinition(): CheckDefinition {
return {
id: CHECK_IDS.MODEL_RESOLUTION,
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
category: "configuration",
check: checkModelResolution,
critical: false,
}
}

View File

@@ -22,6 +22,9 @@ function findPluginEntry(plugins: string[]): { entry: string; isPinned: boolean;
const version = isPinned ? plugin.split("@")[1] : null
return { entry: plugin, isPinned, version }
}
if (plugin.startsWith("file://") && plugin.includes(PACKAGE_NAME)) {
return { entry: plugin, isPinned: false, version: "local-dev" }
}
}
return null
}

View File

@@ -21,6 +21,7 @@ export const CHECK_IDS = {
OPENCODE_INSTALLATION: "opencode-installation",
PLUGIN_REGISTRATION: "plugin-registration",
CONFIG_VALIDATION: "config-validation",
MODEL_RESOLUTION: "model-resolution",
AUTH_ANTHROPIC: "auth-anthropic",
AUTH_OPENAI: "auth-openai",
AUTH_GOOGLE: "auth-google",
@@ -38,6 +39,7 @@ export const CHECK_NAMES: Record<string, string> = {
[CHECK_IDS.OPENCODE_INSTALLATION]: "OpenCode Installation",
[CHECK_IDS.PLUGIN_REGISTRATION]: "Plugin Registration",
[CHECK_IDS.CONFIG_VALIDATION]: "Configuration Validity",
[CHECK_IDS.MODEL_RESOLUTION]: "Model Resolution",
[CHECK_IDS.AUTH_ANTHROPIC]: "Anthropic (Claude) Auth",
[CHECK_IDS.AUTH_OPENAI]: "OpenAI (ChatGPT) Auth",
[CHECK_IDS.AUTH_GOOGLE]: "Google (Gemini) Auth",

View File

@@ -2,13 +2,13 @@ import color from "picocolors"
import type { VersionInfo } from "./types"
const SYMBOLS = {
check: color.green(""),
cross: color.red(""),
arrow: color.cyan(""),
info: color.blue(""),
warn: color.yellow(""),
pin: color.magenta("📌"),
dev: color.cyan("🔧"),
check: color.green("[OK]"),
cross: color.red("[X]"),
arrow: color.cyan("->"),
info: color.blue("[i]"),
warn: color.yellow("[!]"),
pin: color.magenta("[PINNED]"),
dev: color.cyan("[DEV]"),
}
export function formatVersionOutput(info: VersionInfo): string {

17
src/cli/index.test.ts Normal file
View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from "bun:test"
import packageJson from "../../package.json" with { type: "json" }
describe("CLI version", () => {
it("reads version from package.json as valid semver", () => {
//#given
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/
//#when
const version = packageJson.version
//#then
expect(version).toMatch(semverRegex)
expect(typeof version).toBe("string")
expect(version.length).toBeGreaterThan(0)
})
})

151
src/cli/install.test.ts Normal file
View File

@@ -0,0 +1,151 @@
import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test"
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { install } from "./install"
import * as configManager from "./config-manager"
import type { InstallArgs } from "./types"
// Mock console methods to capture output
const mockConsoleLog = mock(() => {})
const mockConsoleError = mock(() => {})
describe("install CLI - binary check behavior", () => {
let tempDir: string
let originalEnv: string | undefined
let isOpenCodeInstalledSpy: ReturnType<typeof spyOn>
let getOpenCodeVersionSpy: ReturnType<typeof spyOn>
beforeEach(() => {
// #given temporary config directory
tempDir = join(tmpdir(), `omo-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
mkdirSync(tempDir, { recursive: true })
originalEnv = process.env.OPENCODE_CONFIG_DIR
process.env.OPENCODE_CONFIG_DIR = tempDir
// Reset config context
configManager.resetConfigContext()
configManager.initConfigContext("opencode", null)
// Capture console output
console.log = mockConsoleLog
mockConsoleLog.mockClear()
})
afterEach(() => {
if (originalEnv !== undefined) {
process.env.OPENCODE_CONFIG_DIR = originalEnv
} else {
delete process.env.OPENCODE_CONFIG_DIR
}
if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true })
}
isOpenCodeInstalledSpy?.mockRestore()
getOpenCodeVersionSpy?.mockRestore()
})
test("non-TUI mode: should show warning but continue when OpenCode binary not found", async () => {
// #given OpenCode binary is NOT installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
const args: InstallArgs = {
tui: false,
claude: "yes",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
}
// #when running install
const exitCode = await install(args)
// #then should return success (0), not failure (1)
expect(exitCode).toBe(0)
// #then should have printed a warning (not error)
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
expect(allCalls).toContain("[!]") // warning symbol
expect(allCalls).toContain("OpenCode")
})
test("non-TUI mode: should create opencode.json with plugin even when binary not found", async () => {
// #given OpenCode binary is NOT installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
// #given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }),
} as Response)
) as unknown as typeof fetch
const args: InstallArgs = {
tui: false,
claude: "yes",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
}
// #when running install
const exitCode = await install(args)
// #then should create opencode.json
const configPath = join(tempDir, "opencode.json")
expect(existsSync(configPath)).toBe(true)
// #then opencode.json should have plugin entry
const config = JSON.parse(readFileSync(configPath, "utf-8"))
expect(config.plugin).toBeDefined()
expect(config.plugin.some((p: string) => p.includes("oh-my-opencode"))).toBe(true)
// #then exit code should be 0 (success)
expect(exitCode).toBe(0)
})
test("non-TUI mode: should still succeed and complete all steps when binary exists", async () => {
// #given OpenCode binary IS installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.0.200")
// #given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }),
} as Response)
) as unknown as typeof fetch
const args: InstallArgs = {
tui: false,
claude: "yes",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
}
// #when running install
const exitCode = await install(args)
// #then should return success
expect(exitCode).toBe(0)
// #then should have printed success (OK symbol)
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
expect(allCalls).toContain("[OK]")
expect(allCalls).toContain("OpenCode 1.0.200")
})
})

View File

@@ -16,13 +16,13 @@ import packageJson from "../../package.json" with { type: "json" }
const VERSION = packageJson.version
const SYMBOLS = {
check: color.green(""),
cross: color.red(""),
arrow: color.cyan(""),
bullet: color.dim(""),
info: color.blue(""),
warn: color.yellow(""),
star: color.yellow(""),
check: color.green("[OK]"),
cross: color.red("[X]"),
arrow: color.cyan("->"),
bullet: color.dim("*"),
info: color.blue("[i]"),
warn: color.yellow("[!]"),
star: color.yellow("*"),
}
function formatProvider(name: string, enabled: boolean, detail?: string): string {
@@ -44,7 +44,7 @@ function formatConfigSummary(config: InstallConfig): string {
lines.push(formatProvider("Gemini", config.hasGemini))
lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback"))
lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models"))
lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian: glm-4.7"))
lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian/Multimodal"))
lines.push("")
lines.push(color.dim("─".repeat(40)))
@@ -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" },
],
@@ -250,7 +250,7 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
message: "Do you have a Z.ai Coding Plan subscription?",
options: [
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
{ value: "yes" as const, label: "Yes", hint: "zai-coding-plan/glm-4.7 for Librarian" },
{ value: "yes" as const, label: "Yes", hint: "Fallback for Librarian and Multimodal Looker" },
],
initialValue: initial.zaiCodingPlan,
})
@@ -295,14 +295,13 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
printStep(step++, totalSteps, "Checking OpenCode installation...")
const installed = await isOpenCodeInstalled()
if (!installed) {
printError("OpenCode is not installed on this system.")
printInfo("Visit https://opencode.ai/docs for installation instructions")
return 1
}
const version = await getOpenCodeVersion()
printSuccess(`OpenCode ${version ?? ""} detected`)
if (!installed) {
printWarning("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
printInfo("Visit https://opencode.ai/docs for installation instructions")
} else {
printSuccess(`OpenCode ${version ?? ""} detected`)
}
if (isUpdate) {
const initial = detectedToInitialValues(detected)
@@ -351,7 +350,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
if (!config.hasClaude) {
console.log()
console.log(color.bgRed(color.white(color.bold(" ⚠️ CRITICAL WARNING "))))
console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING "))))
console.log()
console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5.")))
console.log(color.red(" Without Claude, you may experience significantly degraded performance:"))
@@ -364,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!"))}`)
@@ -375,7 +374,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
`${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` +
`All features work like magic—parallel agents, background tasks,\n` +
`deep exploration, and relentless execution until completion.`,
"🪄 The Magic Word"
"The Magic Word"
)
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
@@ -390,7 +389,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
"🔐 Authenticate Your Providers"
"Authenticate Your Providers"
)
}
@@ -416,16 +415,14 @@ export async function install(args: InstallArgs): Promise<number> {
s.start("Checking OpenCode installation")
const installed = await isOpenCodeInstalled()
if (!installed) {
s.stop("OpenCode is not installed")
p.log.error("OpenCode is not installed on this system.")
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
p.outro(color.red("Please install OpenCode first."))
return 1
}
const version = await getOpenCodeVersion()
s.stop(`OpenCode ${version ?? "installed"} ${color.green("✓")}`)
if (!installed) {
s.stop(`OpenCode binary not found ${color.yellow("[!]")}`)
p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
} else {
s.stop(`OpenCode ${version ?? "installed"} ${color.green("[OK]")}`)
}
const config = await runTuiMode(detected)
if (!config) return 1
@@ -470,7 +467,7 @@ export async function install(args: InstallArgs): Promise<number> {
if (!config.hasClaude) {
console.log()
console.log(color.bgRed(color.white(color.bold(" ⚠️ CRITICAL WARNING "))))
console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING "))))
console.log()
console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5.")))
console.log(color.red(" Without Claude, you may experience significantly degraded performance:"))
@@ -483,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")
@@ -495,7 +492,7 @@ export async function install(args: InstallArgs): Promise<number> {
`Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` +
`All features work like magic—parallel agents, background tasks,\n` +
`deep exploration, and relentless execution until completion.`,
"🪄 The Magic Word"
"The Magic Word"
)
p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`)
@@ -510,7 +507,7 @@ export async function install(args: InstallArgs): Promise<number> {
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
console.log()
console.log(color.bold("🔐 Authenticate Your Providers"))
console.log(color.bold("Authenticate Your Providers"))
console.log()
console.log(` Run ${color.cyan("opencode auth login")} and select:`)
for (const provider of providers) {

View File

@@ -0,0 +1,434 @@
import { describe, expect, test } from "bun:test"
import { generateModelConfig } from "./model-fallback"
import type { InstallConfig } from "./types"
function createConfig(overrides: Partial<InstallConfig> = {}): InstallConfig {
return {
hasClaude: false,
isMax20: false,
hasOpenAI: false,
hasGemini: false,
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
...overrides,
}
}
describe("generateModelConfig", () => {
describe("no providers available", () => {
test("returns ULTIMATE_FALLBACK for all agents and categories when no providers", () => {
// #given no providers are available
const config = createConfig()
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use ULTIMATE_FALLBACK for everything
expect(result).toMatchSnapshot()
})
})
describe("single native provider", () => {
test("uses Claude models when only Claude is available", () => {
// #given only Claude is available
const config = createConfig({ hasClaude: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use Claude models per NATIVE_FALLBACK_CHAINS
expect(result).toMatchSnapshot()
})
test("uses Claude models with isMax20 flag", () => {
// #given Claude is available with Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models for Sisyphus
expect(result).toMatchSnapshot()
})
test("uses OpenAI models when only OpenAI is available", () => {
// #given only OpenAI is available
const config = createConfig({ hasOpenAI: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use OpenAI models
expect(result).toMatchSnapshot()
})
test("uses OpenAI models with isMax20 flag", () => {
// #given OpenAI is available with Max 20 plan
const config = createConfig({ hasOpenAI: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
expect(result).toMatchSnapshot()
})
test("uses Gemini models when only Gemini is available", () => {
// #given only Gemini is available
const config = createConfig({ hasGemini: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use Gemini models
expect(result).toMatchSnapshot()
})
test("uses Gemini models with isMax20 flag", () => {
// #given Gemini is available with Max 20 plan
const config = createConfig({ hasGemini: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
expect(result).toMatchSnapshot()
})
})
describe("all native providers", () => {
test("uses preferred models from fallback chains when all natives available", () => {
// #given all native providers are available
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
hasGemini: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use first provider in each fallback chain
expect(result).toMatchSnapshot()
})
test("uses preferred models with isMax20 flag when all natives available", () => {
// #given all native providers are available with Max 20 plan
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
hasGemini: true,
isMax20: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
expect(result).toMatchSnapshot()
})
})
describe("fallback providers", () => {
test("uses OpenCode Zen models when only OpenCode Zen is available", () => {
// #given only OpenCode Zen is available
const config = createConfig({ hasOpencodeZen: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use OPENCODE_ZEN_MODELS
expect(result).toMatchSnapshot()
})
test("uses OpenCode Zen models with isMax20 flag", () => {
// #given OpenCode Zen is available with Max 20 plan
const config = createConfig({ hasOpencodeZen: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
expect(result).toMatchSnapshot()
})
test("uses GitHub Copilot models when only Copilot is available", () => {
// #given only GitHub Copilot is available
const config = createConfig({ hasCopilot: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use GITHUB_COPILOT_MODELS
expect(result).toMatchSnapshot()
})
test("uses GitHub Copilot models with isMax20 flag", () => {
// #given GitHub Copilot is available with Max 20 plan
const config = createConfig({ hasCopilot: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
expect(result).toMatchSnapshot()
})
test("uses ZAI model for librarian when only ZAI is available", () => {
// #given only ZAI is available
const config = createConfig({ hasZaiCodingPlan: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use ZAI_MODEL for librarian
expect(result).toMatchSnapshot()
})
test("uses ZAI model for librarian with isMax20 flag", () => {
// #given ZAI is available with Max 20 plan
const config = createConfig({ hasZaiCodingPlan: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use ZAI_MODEL for librarian
expect(result).toMatchSnapshot()
})
})
describe("mixed provider scenarios", () => {
test("uses Claude + OpenCode Zen combination", () => {
// #given Claude and OpenCode Zen are available
const config = createConfig({
hasClaude: true,
hasOpencodeZen: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer Claude (native) over OpenCode Zen
expect(result).toMatchSnapshot()
})
test("uses OpenAI + Copilot combination", () => {
// #given OpenAI and Copilot are available
const config = createConfig({
hasOpenAI: true,
hasCopilot: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer OpenAI (native) over Copilot
expect(result).toMatchSnapshot()
})
test("uses Claude + ZAI combination (librarian uses ZAI)", () => {
// #given Claude and ZAI are available
const config = createConfig({
hasClaude: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then librarian should use ZAI, others use Claude
expect(result).toMatchSnapshot()
})
test("uses Gemini + Claude combination (explore uses Gemini)", () => {
// #given Gemini and Claude are available
const config = createConfig({
hasGemini: true,
hasClaude: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use Gemini flash
expect(result).toMatchSnapshot()
})
test("uses all fallback providers together", () => {
// #given all fallback providers are available
const config = createConfig({
hasOpencodeZen: true,
hasCopilot: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer OpenCode Zen, but librarian uses ZAI
expect(result).toMatchSnapshot()
})
test("uses all providers together", () => {
// #given all providers are available
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
hasGemini: true,
hasOpencodeZen: true,
hasCopilot: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer native providers, librarian uses ZAI
expect(result).toMatchSnapshot()
})
test("uses all providers with isMax20 flag", () => {
// #given all providers are available with Max 20 plan
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
hasGemini: true,
hasOpencodeZen: true,
hasCopilot: true,
hasZaiCodingPlan: true,
isMax20: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
expect(result).toMatchSnapshot()
})
})
describe("explore agent special cases", () => {
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 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", () => {
// #given Claude is available
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use claude-haiku-4-5
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
})
test("explore uses Claude haiku regardless of isMax20 flag", () => {
// #given Claude is available without Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: false })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use claude-haiku-4-5 (isMax20 doesn't affect explore)
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
})
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 gpt-5-nano (fallback)
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
})
test("explore uses gpt-5-mini when only Copilot available", () => {
// #given only Copilot is available
const config = createConfig({ hasCopilot: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use gpt-5-mini (Copilot fallback)
expect(result.agents?.explore?.model).toBe("github-copilot/gpt-5-mini")
})
})
describe("Sisyphus agent special cases", () => {
test("Sisyphus uses sisyphus-high capability when isMax20 is true", () => {
// #given Claude is available with Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then Sisyphus should use opus (sisyphus-high)
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
})
test("Sisyphus uses sisyphus-low capability when isMax20 is false", () => {
// #given Claude is available without Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: false })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then Sisyphus should use sonnet (sisyphus-low)
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-sonnet-4-5")
})
})
describe("librarian agent special cases", () => {
test("librarian uses ZAI when ZAI is available regardless of other providers", () => {
// #given ZAI and Claude are available
const config = createConfig({
hasClaude: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then librarian should use ZAI_MODEL
expect(result.agents?.librarian?.model).toBe("zai-coding-plan/glm-4.7")
})
test("librarian uses claude-sonnet when ZAI not available but Claude is", () => {
// #given only Claude is available (no ZAI)
const config = createConfig({ hasClaude: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm)
expect(result.agents?.librarian?.model).toBe("anthropic/claude-sonnet-4-5")
})
})
describe("schema URL", () => {
test("always includes correct schema URL", () => {
// #given any config
const config = createConfig()
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then should include correct schema URL
expect(result.$schema).toBe(
"https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
)
})
})
})

View File

@@ -1,17 +1,10 @@
import {
AGENT_MODEL_REQUIREMENTS,
CATEGORY_MODEL_REQUIREMENTS,
type FallbackEntry,
} from "../shared/model-requirements"
import type { InstallConfig } from "./types"
type NativeProvider = "claude" | "openai" | "gemini"
type ModelCapability =
| "unspecified-high"
| "unspecified-low"
| "quick"
| "ultrabrain"
| "visual-engineering"
| "artistry"
| "writing"
| "glm"
interface ProviderAvailability {
native: {
claude: boolean
@@ -41,107 +34,9 @@ export interface GeneratedOmoConfig {
[key: string]: unknown
}
interface NativeFallbackEntry {
provider: NativeProvider
model: string
}
const NATIVE_FALLBACK_CHAINS: Record<ModelCapability, NativeFallbackEntry[]> = {
"unspecified-high": [
{ provider: "claude", model: "anthropic/claude-opus-4-5" },
{ provider: "openai", model: "openai/gpt-5.2" },
{ provider: "gemini", model: "google/gemini-3-pro-preview" },
],
"unspecified-low": [
{ provider: "claude", model: "anthropic/claude-sonnet-4-5" },
{ provider: "openai", model: "openai/gpt-5.2" },
{ provider: "gemini", model: "google/gemini-3-flash-preview" },
],
quick: [
{ provider: "claude", model: "anthropic/claude-haiku-4-5" },
{ provider: "openai", model: "openai/gpt-5.1-codex-mini" },
{ provider: "gemini", model: "google/gemini-3-flash-preview" },
],
ultrabrain: [
{ provider: "openai", model: "openai/gpt-5.2-codex" },
{ provider: "claude", model: "anthropic/claude-opus-4-5" },
{ provider: "gemini", model: "google/gemini-3-pro-preview" },
],
"visual-engineering": [
{ provider: "gemini", model: "google/gemini-3-pro-preview" },
{ provider: "openai", model: "openai/gpt-5.2" },
{ provider: "claude", model: "anthropic/claude-sonnet-4-5" },
],
artistry: [
{ provider: "gemini", model: "google/gemini-3-pro-preview" },
{ provider: "openai", model: "openai/gpt-5.2" },
{ provider: "claude", model: "anthropic/claude-opus-4-5" },
],
writing: [
{ provider: "gemini", model: "google/gemini-3-flash-preview" },
{ provider: "openai", model: "openai/gpt-5.2" },
{ provider: "claude", model: "anthropic/claude-sonnet-4-5" },
],
glm: [],
}
const OPENCODE_ZEN_MODELS: Record<ModelCapability, string> = {
"unspecified-high": "opencode/claude-opus-4-5",
"unspecified-low": "opencode/claude-sonnet-4-5",
quick: "opencode/claude-haiku-4-5",
ultrabrain: "opencode/gpt-5.2-codex",
"visual-engineering": "opencode/gemini-3-pro",
artistry: "opencode/gemini-3-pro",
writing: "opencode/gemini-3-flash",
glm: "opencode/glm-4.7-free",
}
const GITHUB_COPILOT_MODELS: Record<ModelCapability, string> = {
"unspecified-high": "github-copilot/claude-opus-4.5",
"unspecified-low": "github-copilot/claude-sonnet-4.5",
quick: "github-copilot/claude-haiku-4.5",
ultrabrain: "github-copilot/gpt-5.2-codex",
"visual-engineering": "github-copilot/gemini-3-pro-preview",
artistry: "github-copilot/gemini-3-pro-preview",
writing: "github-copilot/gemini-3-flash-preview",
glm: "github-copilot/gpt-5.2",
}
const ZAI_MODEL = "zai-coding-plan/glm-4.7"
interface AgentRequirement {
capability: ModelCapability
variant?: string
}
const AGENT_REQUIREMENTS: Record<string, AgentRequirement> = {
Sisyphus: { capability: "unspecified-high" },
oracle: { capability: "ultrabrain", variant: "high" },
librarian: { capability: "glm" },
explore: { capability: "quick" },
"multimodal-looker": { capability: "visual-engineering" },
"Prometheus (Planner)": { capability: "unspecified-high" },
"Metis (Plan Consultant)": { capability: "unspecified-high" },
"Momus (Plan Reviewer)": { capability: "ultrabrain", variant: "medium" },
Atlas: { capability: "unspecified-high" },
}
interface CategoryRequirement {
capability: ModelCapability
variant?: string
}
const CATEGORY_REQUIREMENTS: Record<string, CategoryRequirement> = {
"visual-engineering": { capability: "visual-engineering" },
ultrabrain: { capability: "ultrabrain" },
artistry: { capability: "artistry", variant: "max" },
quick: { capability: "quick" },
"unspecified-low": { capability: "unspecified-low" },
"unspecified-high": { capability: "unspecified-high" },
writing: { capability: "writing" },
}
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 {
@@ -158,31 +53,58 @@ function toProviderAvailability(config: InstallConfig): ProviderAvailability {
}
}
function resolveModel(capability: ModelCapability, avail: ProviderAvailability): string {
const nativeChain = NATIVE_FALLBACK_CHAINS[capability]
for (const entry of nativeChain) {
if (avail.native[entry.provider]) {
return entry.model
}
function isProviderAvailable(provider: string, avail: ProviderAvailability): boolean {
const mapping: Record<string, boolean> = {
anthropic: avail.native.claude,
openai: avail.native.openai,
google: avail.native.gemini,
"github-copilot": avail.copilot,
opencode: avail.opencodeZen,
"zai-coding-plan": avail.zai,
}
if (avail.opencodeZen) {
return OPENCODE_ZEN_MODELS[capability]
}
if (avail.copilot) {
return GITHUB_COPILOT_MODELS[capability]
}
if (avail.zai) {
return ZAI_MODEL
}
return ULTIMATE_FALLBACK
return mapping[provider] ?? false
}
function resolveClaudeCapability(avail: ProviderAvailability): ModelCapability {
return avail.isMaxPlan ? "unspecified-high" : "unspecified-low"
function transformModelForProvider(provider: string, model: string): string {
if (provider === "github-copilot") {
return model
.replace("claude-opus-4-5", "claude-opus-4.5")
.replace("claude-sonnet-4-5", "claude-sonnet-4.5")
.replace("claude-haiku-4-5", "claude-haiku-4.5")
.replace("claude-sonnet-4", "claude-sonnet-4")
}
return model
}
function resolveModelFromChain(
fallbackChain: FallbackEntry[],
avail: ProviderAvailability
): { model: string; variant?: string } | null {
for (const entry of fallbackChain) {
for (const provider of entry.providers) {
if (isProviderAvailable(provider, avail)) {
const transformedModel = transformModelForProvider(provider, entry.model)
return {
model: `${provider}/${transformedModel}`,
variant: entry.variant,
}
}
}
}
return null
}
function getSisyphusFallbackChain(isMaxPlan: boolean): FallbackEntry[] {
// Sisyphus uses opus when isMaxPlan, sonnet otherwise
if (isMaxPlan) {
return AGENT_MODEL_REQUIREMENTS.sisyphus.fallbackChain
}
// For non-max plan, use sonnet instead of opus
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" },
]
}
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
@@ -199,10 +121,10 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
return {
$schema: SCHEMA_URL,
agents: Object.fromEntries(
Object.keys(AGENT_REQUIREMENTS).map((role) => [role, { model: ULTIMATE_FALLBACK }])
Object.keys(AGENT_MODEL_REQUIREMENTS).map((role) => [role, { model: ULTIMATE_FALLBACK }])
),
categories: Object.fromEntries(
Object.keys(CATEGORY_REQUIREMENTS).map((cat) => [cat, { model: ULTIMATE_FALLBACK }])
Object.keys(CATEGORY_MODEL_REQUIREMENTS).map((cat) => [cat, { model: ULTIMATE_FALLBACK }])
),
}
}
@@ -210,28 +132,54 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
const agents: Record<string, AgentConfig> = {}
const categories: Record<string, CategoryConfig> = {}
const claudeCapability = resolveClaudeCapability(avail)
for (const [role, req] of Object.entries(AGENT_REQUIREMENTS)) {
for (const [role, req] of Object.entries(AGENT_MODEL_REQUIREMENTS)) {
// Special case: librarian always uses ZAI first if available
if (role === "librarian" && avail.zai) {
agents[role] = { model: ZAI_MODEL }
} else if (role === "explore") {
if (avail.native.claude && avail.isMaxPlan) {
continue
}
// Special case: explore uses Claude haiku → GitHub Copilot gpt-5-mini → 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 if (avail.copilot) {
agents[role] = { model: "github-copilot/gpt-5-mini" }
} else {
agents[role] = { model: "opencode/grok-code" }
agents[role] = { model: "opencode/gpt-5-nano" }
}
continue
}
// Special case: Sisyphus uses different fallbackChain based on isMaxPlan
const fallbackChain =
role === "sisyphus" ? getSisyphusFallbackChain(avail.isMaxPlan) : req.fallbackChain
const resolved = resolveModelFromChain(fallbackChain, avail)
if (resolved) {
const variant = resolved.variant ?? req.variant
agents[role] = variant ? { model: resolved.model, variant } : { model: resolved.model }
} else {
const capability = req.capability === "unspecified-high" ? claudeCapability : req.capability
const model = resolveModel(capability, avail)
agents[role] = req.variant ? { model, variant: req.variant } : { model }
agents[role] = { model: ULTIMATE_FALLBACK }
}
}
for (const [cat, req] of Object.entries(CATEGORY_REQUIREMENTS)) {
const capability = req.capability === "unspecified-high" ? claudeCapability : req.capability
const model = resolveModel(capability, avail)
categories[cat] = req.variant ? { model, variant: req.variant } : { model }
for (const [cat, req] of Object.entries(CATEGORY_MODEL_REQUIREMENTS)) {
// Special case: unspecified-high downgrades to unspecified-low when not isMaxPlan
const fallbackChain =
cat === "unspecified-high" && !avail.isMaxPlan
? CATEGORY_MODEL_REQUIREMENTS["unspecified-low"].fallbackChain
: req.fallbackChain
const resolved = resolveModelFromChain(fallbackChain, avail)
if (resolved) {
const variant = resolved.variant ?? req.variant
categories[cat] = variant ? { model: resolved.model, variant } : { model: resolved.model }
} else {
categories[cat] = { model: ULTIMATE_FALLBACK }
}
}
return {

View File

@@ -154,7 +154,7 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
const input = toolProps?.input ?? {}
const inputStr = JSON.stringify(input).slice(0, 150)
console.error(
pc.cyan(`${sessionTag} TOOL.EXECUTE: ${pc.bold(toolName)}`)
pc.cyan(`${sessionTag} TOOL.EXECUTE: ${pc.bold(toolName)}`)
)
console.error(pc.dim(` input: ${inputStr}${inputStr.length >= 150 ? "..." : ""}`))
break
@@ -165,7 +165,7 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
const output = resultProps?.output ?? ""
const preview = output.slice(0, 200).replace(/\n/g, "\\n")
console.error(
pc.green(`${sessionTag} TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`)
pc.green(`${sessionTag} TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`)
)
break
}
@@ -173,7 +173,7 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
case "session.error": {
const errorProps = props as SessionErrorProps | undefined
const errorMsg = serializeError(errorProps?.error)
console.error(pc.red(`${sessionTag} SESSION.ERROR: ${errorMsg}`))
console.error(pc.red(`${sessionTag} SESSION.ERROR: ${errorMsg}`))
break
}
@@ -296,7 +296,7 @@ function handleToolExecute(
}
}
process.stdout.write(`\n${pc.cyan("")} ${pc.bold(toolName)}${inputPreview}\n`)
process.stdout.write(`\n${pc.cyan(">")} ${pc.bold(toolName)}${inputPreview}\n`)
}
function handleToolResult(

View File

@@ -31,8 +31,18 @@ export async function run(options: RunOptions): Promise<number> {
}
try {
// Support custom OpenCode server port via environment variable
// This allows Open Agent and other orchestrators to run multiple
// concurrent missions without port conflicts
const serverPort = process.env.OPENCODE_SERVER_PORT
? parseInt(process.env.OPENCODE_SERVER_PORT, 10)
: undefined
const serverHostname = process.env.OPENCODE_SERVER_HOSTNAME || undefined
const { client, server } = await createOpencode({
signal: abortController.signal,
...(serverPort && !isNaN(serverPort) ? { port: serverPort } : {}),
...(serverHostname ? { hostname: serverHostname } : {}),
})
const cleanup = () => {

View File

@@ -9,6 +9,8 @@ export {
SisyphusAgentConfigSchema,
ExperimentalConfigSchema,
RalphLoopConfigSchema,
TmuxConfigSchema,
TmuxLayoutSchema,
} from "./schema"
export type {
@@ -23,4 +25,6 @@ export type {
ExperimentalConfig,
DynamicContextPruningConfig,
RalphLoopConfig,
TmuxConfig,
TmuxLayout,
} from "./schema"

View File

@@ -1,5 +1,12 @@
import { describe, expect, test } from "bun:test"
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, CategoryConfigSchema, OhMyOpenCodeConfigSchema } from "./schema"
import {
AgentOverrideConfigSchema,
BrowserAutomationConfigSchema,
BrowserAutomationProviderSchema,
BuiltinCategoryNameSchema,
CategoryConfigSchema,
OhMyOpenCodeConfigSchema,
} from "./schema"
describe("disabled_mcps schema", () => {
test("should accept built-in MCP names", () => {
@@ -345,6 +352,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 }
@@ -375,7 +396,7 @@ describe("Sisyphus-Junior agent override", () => {
// #given
const config = {
agents: {
"Sisyphus-Junior": {
"sisyphus-junior": {
model: "openai/gpt-5.2",
temperature: 0.2,
},
@@ -388,18 +409,18 @@ describe("Sisyphus-Junior agent override", () => {
// #then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.agents?.["Sisyphus-Junior"]).toBeDefined()
expect(result.data.agents?.["Sisyphus-Junior"]?.model).toBe("openai/gpt-5.2")
expect(result.data.agents?.["Sisyphus-Junior"]?.temperature).toBe(0.2)
expect(result.data.agents?.["sisyphus-junior"]).toBeDefined()
expect(result.data.agents?.["sisyphus-junior"]?.model).toBe("openai/gpt-5.2")
expect(result.data.agents?.["sisyphus-junior"]?.temperature).toBe(0.2)
}
})
test("schema accepts Sisyphus-Junior with prompt_append", () => {
test("schema accepts sisyphus-junior with prompt_append", () => {
// #given
const config = {
agents: {
"Sisyphus-Junior": {
prompt_append: "Additional instructions for Sisyphus-Junior",
"sisyphus-junior": {
prompt_append: "Additional instructions for sisyphus-junior",
},
},
}
@@ -410,17 +431,17 @@ describe("Sisyphus-Junior agent override", () => {
// #then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.agents?.["Sisyphus-Junior"]?.prompt_append).toBe(
"Additional instructions for Sisyphus-Junior"
expect(result.data.agents?.["sisyphus-junior"]?.prompt_append).toBe(
"Additional instructions for sisyphus-junior"
)
}
})
test("schema accepts Sisyphus-Junior with tools override", () => {
test("schema accepts sisyphus-junior with tools override", () => {
// #given
const config = {
agents: {
"Sisyphus-Junior": {
"sisyphus-junior": {
tools: {
read: true,
write: false,
@@ -435,10 +456,153 @@ describe("Sisyphus-Junior agent override", () => {
// #then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.agents?.["Sisyphus-Junior"]?.tools).toEqual({
expect(result.data.agents?.["sisyphus-junior"]?.tools).toEqual({
read: true,
write: false,
})
}
})
test("schema accepts lowercase agent names (sisyphus, atlas, prometheus)", () => {
// #given
const config = {
agents: {
sisyphus: {
temperature: 0.1,
},
atlas: {
temperature: 0.2,
},
prometheus: {
temperature: 0.3,
},
},
}
// #when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// #then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.agents?.sisyphus?.temperature).toBe(0.1)
expect(result.data.agents?.atlas?.temperature).toBe(0.2)
expect(result.data.agents?.prometheus?.temperature).toBe(0.3)
}
})
test("schema accepts lowercase metis and momus agent names", () => {
// #given
const config = {
agents: {
metis: {
category: "ultrabrain",
},
momus: {
category: "quick",
},
},
}
// #when
const result = OhMyOpenCodeConfigSchema.safeParse(config)
// #then
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.agents?.metis?.category).toBe("ultrabrain")
expect(result.data.agents?.momus?.category).toBe("quick")
}
})
})
describe("BrowserAutomationProviderSchema", () => {
test("accepts 'playwright' as valid provider", () => {
// #given
const input = "playwright"
// #when
const result = BrowserAutomationProviderSchema.safeParse(input)
// #then
expect(result.success).toBe(true)
expect(result.data).toBe("playwright")
})
test("accepts 'agent-browser' as valid provider", () => {
// #given
const input = "agent-browser"
// #when
const result = BrowserAutomationProviderSchema.safeParse(input)
// #then
expect(result.success).toBe(true)
expect(result.data).toBe("agent-browser")
})
test("rejects invalid provider", () => {
// #given
const input = "invalid-provider"
// #when
const result = BrowserAutomationProviderSchema.safeParse(input)
// #then
expect(result.success).toBe(false)
})
})
describe("BrowserAutomationConfigSchema", () => {
test("defaults provider to 'playwright' when not specified", () => {
// #given
const input = {}
// #when
const result = BrowserAutomationConfigSchema.parse(input)
// #then
expect(result.provider).toBe("playwright")
})
test("accepts agent-browser provider", () => {
// #given
const input = { provider: "agent-browser" }
// #when
const result = BrowserAutomationConfigSchema.parse(input)
// #then
expect(result.provider).toBe("agent-browser")
})
})
describe("OhMyOpenCodeConfigSchema - browser_automation_engine", () => {
test("accepts browser_automation_engine config", () => {
// #given
const input = {
browser_automation_engine: {
provider: "agent-browser",
},
}
// #when
const result = OhMyOpenCodeConfigSchema.safeParse(input)
// #then
expect(result.success).toBe(true)
expect(result.data?.browser_automation_engine?.provider).toBe("agent-browser")
})
test("accepts config without browser_automation_engine", () => {
// #given
const input = {}
// #when
const result = OhMyOpenCodeConfigSchema.safeParse(input)
// #then
expect(result.success).toBe(true)
expect(result.data?.browser_automation_engine).toBeUndefined()
})
})

View File

@@ -17,18 +17,20 @@ const AgentPermissionSchema = z.object({
})
export const BuiltinAgentNameSchema = z.enum([
"Sisyphus",
"sisyphus",
"prometheus",
"oracle",
"librarian",
"explore",
"multimodal-looker",
"Metis (Plan Consultant)",
"Momus (Plan Reviewer)",
"Atlas",
"metis",
"momus",
"atlas",
])
export const BuiltinSkillNameSchema = z.enum([
"playwright",
"agent-browser",
"frontend-ui-ux",
"git-master",
])
@@ -36,17 +38,17 @@ export const BuiltinSkillNameSchema = z.enum([
export const OverridableAgentNameSchema = z.enum([
"build",
"plan",
"Sisyphus",
"Sisyphus-Junior",
"sisyphus",
"sisyphus-junior",
"OpenCode-Builder",
"Prometheus (Planner)",
"Metis (Plan Consultant)",
"Momus (Plan Reviewer)",
"prometheus",
"metis",
"momus",
"oracle",
"librarian",
"explore",
"multimodal-looker",
"Atlas",
"atlas",
])
export const AgentNameSchema = BuiltinAgentNameSchema
@@ -75,6 +77,7 @@ export const HookNameSchema = z.enum([
"thinking-block-validator",
"ralph-loop",
"category-skill-reminder",
"compaction-context-injector",
"claude-code-hooks",
@@ -82,6 +85,7 @@ export const HookNameSchema = z.enum([
"edit-error-recovery",
"delegate-task-retry",
"prometheus-md-only",
"sisyphus-junior-notepad",
"start-work",
"atlas",
])
@@ -112,22 +116,35 @@ export const AgentOverrideConfigSchema = z.object({
.regex(/^#[0-9A-Fa-f]{6}$/)
.optional(),
permission: AgentPermissionSchema.optional(),
/** Maximum tokens for response. Passed directly to OpenCode SDK. */
maxTokens: z.number().optional(),
/** Extended thinking configuration (Anthropic). Overrides category and default settings. */
thinking: z.object({
type: z.enum(["enabled", "disabled"]),
budgetTokens: z.number().optional(),
}).optional(),
/** Reasoning effort level (OpenAI). Overrides category and default settings. */
reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(),
/** Text verbosity level. */
textVerbosity: z.enum(["low", "medium", "high"]).optional(),
/** Provider-specific options. Passed directly to OpenCode SDK. */
providerOptions: z.record(z.string(), z.unknown()).optional(),
})
export const AgentOverridesSchema = z.object({
build: AgentOverrideConfigSchema.optional(),
plan: AgentOverrideConfigSchema.optional(),
Sisyphus: AgentOverrideConfigSchema.optional(),
"Sisyphus-Junior": AgentOverrideConfigSchema.optional(),
sisyphus: AgentOverrideConfigSchema.optional(),
"sisyphus-junior": AgentOverrideConfigSchema.optional(),
"OpenCode-Builder": AgentOverrideConfigSchema.optional(),
"Prometheus (Planner)": AgentOverrideConfigSchema.optional(),
"Metis (Plan Consultant)": AgentOverrideConfigSchema.optional(),
"Momus (Plan Reviewer)": AgentOverrideConfigSchema.optional(),
prometheus: AgentOverrideConfigSchema.optional(),
metis: AgentOverrideConfigSchema.optional(),
momus: AgentOverrideConfigSchema.optional(),
oracle: AgentOverrideConfigSchema.optional(),
librarian: AgentOverrideConfigSchema.optional(),
explore: AgentOverrideConfigSchema.optional(),
"multimodal-looker": AgentOverrideConfigSchema.optional(),
Atlas: AgentOverrideConfigSchema.optional(),
atlas: AgentOverrideConfigSchema.optional(),
})
export const ClaudeCodeConfigSchema = z.object({
@@ -148,6 +165,8 @@ export const SisyphusAgentConfigSchema = z.object({
})
export const CategoryConfigSchema = z.object({
/** Human-readable description of the category's purpose. Shown in delegate_task prompt. */
description: z.string().optional(),
model: z.string().optional(),
variant: z.string().optional(),
temperature: z.number().min(0).max(2).optional(),
@@ -157,7 +176,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(),
@@ -276,8 +295,8 @@ export const RalphLoopConfigSchema = z.object({
export const BackgroundTaskConfigSchema = z.object({
defaultConcurrency: z.number().min(1).optional(),
providerConcurrency: z.record(z.string(), z.number().min(1)).optional(),
modelConcurrency: z.record(z.string(), z.number().min(1)).optional(),
providerConcurrency: z.record(z.string(), z.number().min(0)).optional(),
modelConcurrency: z.record(z.string(), z.number().min(0)).optional(),
/** Stale timeout in milliseconds - interrupt tasks with no activity for this duration (default: 180000 = 3 minutes, minimum: 60000 = 1 minute) */
staleTimeoutMs: z.number().min(60000).optional(),
})
@@ -294,6 +313,56 @@ export const GitMasterConfigSchema = z.object({
include_co_authored_by: z.boolean().default(true),
})
export const BrowserAutomationProviderSchema = z.enum(["playwright", "agent-browser", "dev-browser"])
export const BrowserAutomationConfigSchema = z.object({
/**
* Browser automation provider to use for the "playwright" skill.
* - "playwright": Uses Playwright MCP server (@playwright/mcp) - default
* - "agent-browser": Uses Vercel's agent-browser CLI (requires: bun add -g agent-browser)
* - "dev-browser": Uses dev-browser skill with persistent browser state
*/
provider: BrowserAutomationProviderSchema.default("playwright"),
})
export const TmuxLayoutSchema = z.enum([
'main-horizontal', // main pane top, agent panes bottom stack
'main-vertical', // main pane left, agent panes right stack (default)
'tiled', // all panes same size grid
'even-horizontal', // all panes horizontal row
'even-vertical', // all panes vertical stack
])
export const TmuxConfigSchema = z.object({
enabled: z.boolean().default(false),
layout: TmuxLayoutSchema.default('main-vertical'),
main_pane_size: z.number().min(20).max(80).default(60),
main_pane_min_width: z.number().min(40).default(120),
agent_pane_min_width: z.number().min(20).default(40),
})
export const SisyphusTasksConfigSchema = z.object({
/** Enable Sisyphus Tasks system (default: false) */
enabled: z.boolean().default(false),
/** Storage path for tasks (default: .sisyphus/tasks) */
storage_path: z.string().default(".sisyphus/tasks"),
/** Enable Claude Code path compatibility mode */
claude_code_compat: z.boolean().default(false),
})
export const SisyphusSwarmConfigSchema = z.object({
/** Enable Sisyphus Swarm system (default: false) */
enabled: z.boolean().default(false),
/** Storage path for teams (default: .sisyphus/teams) */
storage_path: z.string().default(".sisyphus/teams"),
/** UI mode: toast notifications, tmux panes, or both */
ui_mode: z.enum(["toast", "tmux", "both"]).default("toast"),
})
export const SisyphusConfigSchema = z.object({
tasks: SisyphusTasksConfigSchema.optional(),
swarm: SisyphusSwarmConfigSchema.optional(),
})
export const OhMyOpenCodeConfigSchema = z.object({
$schema: z.string().optional(),
disabled_mcps: z.array(AnyMcpNameSchema).optional(),
@@ -313,6 +382,9 @@ export const OhMyOpenCodeConfigSchema = z.object({
background_task: BackgroundTaskConfigSchema.optional(),
notification: NotificationConfigSchema.optional(),
git_master: GitMasterConfigSchema.optional(),
browser_automation_engine: BrowserAutomationConfigSchema.optional(),
tmux: TmuxConfigSchema.optional(),
sisyphus: SisyphusConfigSchema.optional(),
})
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
@@ -335,5 +407,12 @@ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>
export type CategoriesConfig = z.infer<typeof CategoriesConfigSchema>
export type BuiltinCategoryName = z.infer<typeof BuiltinCategoryNameSchema>
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>
export type BrowserAutomationProvider = z.infer<typeof BrowserAutomationProviderSchema>
export type BrowserAutomationConfig = z.infer<typeof BrowserAutomationConfigSchema>
export type TmuxConfig = z.infer<typeof TmuxConfigSchema>
export type TmuxLayout = z.infer<typeof TmuxLayoutSchema>
export type SisyphusTasksConfig = z.infer<typeof SisyphusTasksConfigSchema>
export type SisyphusSwarmConfig = z.infer<typeof SisyphusSwarmConfigSchema>
export type SisyphusConfig = z.infer<typeof SisyphusConfigSchema>
export { AnyMcpNameSchema, type AnyMcpName, McpNameSchema, type McpName } from "../mcp/types"

View File

@@ -2,76 +2,58 @@
## OVERVIEW
Core feature modules + Claude Code compatibility layer. Background agents, skill MCP, builtin skills/commands, and 5 loaders for Claude Code compat.
Core feature modules + Claude Code compatibility layer. Orchestrates background agents, skill MCPs, builtin skills/commands, and 16 feature modules.
## STRUCTURE
```
features/
├── background-agent/ # Task lifecycle (1165 lines manager.ts)
│ ├── manager.ts # Launch → poll → complete orchestration
── concurrency.ts # Per-provider/model limits
│ └── types.ts # BackgroundTask, LaunchInput
── skill-mcp-manager/ # MCP client lifecycle
│ ├── manager.ts # Lazy loading, idle cleanup
│ └── types.ts # SkillMcpConfig, transports
├── builtin-skills/ # Playwright, git-master, frontend-ui-ux
│ └── skills.ts # 1203 lines of skill definitions
├── builtin-commands/ # ralph-loop, refactor, init-deep
│ └── templates/ # Command implementations
├── background-agent/ # Task lifecycle (1377 lines)
│ ├── manager.ts # Launch → poll → complete
── concurrency.ts # Per-provider limits
├── builtin-skills/ # Core skills (1729 lines)
│ └── skills.ts # agent-browser, dev-browser, frontend-ui-ux, git-master, typescript-programmer
├── builtin-commands/ # ralph-loop, refactor, ulw-loop, init-deep, start-work, cancel-ralph
├── claude-code-agent-loader/ # ~/.claude/agents/*.md
├── claude-code-command-loader/ # ~/.claude/commands/*.md
├── claude-code-mcp-loader/ # .mcp.json with ${VAR} expansion
├── claude-code-plugin-loader/ # installed_plugins.json
├── claude-code-session-state/ # Session state persistence
├── claude-code-session-state/ # Session persistence
├── opencode-skill-loader/ # Skills from 6 directories
├── context-injector/ # AGENTS.md/README.md injection
├── boulder-state/ # Todo state persistence
├── task-toast-manager/ # Toast notifications
── hook-message-injector/ # Message injection
├── hook-message-injector/ # Message injection
── task-toast-manager/ # Background task notifications
├── skill-mcp-manager/ # MCP client lifecycle (520 lines)
├── tmux-subagent/ # Tmux session management
└── ... (16 modules total)
```
## LOADER PRIORITY
| Type | Priority (highest first) |
|------|--------------------------|
| Commands | `.opencode/command/` > `~/.config/opencode/command/` > `.claude/commands/` > `~/.claude/commands/` |
| Skills | `.opencode/skills/` > `~/.config/opencode/skills/` > `.claude/skills/` > `~/.claude/skills/` |
| Agents | `.claude/agents/` > `~/.claude/agents/` |
| Commands | `.opencode/command/` > `~/.config/opencode/command/` > `.claude/commands/` |
| Skills | `.opencode/skills/` > `~/.config/opencode/skills/` > `.claude/skills/` |
| MCPs | `.claude/.mcp.json` > `.mcp.json` > `~/.claude/.mcp.json` |
## BACKGROUND AGENT
- **Lifecycle**: `launch``poll` (2s interval) → `complete`
- **Stability**: 3 consecutive polls with same message count = idle
- **Concurrency**: Per-provider/model limits (e.g., max 3 Opus, max 10 Gemini)
- **Notification**: Batched system reminders to parent session
- **Cleanup**: 30m TTL, 3m stale timeout, signal handlers
- **Lifecycle**: `launch``poll` (2s) → `complete`
- **Stability**: 3 consecutive polls = idle
- **Concurrency**: Per-provider/model limits via `ConcurrencyManager`
- **Cleanup**: 30m TTL, 3m stale timeout
- **State**: Per-session Maps, cleaned on `session.deleted`
## SKILL MCP
- **Lazy**: Clients created on first tool call
- **Transports**: stdio (local process), http (SSE/Streamable)
- **Environment**: `${VAR}` expansion in config
- **Lifecycle**: 5m idle cleanup, session-scoped
## CONFIG TOGGLES
```jsonc
{
"claude_code": {
"mcp": false, // Skip .mcp.json
"commands": false, // Skip commands/*.md
"skills": false, // Skip skills/*/SKILL.md
"agents": false, // Skip agents/*.md
"hooks": false // Skip settings.json hooks
}
}
```
- **Lazy**: Clients created on first call
- **Transports**: stdio, http (SSE/Streamable)
- **Lifecycle**: 5m idle cleanup
## ANTI-PATTERNS
- **Sequential delegation**: Use `delegate_task` for parallel
- **Trust self-reports**: ALWAYS verify agent outputs
- **Sequential delegation**: Use `delegate_task` parallel
- **Trust self-reports**: ALWAYS verify
- **Main thread blocks**: No heavy I/O in loader init
- **Manual versioning**: CI manages package.json version
- **Direct state mutation**: Use managers for boulder/session state

View File

@@ -776,7 +776,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
parentModel: { providerID: "old", modelID: "old-model" },
}
const currentMessage: CurrentMessage = {
agent: "Sisyphus",
agent: "sisyphus",
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
}
@@ -784,7 +784,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
const promptBody = buildNotificationPromptBody(task, currentMessage)
// #then - uses currentMessage values, not task.parentModel/parentAgent
expect(promptBody.agent).toBe("Sisyphus")
expect(promptBody.agent).toBe("sisyphus")
expect(promptBody.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-5" })
})
@@ -827,11 +827,11 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
parentAgent: "Sisyphus",
parentAgent: "sisyphus",
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
}
const currentMessage: CurrentMessage = {
agent: "Sisyphus",
agent: "sisyphus",
model: { providerID: "anthropic" },
}
@@ -839,7 +839,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
const promptBody = buildNotificationPromptBody(task, currentMessage)
// #then - model not passed due to incomplete data
expect(promptBody.agent).toBe("Sisyphus")
expect(promptBody.agent).toBe("sisyphus")
expect("model" in promptBody).toBe(false)
})
@@ -856,7 +856,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
parentAgent: "Sisyphus",
parentAgent: "sisyphus",
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
}
@@ -864,7 +864,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
const promptBody = buildNotificationPromptBody(task, null)
// #then - falls back to task.parentAgent, no model
expect(promptBody.agent).toBe("Sisyphus")
expect(promptBody.agent).toBe("sisyphus")
expect("model" in promptBody).toBe(false)
})
})

View File

@@ -7,7 +7,8 @@ import type {
} from "./types"
import { log, getAgentToolRestrictions } from "../../shared"
import { ConcurrencyManager } from "./concurrency"
import type { BackgroundTaskConfig } from "../../config/schema"
import type { BackgroundTaskConfig, TmuxConfig } from "../../config/schema"
import { isInsideTmux } from "../../shared/tmux"
import { subagentSessions } from "../claude-code-session-state"
import { getTaskToastManager } from "../task-toast-manager"
@@ -54,6 +55,14 @@ interface QueueItem {
input: LaunchInput
}
export interface SubagentSessionCreatedEvent {
sessionID: string
parentID: string
title: string
}
export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise<void>
export class BackgroundManager {
private static cleanupManagers = new Set<BackgroundManager>()
private static cleanupRegistered = false
@@ -68,12 +77,20 @@ export class BackgroundManager {
private concurrencyManager: ConcurrencyManager
private shutdownTriggered = false
private config?: BackgroundTaskConfig
private tmuxEnabled: boolean
private onSubagentSessionCreated?: OnSubagentSessionCreated
private queuesByKey: Map<string, QueueItem[]> = new Map()
private processingKeys: Set<string> = new Set()
constructor(ctx: PluginInput, config?: BackgroundTaskConfig) {
constructor(
ctx: PluginInput,
config?: BackgroundTaskConfig,
options?: {
tmuxConfig?: TmuxConfig
onSubagentSessionCreated?: OnSubagentSessionCreated
}
) {
this.tasks = new Map()
this.notifications = new Map()
this.pendingByParent = new Map()
@@ -81,6 +98,8 @@ export class BackgroundManager {
this.directory = ctx.directory
this.concurrencyManager = new ConcurrencyManager(config)
this.config = config
this.tmuxEnabled = options?.tmuxConfig?.enabled ?? false
this.onSubagentSessionCreated = options?.onSubagentSessionCreated
this.registerProcessCleanup()
}
@@ -205,7 +224,10 @@ export class BackgroundManager {
body: {
parentID: input.parentSessionID,
title: `Background: ${input.description}`,
},
permission: [
{ permission: "question", action: "deny" as const, pattern: "*" },
],
} as any,
query: {
directory: parentDirectory,
},
@@ -222,6 +244,29 @@ export class BackgroundManager {
const sessionID = createResult.data.id
subagentSessions.add(sessionID)
log("[background-agent] tmux callback check", {
hasCallback: !!this.onSubagentSessionCreated,
tmuxEnabled: this.tmuxEnabled,
isInsideTmux: isInsideTmux(),
sessionID,
parentID: input.parentSessionID,
})
if (this.onSubagentSessionCreated && this.tmuxEnabled && isInsideTmux()) {
log("[background-agent] Invoking tmux callback NOW", { sessionID })
await this.onSubagentSessionCreated({
sessionID,
parentID: input.parentSessionID,
title: input.description,
}).catch((err) => {
log("[background-agent] Failed to spawn tmux pane:", err)
})
log("[background-agent] tmux callback completed, waiting 200ms")
await new Promise(r => setTimeout(r, 200))
} else {
log("[background-agent] SKIP tmux callback - conditions not met")
}
// Update task to running state
task.status = "running"
task.startedAt = new Date()
@@ -252,17 +297,26 @@ export class BackgroundManager {
// Use prompt() instead of promptAsync() to properly initialize agent loop (fire-and-forget)
// Include model if caller provided one (e.g., from Sisyphus category configs)
// IMPORTANT: variant must be a top-level field in the body, NOT nested inside model
// OpenCode's PromptInput schema expects: { model: { providerID, modelID }, variant: "max" }
const launchModel = input.model
? { providerID: input.model.providerID, modelID: input.model.modelID }
: undefined
const launchVariant = input.model?.variant
this.client.session.prompt({
path: { id: sessionID },
body: {
agent: input.agent,
...(input.model ? { model: input.model } : {}),
...(launchModel ? { model: launchModel } : {}),
...(launchVariant ? { variant: launchVariant } : {}),
system: input.skillContent,
tools: {
...getAgentToolRestrictions(input.agent),
task: false,
delegate_task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: input.prompt }],
},
@@ -499,16 +553,24 @@ export class BackgroundManager {
// Use prompt() instead of promptAsync() to properly initialize agent loop
// Include model if task has one (preserved from original launch with category config)
// variant must be top-level in body, not nested inside model (OpenCode PromptInput schema)
const resumeModel = existingTask.model
? { providerID: existingTask.model.providerID, modelID: existingTask.model.modelID }
: undefined
const resumeVariant = existingTask.model?.variant
this.client.session.prompt({
path: { id: existingTask.sessionID },
body: {
agent: existingTask.agent,
...(existingTask.model ? { model: existingTask.model } : {}),
...(resumeModel ? { model: resumeModel } : {}),
...(resumeVariant ? { variant: resumeVariant } : {}),
tools: {
...getAgentToolRestrictions(existingTask.agent),
task: false,
delegate_task: false,
call_omo_agent: true,
question: false,
},
parts: [{ type: "text", text: input.prompt }],
},

View File

@@ -55,7 +55,7 @@ ${REFACTOR_TEMPLATE}
},
"start-work": {
description: "(builtin) Start Sisyphus work session from Prometheus plan",
agent: "Atlas",
agent: "atlas",
template: `<command-instruction>
${START_WORK_TEMPLATE}
</command-instruction>
@@ -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
}
}

View File

@@ -275,8 +275,8 @@ For each generated file:
Mode: {update | create-new}
Files:
./AGENTS.md (root, {N} lines)
./src/hooks/AGENTS.md ({N} lines)
[OK] ./AGENTS.md (root, {N} lines)
[OK] ./src/hooks/AGENTS.md ({N} lines)
Dirs Analyzed: {N}
AGENTS.md Created: {N}

View File

@@ -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

View File

@@ -25,13 +25,13 @@ export const START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
}
\`\`\`
5. **Read the plan file** and start executing tasks according to Orchestrator Sisyphus workflow
5. **Read the plan file** and start executing tasks according to atlas workflow
## OUTPUT FORMAT
When listing plans for selection:
\`\`\`
📋 Available Work Plans
Available Work Plans
Current Time: {ISO timestamp}
Session ID: {current session id}
@@ -44,7 +44,7 @@ Which plan would you like to work on? (Enter number or plan name)
When resuming existing work:
\`\`\`
🔄 Resuming Work Session
Resuming Work Session
Active Plan: {plan-name}
Progress: {completed}/{total} tasks
@@ -55,7 +55,7 @@ Reading plan and continuing from last incomplete task...
When auto-selecting single plan:
\`\`\`
🚀 Starting Work Session
Starting Work Session
Plan: {plan-name}
Session ID: {session_id}
@@ -69,4 +69,4 @@ Reading plan and beginning execution...
- The session_id is injected by the hook - use it directly
- Always update boulder.json BEFORE starting work
- Read the FULL plan file before delegating any tasks
- Follow Orchestrator Sisyphus delegation protocols (7-section format)`
- Follow atlas delegation protocols (7-section format)`

View File

@@ -0,0 +1,336 @@
---
name: agent-browser
description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages.
---
# Browser Automation with agent-browser
## Quick start
```bash
agent-browser open <url> # Navigate to page
agent-browser snapshot -i # Get interactive elements with refs
agent-browser click @e1 # Click element by ref
agent-browser fill @e2 "text" # Fill input by ref
agent-browser close # Close browser
```
## Core workflow
1. Navigate: `agent-browser open <url>`
2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`)
3. Interact using refs from the snapshot
4. Re-snapshot after navigation or significant DOM changes
## Commands
### Navigation
```bash
agent-browser open <url> # Navigate to URL
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser close # Close browser
```
### Snapshot (page analysis)
```bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact output
agent-browser snapshot -d 3 # Limit depth to 3
agent-browser snapshot -s "#main" # Scope to CSS selector
```
### Interactions (use @refs from snapshot)
```bash
agent-browser click @e1 # Click
agent-browser dblclick @e1 # Double-click
agent-browser focus @e1 # Focus element
agent-browser fill @e2 "text" # Clear and type
agent-browser type @e2 "text" # Type without clearing
agent-browser press Enter # Press key
agent-browser press Control+a # Key combination
agent-browser keydown Shift # Hold key down
agent-browser keyup Shift # Release key
agent-browser hover @e1 # Hover
agent-browser check @e1 # Check checkbox
agent-browser uncheck @e1 # Uncheck checkbox
agent-browser select @e1 "value" # Select dropdown
agent-browser scroll down 500 # Scroll page
agent-browser scrollintoview @e1 # Scroll element into view
agent-browser drag @e1 @e2 # Drag and drop
agent-browser upload @e1 file.pdf # Upload files
```
### Get information
```bash
agent-browser get text @e1 # Get element text
agent-browser get html @e1 # Get innerHTML
agent-browser get value @e1 # Get input value
agent-browser get attr @e1 href # Get attribute
agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get count ".item" # Count matching elements
agent-browser get box @e1 # Get bounding box
```
### Check state
```bash
agent-browser is visible @e1 # Check if visible
agent-browser is enabled @e1 # Check if enabled
agent-browser is checked @e1 # Check if checked
```
### Screenshots & PDF
```bash
agent-browser screenshot # Screenshot to stdout
agent-browser screenshot path.png # Save to file
agent-browser screenshot --full # Full page
agent-browser pdf output.pdf # Save as PDF
```
### Video recording
```bash
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
agent-browser click @e1 # Perform actions
agent-browser record stop # Stop and save video
agent-browser record restart ./take2.webm # Stop current + start new recording
```
Recording creates a fresh context but preserves cookies/storage from your session.
### Wait
```bash
agent-browser wait @e1 # Wait for element
agent-browser wait 2000 # Wait milliseconds
agent-browser wait --text "Success" # Wait for text
agent-browser wait --url "**/dashboard" # Wait for URL pattern
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --fn "window.ready" # Wait for JS condition
```
### Mouse control
```bash
agent-browser mouse move 100 200 # Move mouse
agent-browser mouse down left # Press button
agent-browser mouse up left # Release button
agent-browser mouse wheel 100 # Scroll wheel
```
### Semantic locators (alternative to refs)
```bash
agent-browser find role button click --name "Submit"
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find first ".item" click
agent-browser find nth 2 "a" text
```
### Browser settings
```bash
agent-browser set viewport 1920 1080 # Set viewport size
agent-browser set device "iPhone 14" # Emulate device
agent-browser set geo 37.7749 -122.4194 # Set geolocation
agent-browser set offline on # Toggle offline mode
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
agent-browser set credentials user pass # HTTP basic auth
agent-browser set media dark # Emulate color scheme
```
### Cookies & Storage
```bash
agent-browser cookies # Get all cookies
agent-browser cookies set name value # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local key # Get specific key
agent-browser storage local set k v # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Get all sessionStorage
agent-browser storage session key # Get specific key
agent-browser storage session set k v # Set value
agent-browser storage session clear # Clear all
```
### Network
```bash
agent-browser network route <url> # Intercept requests
agent-browser network route <url> --abort # Block requests
agent-browser network route <url> --body '{}' # Mock response
agent-browser network unroute [url] # Remove routes
agent-browser network requests # View tracked requests
agent-browser network requests --filter api # Filter requests
```
### Tabs & Windows
```bash
agent-browser tab # List tabs
agent-browser tab new [url] # New tab
agent-browser tab 2 # Switch to tab
agent-browser tab close # Close tab
agent-browser window new # New window
```
### Frames
```bash
agent-browser frame "#iframe" # Switch to iframe
agent-browser frame main # Back to main frame
```
### Dialogs
```bash
agent-browser dialog accept [text] # Accept dialog
agent-browser dialog dismiss # Dismiss dialog
```
### JavaScript
```bash
agent-browser eval "document.title" # Run JavaScript
```
## Global Options
| Option | Description |
|--------|-------------|
| `--session <name>` | Isolated browser session (`AGENT_BROWSER_SESSION` env) |
| `--profile <path>` | Persistent browser profile (`AGENT_BROWSER_PROFILE` env) |
| `--headers <json>` | HTTP headers scoped to URL's origin |
| `--executable-path <path>` | Custom browser binary (`AGENT_BROWSER_EXECUTABLE_PATH` env) |
| `--args <args>` | Browser launch args (`AGENT_BROWSER_ARGS` env) |
| `--user-agent <ua>` | Custom User-Agent (`AGENT_BROWSER_USER_AGENT` env) |
| `--proxy <url>` | Proxy server (`AGENT_BROWSER_PROXY` env) |
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (`AGENT_BROWSER_PROXY_BYPASS` env) |
| `-p, --provider <name>` | Cloud browser provider (`AGENT_BROWSER_PROVIDER` env) |
| `--json` | Machine-readable JSON output |
| `--headed` | Show browser window (not headless) |
| `--cdp <port\|wss://url>` | Connect via Chrome DevTools Protocol |
| `--debug` | Debug output |
## Example: Form submission
```bash
agent-browser open https://example.com/form
agent-browser snapshot -i
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3
agent-browser wait --load networkidle
agent-browser snapshot -i # Check result
```
## Example: Authentication with saved state
```bash
# Login once
agent-browser open https://app.example.com/login
agent-browser snapshot -i
agent-browser fill @e1 "username"
agent-browser fill @e2 "password"
agent-browser click @e3
agent-browser wait --url "**/dashboard"
agent-browser state save auth.json
# Later sessions: load saved state
agent-browser state load auth.json
agent-browser open https://app.example.com/dashboard
```
### Header-based Auth (Skip login flows)
```bash
# Headers scoped to api.example.com only
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
# Navigate to another domain - headers NOT sent (safe)
agent-browser open other-site.com
# Global headers (all domains)
agent-browser set headers '{"X-Custom-Header": "value"}'
```
## Sessions & Persistent Profiles
### Sessions (parallel browsers)
```bash
agent-browser --session test1 open site-a.com
agent-browser --session test2 open site-b.com
agent-browser session list
```
### Persistent Profiles
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
```bash
agent-browser --profile ~/.myapp-profile open myapp.com
# Or via env var
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
```
- Use different profile paths for different projects
- Login once → restart browser → still logged in
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
## JSON output (for parsing)
Add `--json` for machine-readable output:
```bash
agent-browser snapshot -i --json
agent-browser get text @e1 --json
```
## Debugging
```bash
agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages
agent-browser errors # View page errors
agent-browser record start ./debug.webm # Record from current page
agent-browser record stop # Save recording
agent-browser connect 9222 # Local CDP port
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
agent-browser console --clear # Clear console
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
```
---
## Installation
### Step 1: Install agent-browser CLI
```bash
bun add -g agent-browser
```
### Step 2: Install Playwright browsers
**IMPORTANT**: `agent-browser install` may fail on some platforms (e.g., darwin-arm64) with "No binary found" error. In that case, install Playwright browsers directly:
```bash
# Create a temp project and install playwright
cd /tmp && bun init -y && bun add playwright
# Install Chromium browser
bun playwright install chromium
```
This downloads Chrome for Testing to `~/Library/Caches/ms-playwright/`.
### Verify installation
```bash
agent-browser open https://example.com --headed
```
If the browser opens successfully, installation is complete.
### Troubleshooting
| Error | Solution |
|-------|----------|
| `No binary found for darwin-arm64` | Run `bun playwright install chromium` in a project with playwright dependency |
| `Executable doesn't exist at .../chromium-XXXX` | Re-run `bun playwright install chromium` |
| Browser doesn't open | Ensure `--headed` flag is used for visible browser |
---
Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser

View File

@@ -0,0 +1,213 @@
---
name: dev-browser
description: Browser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include "go to [url]", "click on", "fill out the form", "take a screenshot", "scrape", "automate", "test the website", "log into", or any browser interaction request.
---
# Dev Browser Skill
Browser automation that maintains page state across script executions. Write small, focused scripts to accomplish tasks incrementally. Once you've proven out part of a workflow and there is repeated work to be done, you can write a script to do the repeated work in a single execution.
## Choosing Your Approach
- **Local/source-available sites**: Read the source code first to write selectors directly
- **Unknown page layouts**: Use `getAISnapshot()` to discover elements and `selectSnapshotRef()` to interact with them
- **Visual feedback**: Take screenshots to see what the user sees
## Setup
> **Installation**: See [references/installation.md](references/installation.md) for detailed setup instructions including Windows support.
Two modes available. Ask the user if unclear which to use.
### Standalone Mode (Default)
Launches a new Chromium browser for fresh automation sessions.
```bash
./skills/dev-browser/server.sh &
```
Add `--headless` flag if user requests it. **Wait for the `Ready` message before running scripts.**
### Extension Mode
Connects to user's existing Chrome browser. Use this when:
- The user is already logged into sites and wants you to do things behind an authed experience that isn't local dev.
- The user asks you to use the extension
**Important**: The core flow is still the same. You create named pages inside of their browser.
**Start the relay server:**
```bash
cd skills/dev-browser && npm i && npm run start-extension &
```
Wait for `Waiting for extension to connect...` followed by `Extension connected` in the console. To know that a client has connected and the browser is ready to be controlled.
**Workflow:**
1. Scripts call `client.page("name")` just like the normal mode to create new pages / connect to existing ones.
2. Automation runs on the user's actual browser session
If the extension hasn't connected yet, tell the user to launch and activate it. Download link: https://github.com/SawyerHood/dev-browser/releases
## Writing Scripts
> **Run all scripts from `skills/dev-browser/` directory.** The `@/` import alias requires this directory's config.
Execute scripts inline using heredocs:
```bash
cd skills/dev-browser && npx tsx <<'EOF'
import { connect, waitForPageLoad } from "@/client.js";
const client = await connect();
// Create page with custom viewport size (optional)
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
await page.goto("https://example.com");
await waitForPageLoad(page);
console.log({ title: await page.title(), url: page.url() });
await client.disconnect();
EOF
```
**Write to `tmp/` files only when** the script needs reuse, is complex, or user explicitly requests it.
### Key Principles
1. **Small scripts**: Each script does ONE thing (navigate, click, fill, check)
2. **Evaluate state**: Log/return state at the end to decide next steps
3. **Descriptive page names**: Use `"checkout"`, `"login"`, not `"main"`
4. **Disconnect to exit**: `await client.disconnect()` - pages persist on server
5. **Plain JS in evaluate**: `page.evaluate()` runs in browser - no TypeScript syntax
## Workflow Loop
Follow this pattern for complex tasks:
1. **Write a script** to perform one action
2. **Run it** and observe the output
3. **Evaluate** - did it work? What's the current state?
4. **Decide** - is the task complete or do we need another script?
5. **Repeat** until task is done
### No TypeScript in Browser Context
Code passed to `page.evaluate()` runs in the browser, which doesn't understand TypeScript:
```typescript
// ✅ Correct: plain JavaScript
const text = await page.evaluate(() => {
return document.body.innerText;
});
// ❌ Wrong: TypeScript syntax will fail at runtime
const text = await page.evaluate(() => {
const el: HTMLElement = document.body; // Type annotation breaks in browser!
return el.innerText;
});
```
## Scraping Data
For scraping large datasets, intercept and replay network requests rather than scrolling the DOM. See [references/scraping.md](references/scraping.md) for the complete guide covering request capture, schema discovery, and paginated API replay.
## Client API
```typescript
const client = await connect();
// Get or create named page (viewport only applies to new pages)
const page = await client.page("name");
const pageWithSize = await client.page("name", { viewport: { width: 1920, height: 1080 } });
const pages = await client.list(); // List all page names
await client.close("name"); // Close a page
await client.disconnect(); // Disconnect (pages persist)
// ARIA Snapshot methods
const snapshot = await client.getAISnapshot("name"); // Get accessibility tree
const element = await client.selectSnapshotRef("name", "e5"); // Get element by ref
```
The `page` object is a standard Playwright Page.
## Waiting
```typescript
import { waitForPageLoad } from "@/client.js";
await waitForPageLoad(page); // After navigation
await page.waitForSelector(".results"); // For specific elements
await page.waitForURL("**/success"); // For specific URL
```
## Inspecting Page State
### Screenshots
```typescript
await page.screenshot({ path: "tmp/screenshot.png" });
await page.screenshot({ path: "tmp/full.png", fullPage: true });
```
### ARIA Snapshot (Element Discovery)
Use `getAISnapshot()` to discover page elements. Returns YAML-formatted accessibility tree:
```yaml
- banner:
- link "Hacker News" [ref=e1]
- navigation:
- link "new" [ref=e2]
- main:
- list:
- listitem:
- link "Article Title" [ref=e8]
- link "328 comments" [ref=e9]
- contentinfo:
- textbox [ref=e10]
- /placeholder: "Search"
```
**Interpreting refs:**
- `[ref=eN]` - Element reference for interaction (visible, clickable elements only)
- `[checked]`, `[disabled]`, `[expanded]` - Element states
- `[level=N]` - Heading level
- `/url:`, `/placeholder:` - Element properties
**Interacting with refs:**
```typescript
const snapshot = await client.getAISnapshot("hackernews");
console.log(snapshot); // Find the ref you need
const element = await client.selectSnapshotRef("hackernews", "e2");
await element.click();
```
## Error Recovery
Page state persists after failures. Debug with:
```bash
cd skills/dev-browser && npx tsx <<'EOF'
import { connect } from "@/client.js";
const client = await connect();
const page = await client.page("hackernews");
await page.screenshot({ path: "tmp/debug.png" });
console.log({
url: page.url(),
title: await page.title(),
bodyText: await page.textContent("body").then((t) => t?.slice(0, 200)),
});
await client.disconnect();
EOF
```

View File

@@ -0,0 +1,193 @@
# Dev Browser Installation Guide
This guide covers installation for all platforms: macOS, Linux, and Windows.
## Prerequisites
- [Node.js](https://nodejs.org) v18 or later with npm
- Git (for cloning the skill)
## Installation
### Step 1: Clone the Skill
```bash
# Clone dev-browser to a temporary location
git clone https://github.com/sawyerhood/dev-browser /tmp/dev-browser-skill
# Copy to skills directory (adjust path as needed)
# For oh-my-opencode: already bundled
# For manual installation:
mkdir -p ~/.config/opencode/skills
cp -r /tmp/dev-browser-skill/skills/dev-browser ~/.config/opencode/skills/dev-browser
# Cleanup
rm -rf /tmp/dev-browser-skill
```
**Windows (PowerShell):**
```powershell
# Clone dev-browser to temp location
git clone https://github.com/sawyerhood/dev-browser $env:TEMP\dev-browser-skill
# Copy to skills directory
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\skills"
Copy-Item -Recurse "$env:TEMP\dev-browser-skill\skills\dev-browser" "$env:USERPROFILE\.config\opencode\skills\dev-browser"
# Cleanup
Remove-Item -Recurse -Force "$env:TEMP\dev-browser-skill"
```
### Step 2: Install Dependencies
```bash
cd ~/.config/opencode/skills/dev-browser
npm install
```
**Windows (PowerShell):**
```powershell
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
npm install
```
### Step 3: Start the Server
#### Standalone Mode (New Browser Instance)
**macOS/Linux:**
```bash
cd ~/.config/opencode/skills/dev-browser
./server.sh &
# Or for headless:
./server.sh --headless &
```
**Windows (PowerShell):**
```powershell
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
Start-Process -NoNewWindow -FilePath "node" -ArgumentList "server.js"
# Or for headless:
Start-Process -NoNewWindow -FilePath "node" -ArgumentList "server.js", "--headless"
```
**Windows (CMD):**
```cmd
cd %USERPROFILE%\.config\opencode\skills\dev-browser
start /B node server.js
```
Wait for the `Ready` message before running scripts.
#### Extension Mode (Use Existing Chrome)
**macOS/Linux:**
```bash
cd ~/.config/opencode/skills/dev-browser
npm run start-extension &
```
**Windows (PowerShell):**
```powershell
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
Start-Process -NoNewWindow -FilePath "npm" -ArgumentList "run", "start-extension"
```
Wait for `Extension connected` message.
## Chrome Extension Setup (Optional)
The Chrome extension allows controlling your existing Chrome browser with all your logged-in sessions.
### Installation
1. Download `extension.zip` from [latest release](https://github.com/sawyerhood/dev-browser/releases/latest)
2. Extract to a permanent location:
- **macOS/Linux:** `~/.dev-browser-extension`
- **Windows:** `%USERPROFILE%\.dev-browser-extension`
3. Open Chrome → `chrome://extensions`
4. Enable "Developer mode" (toggle in top right)
5. Click "Load unpacked" → select the extracted folder
### Usage
1. Click the Dev Browser extension icon in Chrome toolbar
2. Toggle to "Active"
3. Start the extension relay server (see above)
4. Use dev-browser scripts - they'll control your existing Chrome
## Troubleshooting
### Server Won't Start
**Check Node.js version:**
```bash
node --version # Should be v18+
```
**Check port availability:**
```bash
# macOS/Linux
lsof -i :3000
# Windows
netstat -ano | findstr :3000
```
### Playwright Installation Issues
If Chromium fails to install:
```bash
npx playwright install chromium
```
### Windows-Specific Issues
**Execution Policy:**
If PowerShell scripts are blocked:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
**Path Issues:**
Use forward slashes or escaped backslashes in paths:
```powershell
# Good
cd "$env:USERPROFILE/.config/opencode/skills/dev-browser"
# Also good
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
```
### Extension Not Connecting
1. Ensure extension is "Active" (click icon to toggle)
2. Check relay server is running (`npm run start-extension`)
3. Look for `Extension connected` message in console
4. Try reloading the extension in `chrome://extensions`
## Permissions
To skip permission prompts in Claude Code, add to `~/.claude/settings.json`:
```json
{
"permissions": {
"allow": ["Skill(dev-browser:dev-browser)", "Bash(npx tsx:*)"]
}
}
```
## Updating
```bash
cd ~/.config/opencode/skills/dev-browser
git pull
npm install
```
**Windows:**
```powershell
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
git pull
npm install
```

View File

@@ -0,0 +1,155 @@
# Data Scraping Guide
For large datasets (followers, posts, search results), **intercept and replay network requests** rather than scrolling and parsing the DOM. This is faster, more reliable, and handles pagination automatically.
## Why Not Scroll?
Scrolling is slow, unreliable, and wastes time. APIs return structured data with pagination built in. Always prefer API replay.
## Start Small, Then Scale
**Don't try to automate everything at once.** Work incrementally:
1. **Capture one request** - verify you're intercepting the right endpoint
2. **Inspect one response** - understand the schema before writing extraction code
3. **Extract a few items** - make sure your parsing logic works
4. **Then scale up** - add pagination loop only after the basics work
This prevents wasting time debugging a complex script when the issue is a simple path like `data.user.timeline` vs `data.user.result.timeline`.
## Step-by-Step Workflow
### 1. Capture Request Details
First, intercept a request to understand URL structure and required headers:
```typescript
import { connect, waitForPageLoad } from "@/client.js";
import * as fs from "node:fs";
const client = await connect();
const page = await client.page("site");
let capturedRequest = null;
page.on("request", (request) => {
const url = request.url();
// Look for API endpoints (adjust pattern for your target site)
if (url.includes("/api/") || url.includes("/graphql/")) {
capturedRequest = {
url: url,
headers: request.headers(),
method: request.method(),
};
fs.writeFileSync("tmp/request-details.json", JSON.stringify(capturedRequest, null, 2));
console.log("Captured request:", url.substring(0, 80) + "...");
}
});
await page.goto("https://example.com/profile");
await waitForPageLoad(page);
await page.waitForTimeout(3000);
await client.disconnect();
```
### 2. Capture Response to Understand Schema
Save a raw response to inspect the data structure:
```typescript
page.on("response", async (response) => {
const url = response.url();
if (url.includes("UserTweets") || url.includes("/api/data")) {
const json = await response.json();
fs.writeFileSync("tmp/api-response.json", JSON.stringify(json, null, 2));
console.log("Captured response");
}
});
```
Then analyze the structure to find:
- Where the data array lives (e.g., `data.user.result.timeline.instructions[].entries`)
- Where pagination cursors are (e.g., `cursor-bottom` entries)
- What fields you need to extract
### 3. Replay API with Pagination
Once you understand the schema, replay requests directly:
```typescript
import { connect } from "@/client.js";
import * as fs from "node:fs";
const client = await connect();
const page = await client.page("site");
const results = new Map(); // Use Map for deduplication
const headers = JSON.parse(fs.readFileSync("tmp/request-details.json", "utf8")).headers;
const baseUrl = "https://example.com/api/data";
let cursor = null;
let hasMore = true;
while (hasMore) {
// Build URL with pagination cursor
const params = { count: 20 };
if (cursor) params.cursor = cursor;
const url = `${baseUrl}?params=${encodeURIComponent(JSON.stringify(params))}`;
// Execute fetch in browser context (has auth cookies/headers)
const response = await page.evaluate(
async ({ url, headers }) => {
const res = await fetch(url, { headers });
return res.json();
},
{ url, headers }
);
// Extract data and cursor (adjust paths for your API)
const entries = response?.data?.entries || [];
for (const entry of entries) {
if (entry.type === "cursor-bottom") {
cursor = entry.value;
} else if (entry.id && !results.has(entry.id)) {
results.set(entry.id, {
id: entry.id,
text: entry.content,
timestamp: entry.created_at,
});
}
}
console.log(`Fetched page, total: ${results.size}`);
// Check stop conditions
if (!cursor || entries.length === 0) hasMore = false;
// Rate limiting - be respectful
await new Promise((r) => setTimeout(r, 500));
}
// Export results
const data = Array.from(results.values());
fs.writeFileSync("tmp/results.json", JSON.stringify(data, null, 2));
console.log(`Saved ${data.length} items`);
await client.disconnect();
```
## Key Patterns
| Pattern | Description |
| ----------------------- | ------------------------------------------------------ |
| `page.on('request')` | Capture outgoing request URL + headers |
| `page.on('response')` | Capture response data to understand schema |
| `page.evaluate(fetch)` | Replay requests in browser context (inherits auth) |
| `Map` for deduplication | APIs often return overlapping data across pages |
| Cursor-based pagination | Look for `cursor`, `next_token`, `offset` in responses |
## Tips
- **Extension mode**: `page.context().cookies()` doesn't work - capture auth headers from intercepted requests instead
- **Rate limiting**: Add 500ms+ delays between requests to avoid blocks
- **Stop conditions**: Check for empty results, missing cursor, or reaching a date/ID threshold
- **GraphQL APIs**: URL params often include `variables` and `features` JSON objects - capture and reuse them

View File

@@ -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

View File

@@ -1,2 +1,2 @@
export * from "./types"
export { createBuiltinSkills } from "./skills"
export { createBuiltinSkills, type CreateBuiltinSkillsOptions } from "./skills"

View File

@@ -0,0 +1,89 @@
import { describe, test, expect } from "bun:test"
import { createBuiltinSkills } from "./skills"
describe("createBuiltinSkills", () => {
test("returns playwright skill by default", () => {
// #given - no options (default)
// #when
const skills = createBuiltinSkills()
// #then
const browserSkill = skills.find((s) => s.name === "playwright")
expect(browserSkill).toBeDefined()
expect(browserSkill!.description).toContain("browser")
expect(browserSkill!.mcpConfig).toHaveProperty("playwright")
})
test("returns playwright skill when browserProvider is 'playwright'", () => {
// #given
const options = { browserProvider: "playwright" as const }
// #when
const skills = createBuiltinSkills(options)
// #then
const playwrightSkill = skills.find((s) => s.name === "playwright")
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
expect(playwrightSkill).toBeDefined()
expect(agentBrowserSkill).toBeUndefined()
})
test("returns agent-browser skill when browserProvider is 'agent-browser'", () => {
// #given
const options = { browserProvider: "agent-browser" as const }
// #when
const skills = createBuiltinSkills(options)
// #then
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
const playwrightSkill = skills.find((s) => s.name === "playwright")
expect(agentBrowserSkill).toBeDefined()
expect(agentBrowserSkill!.description).toContain("browser")
expect(agentBrowserSkill!.allowedTools).toContain("Bash(agent-browser:*)")
expect(agentBrowserSkill!.template).toContain("agent-browser")
expect(playwrightSkill).toBeUndefined()
})
test("agent-browser skill template is inlined (not loaded from file)", () => {
// #given
const options = { browserProvider: "agent-browser" as const }
// #when
const skills = createBuiltinSkills(options)
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
// #then - template should contain substantial content (inlined, not fallback)
expect(agentBrowserSkill!.template).toContain("## Quick start")
expect(agentBrowserSkill!.template).toContain("## Commands")
expect(agentBrowserSkill!.template).toContain("agent-browser open")
expect(agentBrowserSkill!.template).toContain("agent-browser snapshot")
})
test("always includes frontend-ui-ux and git-master skills", () => {
// #given - both provider options
// #when
const defaultSkills = createBuiltinSkills()
const agentBrowserSkills = createBuiltinSkills({ browserProvider: "agent-browser" })
// #then
for (const skills of [defaultSkills, agentBrowserSkills]) {
expect(skills.find((s) => s.name === "frontend-ui-ux")).toBeDefined()
expect(skills.find((s) => s.name === "git-master")).toBeDefined()
}
})
test("returns exactly 4 skills regardless of provider", () => {
// #given
// #when
const defaultSkills = createBuiltinSkills()
const agentBrowserSkills = createBuiltinSkills({ browserProvider: "agent-browser" })
// #then
expect(defaultSkills).toHaveLength(4)
expect(agentBrowserSkills).toHaveLength(4)
})
})

View File

@@ -1,4 +1,5 @@
import type { BuiltinSkill } from "./types"
import type { BrowserAutomationProvider } from "../../config/schema"
const playwrightSkill: BuiltinSkill = {
name: "playwright",
@@ -14,6 +15,303 @@ This skill provides browser automation capabilities via the Playwright MCP serve
},
}
const agentBrowserSkill: BuiltinSkill = {
name: "agent-browser",
description: "MUST USE for any browser-related tasks. Browser automation via agent-browser CLI - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.",
template: `# Browser Automation with agent-browser
## Quick start
\`\`\`bash
agent-browser open <url> # Navigate to page
agent-browser snapshot -i # Get interactive elements with refs
agent-browser click @e1 # Click element by ref
agent-browser fill @e2 "text" # Fill input by ref
agent-browser close # Close browser
\`\`\`
## Core workflow
1. Navigate: \`agent-browser open <url>\`
2. Snapshot: \`agent-browser snapshot -i\` (returns elements with refs like \`@e1\`, \`@e2\`)
3. Interact using refs from the snapshot
4. Re-snapshot after navigation or significant DOM changes
## Commands
### Navigation
\`\`\`bash
agent-browser open <url> # Navigate to URL
agent-browser back # Go back
agent-browser forward # Go forward
agent-browser reload # Reload page
agent-browser close # Close browser
\`\`\`
### Snapshot (page analysis)
\`\`\`bash
agent-browser snapshot # Full accessibility tree
agent-browser snapshot -i # Interactive elements only (recommended)
agent-browser snapshot -c # Compact output
agent-browser snapshot -d 3 # Limit depth to 3
agent-browser snapshot -s "#main" # Scope to CSS selector
\`\`\`
### Interactions (use @refs from snapshot)
\`\`\`bash
agent-browser click @e1 # Click
agent-browser dblclick @e1 # Double-click
agent-browser focus @e1 # Focus element
agent-browser fill @e2 "text" # Clear and type
agent-browser type @e2 "text" # Type without clearing
agent-browser press Enter # Press key
agent-browser press Control+a # Key combination
agent-browser keydown Shift # Hold key down
agent-browser keyup Shift # Release key
agent-browser hover @e1 # Hover
agent-browser check @e1 # Check checkbox
agent-browser uncheck @e1 # Uncheck checkbox
agent-browser select @e1 "value" # Select dropdown
agent-browser scroll down 500 # Scroll page
agent-browser scrollintoview @e1 # Scroll element into view
agent-browser drag @e1 @e2 # Drag and drop
agent-browser upload @e1 file.pdf # Upload files
\`\`\`
### Get information
\`\`\`bash
agent-browser get text @e1 # Get element text
agent-browser get html @e1 # Get innerHTML
agent-browser get value @e1 # Get input value
agent-browser get attr @e1 href # Get attribute
agent-browser get title # Get page title
agent-browser get url # Get current URL
agent-browser get count ".item" # Count matching elements
agent-browser get box @e1 # Get bounding box
\`\`\`
### Check state
\`\`\`bash
agent-browser is visible @e1 # Check if visible
agent-browser is enabled @e1 # Check if enabled
agent-browser is checked @e1 # Check if checked
\`\`\`
### Screenshots & PDF
\`\`\`bash
agent-browser screenshot # Screenshot to stdout
agent-browser screenshot path.png # Save to file
agent-browser screenshot --full # Full page
agent-browser pdf output.pdf # Save as PDF
\`\`\`
### Video recording
\`\`\`bash
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
agent-browser click @e1 # Perform actions
agent-browser record stop # Stop and save video
agent-browser record restart ./take2.webm # Stop current + start new recording
\`\`\`
Recording creates a fresh context but preserves cookies/storage from your session.
### Wait
\`\`\`bash
agent-browser wait @e1 # Wait for element
agent-browser wait 2000 # Wait milliseconds
agent-browser wait --text "Success" # Wait for text
agent-browser wait --url "**/dashboard" # Wait for URL pattern
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --fn "window.ready" # Wait for JS condition
\`\`\`
### Mouse control
\`\`\`bash
agent-browser mouse move 100 200 # Move mouse
agent-browser mouse down left # Press button
agent-browser mouse up left # Release button
agent-browser mouse wheel 100 # Scroll wheel
\`\`\`
### Semantic locators (alternative to refs)
\`\`\`bash
agent-browser find role button click --name "Submit"
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find first ".item" click
agent-browser find nth 2 "a" text
\`\`\`
### Browser settings
\`\`\`bash
agent-browser set viewport 1920 1080 # Set viewport size
agent-browser set device "iPhone 14" # Emulate device
agent-browser set geo 37.7749 -122.4194 # Set geolocation
agent-browser set offline on # Toggle offline mode
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
agent-browser set credentials user pass # HTTP basic auth
agent-browser set media dark # Emulate color scheme
\`\`\`
### Cookies & Storage
\`\`\`bash
agent-browser cookies # Get all cookies
agent-browser cookies set name value # Set cookie
agent-browser cookies clear # Clear cookies
agent-browser storage local # Get all localStorage
agent-browser storage local key # Get specific key
agent-browser storage local set k v # Set value
agent-browser storage local clear # Clear all
agent-browser storage session # Get all sessionStorage
agent-browser storage session key # Get specific key
agent-browser storage session set k v # Set value
agent-browser storage session clear # Clear all
\`\`\`
### Network
\`\`\`bash
agent-browser network route <url> # Intercept requests
agent-browser network route <url> --abort # Block requests
agent-browser network route <url> --body '{}' # Mock response
agent-browser network unroute [url] # Remove routes
agent-browser network requests # View tracked requests
agent-browser network requests --filter api # Filter requests
\`\`\`
### Tabs & Windows
\`\`\`bash
agent-browser tab # List tabs
agent-browser tab new [url] # New tab
agent-browser tab 2 # Switch to tab
agent-browser tab close # Close tab
agent-browser window new # New window
\`\`\`
### Frames
\`\`\`bash
agent-browser frame "#iframe" # Switch to iframe
agent-browser frame main # Back to main frame
\`\`\`
### Dialogs
\`\`\`bash
agent-browser dialog accept [text] # Accept dialog
agent-browser dialog dismiss # Dismiss dialog
\`\`\`
### JavaScript
\`\`\`bash
agent-browser eval "document.title" # Run JavaScript
\`\`\`
## Global Options
| Option | Description |
|--------|-------------|
| \`--session <name>\` | Isolated browser session (\`AGENT_BROWSER_SESSION\` env) |
| \`--profile <path>\` | Persistent browser profile (\`AGENT_BROWSER_PROFILE\` env) |
| \`--headers <json>\` | HTTP headers scoped to URL's origin |
| \`--executable-path <path>\` | Custom browser binary (\`AGENT_BROWSER_EXECUTABLE_PATH\` env) |
| \`--args <args>\` | Browser launch args (\`AGENT_BROWSER_ARGS\` env) |
| \`--user-agent <ua>\` | Custom User-Agent (\`AGENT_BROWSER_USER_AGENT\` env) |
| \`--proxy <url>\` | Proxy server (\`AGENT_BROWSER_PROXY\` env) |
| \`--proxy-bypass <hosts>\` | Hosts to bypass proxy (\`AGENT_BROWSER_PROXY_BYPASS\` env) |
| \`-p, --provider <name>\` | Cloud browser provider (\`AGENT_BROWSER_PROVIDER\` env) |
| \`--json\` | Machine-readable JSON output |
| \`--headed\` | Show browser window (not headless) |
| \`--cdp <port\\|wss://url>\` | Connect via Chrome DevTools Protocol |
| \`--debug\` | Debug output |
## Example: Form submission
\`\`\`bash
agent-browser open https://example.com/form
agent-browser snapshot -i
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3
agent-browser wait --load networkidle
agent-browser snapshot -i # Check result
\`\`\`
## Example: Authentication with saved state
\`\`\`bash
# Login once
agent-browser open https://app.example.com/login
agent-browser snapshot -i
agent-browser fill @e1 "username"
agent-browser fill @e2 "password"
agent-browser click @e3
agent-browser wait --url "**/dashboard"
agent-browser state save auth.json
# Later sessions: load saved state
agent-browser state load auth.json
agent-browser open https://app.example.com/dashboard
\`\`\`
### Header-based Auth (Skip login flows)
\`\`\`bash
# Headers scoped to api.example.com only
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
# Navigate to another domain - headers NOT sent (safe)
agent-browser open other-site.com
# Global headers (all domains)
agent-browser set headers '{"X-Custom-Header": "value"}'
\`\`\`
## Sessions & Persistent Profiles
### Sessions (parallel browsers)
\`\`\`bash
agent-browser --session test1 open site-a.com
agent-browser --session test2 open site-b.com
agent-browser session list
\`\`\`
### Persistent Profiles
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
\`\`\`bash
agent-browser --profile ~/.myapp-profile open myapp.com
# Or via env var
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
\`\`\`
- Use different profile paths for different projects
- Login once → restart browser → still logged in
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
## JSON output (for parsing)
Add \`--json\` for machine-readable output:
\`\`\`bash
agent-browser snapshot -i --json
agent-browser get text @e1 --json
\`\`\`
## Debugging
\`\`\`bash
agent-browser open example.com --headed # Show browser window
agent-browser console # View console messages
agent-browser errors # View page errors
agent-browser record start ./debug.webm # Record from current page
agent-browser record stop # Save recording
agent-browser connect 9222 # Local CDP port
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
agent-browser console --clear # Clear console
agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
\`\`\`
---
Install: \`bun add -g agent-browser && agent-browser install\`. Run \`agent-browser --help\` for all commands. Repo: https://github.com/vercel-labs/agent-browser`,
allowedTools: ["Bash(agent-browser:*)"],
}
const frontendUiUxSkill: BuiltinSkill = {
name: "frontend-ui-ux",
description: "Designer-turned-developer who crafts stunning UI/UX even without design mockups",
@@ -95,7 +393,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:
@@ -1198,6 +1496,234 @@ POTENTIAL ACTIONS:
- Bisect without proper good/bad boundaries -> Wasted time`,
}
export function createBuiltinSkills(): BuiltinSkill[] {
return [playwrightSkill, frontendUiUxSkill, gitMasterSkill]
const devBrowserSkill: BuiltinSkill = {
name: "dev-browser",
description:
"Browser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include 'go to [url]', 'click on', 'fill out the form', 'take a screenshot', 'scrape', 'automate', 'test the website', 'log into', or any browser interaction request.",
template: `# Dev Browser Skill
Browser automation that maintains page state across script executions. Write small, focused scripts to accomplish tasks incrementally. Once you've proven out part of a workflow and there is repeated work to be done, you can write a script to do the repeated work in a single execution.
## Choosing Your Approach
- **Local/source-available sites**: Read the source code first to write selectors directly
- **Unknown page layouts**: Use \`getAISnapshot()\` to discover elements and \`selectSnapshotRef()\` to interact with them
- **Visual feedback**: Take screenshots to see what the user sees
## Setup
**IMPORTANT**: Before using this skill, ensure the server is running. See [references/installation.md](references/installation.md) for platform-specific setup instructions (macOS, Linux, Windows).
Two modes available. Ask the user if unclear which to use.
### Standalone Mode (Default)
Launches a new Chromium browser for fresh automation sessions.
**macOS/Linux:**
\`\`\`bash
./skills/dev-browser/server.sh &
\`\`\`
**Windows (PowerShell):**
\`\`\`powershell
Start-Process -NoNewWindow -FilePath "node" -ArgumentList "skills/dev-browser/server.js"
\`\`\`
Add \`--headless\` flag if user requests it. **Wait for the \`Ready\` message before running scripts.**
### Extension Mode
Connects to user's existing Chrome browser. Use this when:
- The user is already logged into sites and wants you to do things behind an authed experience that isn't local dev.
- The user asks you to use the extension
**Important**: The core flow is still the same. You create named pages inside of their browser.
**Start the relay server:**
**macOS/Linux:**
\`\`\`bash
cd skills/dev-browser && npm i && npm run start-extension &
\`\`\`
**Windows (PowerShell):**
\`\`\`powershell
cd skills/dev-browser; npm i; Start-Process -NoNewWindow -FilePath "npm" -ArgumentList "run", "start-extension"
\`\`\`
Wait for \`Waiting for extension to connect...\` followed by \`Extension connected\` in the console.
If the extension hasn't connected yet, tell the user to launch and activate it. Download link: https://github.com/SawyerHood/dev-browser/releases
## Writing Scripts
> **Run all scripts from \`skills/dev-browser/\` directory.** The \`@/\` import alias requires this directory's config.
Execute scripts inline using heredocs:
**macOS/Linux:**
\`\`\`bash
cd skills/dev-browser && npx tsx <<'EOF'
import { connect, waitForPageLoad } from "@/client.js";
const client = await connect();
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
await page.goto("https://example.com");
await waitForPageLoad(page);
console.log({ title: await page.title(), url: page.url() });
await client.disconnect();
EOF
\`\`\`
**Windows (PowerShell):**
\`\`\`powershell
cd skills/dev-browser
@"
import { connect, waitForPageLoad } from "@/client.js";
const client = await connect();
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
await page.goto("https://example.com");
await waitForPageLoad(page);
console.log({ title: await page.title(), url: page.url() });
await client.disconnect();
"@ | npx tsx --input-type=module
\`\`\`
### Key Principles
1. **Small scripts**: Each script does ONE thing (navigate, click, fill, check)
2. **Evaluate state**: Log/return state at the end to decide next steps
3. **Descriptive page names**: Use \`"checkout"\`, \`"login"\`, not \`"main"\`
4. **Disconnect to exit**: \`await client.disconnect()\` - pages persist on server
5. **Plain JS in evaluate**: \`page.evaluate()\` runs in browser - no TypeScript syntax
## Workflow Loop
1. **Write a script** to perform one action
2. **Run it** and observe the output
3. **Evaluate** - did it work? What's the current state?
4. **Decide** - is the task complete or do we need another script?
5. **Repeat** until task is done
### No TypeScript in Browser Context
Code passed to \`page.evaluate()\` runs in the browser, which doesn't understand TypeScript:
\`\`\`typescript
// Correct: plain JavaScript
const text = await page.evaluate(() => {
return document.body.innerText;
});
// Wrong: TypeScript syntax will fail at runtime
const text = await page.evaluate(() => {
const el: HTMLElement = document.body; // Type annotation breaks in browser!
return el.innerText;
});
\`\`\`
## Scraping Data
For scraping large datasets, intercept and replay network requests rather than scrolling the DOM. See [references/scraping.md](references/scraping.md) for the complete guide.
## Client API
\`\`\`typescript
const client = await connect();
// Get or create named page
const page = await client.page("name");
const pageWithSize = await client.page("name", { viewport: { width: 1920, height: 1080 } });
const pages = await client.list(); // List all page names
await client.close("name"); // Close a page
await client.disconnect(); // Disconnect (pages persist)
// ARIA Snapshot methods
const snapshot = await client.getAISnapshot("name"); // Get accessibility tree
const element = await client.selectSnapshotRef("name", "e5"); // Get element by ref
\`\`\`
## Waiting
\`\`\`typescript
import { waitForPageLoad } from "@/client.js";
await waitForPageLoad(page); // After navigation
await page.waitForSelector(".results"); // For specific elements
await page.waitForURL("**/success"); // For specific URL
\`\`\`
## Screenshots
\`\`\`typescript
await page.screenshot({ path: "tmp/screenshot.png" });
await page.screenshot({ path: "tmp/full.png", fullPage: true });
\`\`\`
## ARIA Snapshot (Element Discovery)
Use \`getAISnapshot()\` to discover page elements. Returns YAML-formatted accessibility tree:
\`\`\`yaml
- banner:
- link "Hacker News" [ref=e1]
- navigation:
- link "new" [ref=e2]
- main:
- list:
- listitem:
- link "Article Title" [ref=e8]
\`\`\`
**Interacting with refs:**
\`\`\`typescript
const snapshot = await client.getAISnapshot("hackernews");
console.log(snapshot); // Find the ref you need
const element = await client.selectSnapshotRef("hackernews", "e2");
await element.click();
\`\`\`
## Error Recovery
Page state persists after failures. Debug with:
\`\`\`bash
cd skills/dev-browser && npx tsx <<'EOF'
import { connect } from "@/client.js";
const client = await connect();
const page = await client.page("hackernews");
await page.screenshot({ path: "tmp/debug.png" });
console.log({
url: page.url(),
title: await page.title(),
bodyText: await page.textContent("body").then((t) => t?.slice(0, 200)),
});
await client.disconnect();
EOF
\`\`\``,
}
export interface CreateBuiltinSkillsOptions {
browserProvider?: BrowserAutomationProvider
}
export function createBuiltinSkills(options: CreateBuiltinSkillsOptions = {}): BuiltinSkill[] {
const { browserProvider = "playwright" } = options
const browserSkill = browserProvider === "agent-browser" ? agentBrowserSkill : playwrightSkill
return [browserSkill, frontendUiUxSkill, gitMasterSkill, devBrowserSkill]
}

View File

@@ -1,10 +1,9 @@
import { promises as fs, type Dirent } from "fs"
import { join, basename } from "path"
import { homedir } from "os"
import { parseFrontmatter } from "../../shared/frontmatter"
import { sanitizeModelField } from "../../shared/model-sanitizer"
import { isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir } from "../../shared"
import { getClaudeConfigDir, getOpenCodeConfigDir } from "../../shared"
import { log } from "../../shared/logger"
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
@@ -122,7 +121,8 @@ export async function loadProjectCommands(): Promise<Record<string, CommandDefin
}
export async function loadOpencodeGlobalCommands(): Promise<Record<string, CommandDefinition>> {
const opencodeCommandsDir = join(homedir(), ".config", "opencode", "command")
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
const opencodeCommandsDir = join(configDir, "command")
const commands = await loadCommandsFromDir(opencodeCommandsDir, "opencode")
return commandsToRecord(commands)
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
import { describe, test, expect, beforeEach } from "bun:test"
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
import {
setSessionAgent,
getSessionAgent,
@@ -13,9 +13,11 @@ describe("claude-code-session-state", () => {
beforeEach(() => {
// #given - clean state before each test
_resetForTesting()
clearSessionAgent("test-session-1")
clearSessionAgent("test-session-2")
clearSessionAgent("test-prometheus-session")
})
afterEach(() => {
// #then - cleanup after each test to prevent pollution
_resetForTesting()
})
describe("setSessionAgent", () => {
@@ -37,7 +39,7 @@ describe("claude-code-session-state", () => {
setSessionAgent(sessionID, "Prometheus (Planner)")
// #when - try to overwrite
setSessionAgent(sessionID, "Sisyphus")
setSessionAgent(sessionID, "sisyphus")
// #then - first agent preserved
expect(getSessionAgent(sessionID)).toBe("Prometheus (Planner)")
@@ -58,10 +60,10 @@ describe("claude-code-session-state", () => {
setSessionAgent(sessionID, "Prometheus (Planner)")
// #when - force update
updateSessionAgent(sessionID, "Sisyphus")
updateSessionAgent(sessionID, "sisyphus")
// #then
expect(getSessionAgent(sessionID)).toBe("Sisyphus")
expect(getSessionAgent(sessionID)).toBe("sisyphus")
})
})
@@ -92,9 +94,9 @@ describe("claude-code-session-state", () => {
expect(getMainSessionID()).toBe(mainID)
})
test.skip("should return undefined when not set", () => {
// #given - not set
// TODO: Fix flaky test - parallel test execution causes state pollution
test("should return undefined when not set", () => {
// #given - explicit reset to ensure clean state (parallel test isolation)
_resetForTesting()
// #then
expect(getMainSessionID()).toBeUndefined()
})
@@ -123,4 +125,40 @@ describe("claude-code-session-state", () => {
expect(getSessionAgent(sessionID)).toBeUndefined()
})
})
describe("issue #893: custom agent switch reset", () => {
test("should preserve custom agent when default agent is sent on subsequent messages", () => {
// #given - user switches to custom agent "MyCustomAgent"
const sessionID = "test-session-custom"
const customAgent = "MyCustomAgent"
const defaultAgent = "sisyphus"
// User switches to custom agent (via UI)
setSessionAgent(sessionID, customAgent)
expect(getSessionAgent(sessionID)).toBe(customAgent)
// #when - first message after switch sends default agent
// This simulates the bug: input.agent = "Sisyphus" on first message
// Using setSessionAgent (first-write wins) should preserve custom agent
setSessionAgent(sessionID, defaultAgent)
// #then - custom agent should be preserved, NOT overwritten
expect(getSessionAgent(sessionID)).toBe(customAgent)
})
test("should allow explicit agent update via updateSessionAgent", () => {
// #given - custom agent is set
const sessionID = "test-session-explicit"
const customAgent = "MyCustomAgent"
const newAgent = "AnotherAgent"
setSessionAgent(sessionID, customAgent)
// #when - explicit update (user intentionally switches)
updateSessionAgent(sessionID, newAgent)
// #then - should be updated
expect(getSessionAgent(sessionID)).toBe(newAgent)
})
})
})

View File

@@ -14,6 +14,7 @@ export function getMainSessionID(): string | undefined {
export function _resetForTesting(): void {
_mainSessionID = undefined
subagentSessions.clear()
sessionAgentMap.clear()
}
const sessionAgentMap = new Map<string, string>()

View File

@@ -21,7 +21,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
sessionID,
role,
time: { created: Date.now() },
agent: "Sisyphus",
agent: "sisyphus",
model: { providerID: "test", modelID: "test" },
path: { cwd: "/", root: "/" },
},

View File

@@ -128,8 +128,15 @@ $ARGUMENTS
}
}
function parseAllowedTools(allowedTools: string | undefined): string[] | undefined {
function parseAllowedTools(allowedTools: string | string[] | undefined): string[] | undefined {
if (!allowedTools) return undefined
// Handle YAML array format: already parsed as string[]
if (Array.isArray(allowedTools)) {
return allowedTools.map(t => t.trim()).filter(Boolean)
}
// Handle space-separated string format: "Read Write Edit Bash"
return allowedTools.split(/\s+/).filter(Boolean)
}

View File

@@ -268,6 +268,123 @@ Skill body.
} finally {
process.chdir(originalCwd)
}
})
})
describe("allowed-tools parsing", () => {
it("parses space-separated allowed-tools string", async () => {
// #given
const skillContent = `---
name: space-separated-tools
description: Skill with space-separated allowed-tools
allowed-tools: Read Write Edit Bash
---
Skill body.
`
createTestSkill("space-separated-tools", skillContent)
// #when
const { discoverSkills } = await import("./loader")
const originalCwd = process.cwd()
process.chdir(TEST_DIR)
try {
const skills = await discoverSkills({ includeClaudeCodePaths: false })
const skill = skills.find(s => s.name === "space-separated-tools")
// #then
expect(skill).toBeDefined()
expect(skill?.allowedTools).toEqual(["Read", "Write", "Edit", "Bash"])
} finally {
process.chdir(originalCwd)
}
})
it("parses YAML inline array allowed-tools", async () => {
// #given
const skillContent = `---
name: yaml-inline-array
description: Skill with YAML inline array allowed-tools
allowed-tools: [Read, Write, Edit, Bash]
---
Skill body.
`
createTestSkill("yaml-inline-array", skillContent)
// #when
const { discoverSkills } = await import("./loader")
const originalCwd = process.cwd()
process.chdir(TEST_DIR)
try {
const skills = await discoverSkills({ includeClaudeCodePaths: false })
const skill = skills.find(s => s.name === "yaml-inline-array")
// #then
expect(skill).toBeDefined()
expect(skill?.allowedTools).toEqual(["Read", "Write", "Edit", "Bash"])
} finally {
process.chdir(originalCwd)
}
})
it("parses YAML multi-line array allowed-tools", async () => {
// #given
const skillContent = `---
name: yaml-multiline-array
description: Skill with YAML multi-line array allowed-tools
allowed-tools:
- Read
- Write
- Edit
- Bash
---
Skill body.
`
createTestSkill("yaml-multiline-array", skillContent)
// #when
const { discoverSkills } = await import("./loader")
const originalCwd = process.cwd()
process.chdir(TEST_DIR)
try {
const skills = await discoverSkills({ includeClaudeCodePaths: false })
const skill = skills.find(s => s.name === "yaml-multiline-array")
// #then
expect(skill).toBeDefined()
expect(skill?.allowedTools).toEqual(["Read", "Write", "Edit", "Bash"])
} finally {
process.chdir(originalCwd)
}
})
it("returns undefined for skill without allowed-tools", async () => {
// #given
const skillContent = `---
name: no-allowed-tools
description: Skill without allowed-tools field
---
Skill body.
`
createTestSkill("no-allowed-tools", skillContent)
// #when
const { discoverSkills } = await import("./loader")
const originalCwd = process.cwd()
process.chdir(TEST_DIR)
try {
const skills = await discoverSkills({ includeClaudeCodePaths: false })
const skill = skills.find(s => s.name === "no-allowed-tools")
// #then
expect(skill).toBeDefined()
expect(skill?.allowedTools).toBeUndefined()
} finally {
process.chdir(originalCwd)
}
})
})
})

View File

@@ -50,8 +50,15 @@ async function loadMcpJsonFromDir(skillDir: string): Promise<SkillMcpConfig | un
return undefined
}
function parseAllowedTools(allowedTools: string | undefined): string[] | undefined {
function parseAllowedTools(allowedTools: string | string[] | undefined): string[] | undefined {
if (!allowedTools) return undefined
// Handle YAML array format: already parsed as string[]
if (Array.isArray(allowedTools)) {
return allowedTools.map(t => t.trim()).filter(Boolean)
}
// Handle space-separated string format: "Read Write Edit Bash"
return allowedTools.split(/\s+/).filter(Boolean)
}

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