Compare commits

..

296 Commits

Author SHA1 Message Date
github-actions[bot]
711a347b64 release: v3.1.11 2026-02-01 06:05:22 +00:00
justsisyphus
6667ace7ca fix(ci): remove deleted compaction-context-injector from test paths 2026-02-01 15:03:13 +09:00
justsisyphus
e48be69a62 fix(rules-injector): remove dead batch code, add .sisyphus support
- Remove non-functional batch tool handling (OpenCode has no batch tool)
- Keep working direct tool call path (read/write/edit/multiedit)
- Apply same cleanup to directory-agents-injector and directory-readme-injector
- Add .sisyphus/rules directory support
2026-02-01 15:01:09 +09:00
justsisyphus
3808fd3a4b feat(command): add Oracle safety review for deployment check 2026-02-01 14:48:04 +09:00
justsisyphus
ac33b76193 chore(command): remove hardcoded model from get-unpublished-changes 2026-02-01 14:45:24 +09:00
justsisyphus
a24f1e905e chore: fix bun-build gitignore pattern to catch all variants 2026-02-01 14:43:30 +09:00
justsisyphus
08439a511a fix(test): add missing ToolContext fields to test mocks
@opencode-ai/plugin ToolContext now requires directory, worktree,
metadata, and ask fields. Updated all tool test mocks to comply.
2026-02-01 14:16:28 +09:00
justsisyphus
cbbc7bd075 refactor: remove orphaned compaction-context-injector hook
Hook was disconnected from plugin flow since commit 4a82ff40.
Never called at runtime, superseded by preemptive-compaction hook.
2026-02-01 14:16:21 +09:00
justsisyphus
f9bc23b39f fix: regenerate bun.lock to restore vscode-jsonrpc dependency
- vscode-jsonrpc was missing from lockfile, breaking LSP tools
- Platform binaries restored to 3.1.10 (was incorrectly 3.0.0-beta.8)
2026-02-01 14:16:14 +09:00
github-actions[bot]
69e3bbe362 @edxeth has signed the CLA in code-yeongyu/oh-my-opencode#1348 2026-02-01 00:58:36 +00:00
github-actions[bot]
8c3feb8a9d @dmealing has signed the CLA in code-yeongyu/oh-my-opencode#1296 2026-01-31 20:24:00 +00:00
github-actions[bot]
8b2c134622 @taetaetae has signed the CLA in code-yeongyu/oh-my-opencode#1333 2026-01-31 17:49:05 +00:00
YeonGyu-Kim
96e7b39a83 fix: use _resetForTesting() consistently to prevent flaky tests (#1318)
- Replace setMainSession(undefined) with _resetForTesting() in keyword-detector tests
- Add _resetForTesting() to afterEach hooks for proper cleanup
- Un-skip the previously flaky mainSessionID test in state.test.ts

Fixes #848

Co-authored-by: 배지훈 <new0126@naver.com>
2026-01-31 16:34:07 +09:00
Sisyphus
bb181ee572 fix(background-agent): track and cancel completion timers to prevent memory leaks (#1058)
Track setTimeout timers in notifyParentSession using a completionTimers Map.
Clear all timers on shutdown() and when tasks are deleted via session.deleted.
This prevents the BackgroundManager instance from being held in memory by
uncancelled timer callbacks.

Fixes #1043

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
2026-01-31 16:26:01 +09:00
YeonGyu-Kim
8aa2549368 Merge pull request #1056 from code-yeongyu/feat/glm-4.7-thinking-mode
feat(think-mode): add GLM-4.7 thinking mode support
2026-01-31 16:12:28 +09:00
YeonGyu-Kim
d18bd068c3 Merge pull request #1053 from code-yeongyu/fix/windows-lsp-bun-version-check
fix(lsp): add Bun version check for Windows LSP segfault bug
2026-01-31 16:12:05 +09:00
Nguyen Khac Trung Kien
b03e463bde fix: prevent zombie processes with proper process lifecycle management (#1306)
* fix: prevent zombie processes with proper process lifecycle management

- Await proc.exited for fire-and-forget spawns in tmux-utils.ts
- Remove competing process.exit() calls from LSP client and skill-mcp-manager
  signal handlers to let background-agent manager coordinate final exit
- Await process exit after kill() in interactive-bash timeout handler
- Await process exit after kill() in LSP client stop() method

These changes ensure spawned processes are properly reaped and prevent
orphan/zombie processes when running with tmux integration.

* fix: address Copilot review comments on process cleanup

- LSP cleanup: use async/sync split with Promise.allSettled for proper subprocess cleanup
- LSP stop(): make idempotent by nulling proc before await to prevent race conditions
- Interactive-bash timeout: use .then()/.catch() pattern instead of async callback to avoid unhandled rejections
- Skill-mcp-manager: use void+catch pattern for fire-and-forget signal handlers

* fix: address remaining Copilot review comments

- interactive-bash: reject timeout immediately, fire-and-forget zombie cleanup
- skill-mcp-manager: update comments to accurately describe signal handling strategy

* fix: address additional Copilot review comments

- LSP stop(): add 5s timeout to prevent indefinite hang on stuck processes
- tmux-utils: log warnings when pane title setting fails (both spawn/replace)
- BackgroundManager: delay process.exit() to next tick via setImmediate to allow other signal handlers to complete cleanup

* fix: address code review findings

- Increase exit delay from setImmediate to 100ms setTimeout to allow async cleanup
- Use asyncCleanup for SIGBREAK on Windows for consistency with SIGINT/SIGTERM
- Add try/catch around stderr read in spawnTmuxPane for consistency with replaceTmuxPane

* fix: address latest Copilot review comments

- LSP stop(): properly clear timeout when proc.exited wins the race
- BackgroundManager: use process.exitCode before delayed exit for cleaner shutdown
- spawnTmuxPane: remove redundant log import, reuse existing one

* fix: address latest Copilot review comments

- LSP stop(): escalate to SIGKILL on timeout, add logging
- tmux spawnTmuxPane/replaceTmuxPane: drain stderr immediately to avoid backpressure

* fix: address latest Copilot review comments

- Add .catch() to asyncCleanup() signal handlers to prevent unhandled rejections
- Await proc.exited after SIGKILL with 1s timeout to confirm termination

* fix: increase exit delay to 6s to accommodate LSP cleanup

LSP cleanup can take up to 5s (timeout) + 1s (SIGKILL wait), so the exit
delay must be at least 6s to ensure child processes are properly reaped.
2026-01-31 16:01:19 +09:00
YeonGyu-Kim
4a82ff40fb Consolidate duplicate patterns and simplify codebase (#1317)
* refactor(shared): unify binary downloader and session path storage

- Create binary-downloader.ts for common download/extract logic
- Create session-injected-paths.ts for unified path tracking
- Refactor comment-checker, ast-grep, grep downloaders to use shared util
- Consolidate directory injector types into shared module

* feat(shared): implement unified model resolution pipeline

- Create ModelResolutionPipeline for centralized model selection
- Refactor model-resolver to use pipeline
- Update delegate-task and config-handler to use unified logic
- Ensure consistent model resolution across all agent types

* refactor(agents): simplify agent utils and metadata management

- Extract helper functions for config merging and env context
- Register prompt metadata for all agents
- Simplify agent variant detection logic

* cleanup: inline utilities and remove unused exports

- Remove case-insensitive.ts (inline with native JS)
- Simplify opencode-version helpers
- Remove unused getModelLimit, createCompactionContextInjector exports
- Inline transcript entry creation in claude-code-hooks
- Update tests accordingly

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-31 15:46:14 +09:00
justsisyphus
4b5e38f8f8 fix(hooks): make /stop-continuation one-time only and respect in session recovery
- Clear stop state when user sends new message (chat.message handler)
- Add isContinuationStopped check to session error recovery block
- Continuation resumes automatically after user interaction
2026-01-31 15:24:27 +09:00
YeonGyu-Kim
e63c568c4f feat(hooks): add /stop-continuation command to halt all continuation mechanisms (#1316)
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-31 15:09:05 +09:00
justsisyphus
ddfbdbb84e docs(skill): enforce exhaustive pagination in github-issue-triage
- Add critical warnings about using --limit 500 instead of 100
- Add verification checklist before proceeding to Phase 2
- Add severity levels to anti-patterns (CRITICAL/HIGH/MEDIUM)
- Emphasize counting results and fetching additional pages if needed
2026-01-31 14:25:16 +09:00
justsisyphus
41dd4ce22a fix: always switch to atlas in /start-work to fix Prometheus sessions
Fixes #1298
2026-01-31 13:00:18 +09:00
github-actions[bot]
4f26e99ee7 release: v3.1.10 2026-01-31 03:52:22 +00:00
Kwanghyun Moon
b405494808 fix: resolve deadlock in config handler during plugin initialization (#1304)
* fix: resolve deadlock in config handler during plugin initialization

The config handler and createBuiltinAgents were calling fetchAvailableModels
with client, which triggers client.provider.list() API call to OpenCode server.
This caused a deadlock because:
- Plugin initialization waits for server response
- Server waits for plugin init to complete before handling requests

Now using cache-only mode by passing undefined instead of client.
If cache is unavailable, the fallback chain will use the first model.

Fixes #1301

* test: add regression tests for deadlock prevention in fetchAvailableModels

Add tests to ensure fetchAvailableModels is called with undefined client
during plugin initialization. This prevents regression on issue #1301.

- config-handler.test.ts: verify config handler does not pass client
- utils.test.ts: verify createBuiltinAgents does not pass client

* test: restore spies in utils.test.ts to prevent test pollution

Add mockRestore() calls for all spies created in test cases to ensure proper cleanup between tests and prevent state leakage.

* test: restore fetchAvailableModels spy

---------

Co-authored-by: robin <robin@watcha.com>
Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-31 12:46:05 +09:00
github-actions[bot]
839a4c5316 @robin-watcha has signed the CLA in code-yeongyu/oh-my-opencode#1303 2026-01-30 22:37:44 +00:00
github-actions[bot]
08d43efdb0 @khduy has signed the CLA in code-yeongyu/oh-my-opencode#1297 2026-01-30 18:35:46 +00:00
justsisyphus
061a5f5132 refactor(momus): simplify prompt to prevent nitpicking and infinite loops
- Reduce prompt from 392 to 125 lines
- Add APPROVAL BIAS: approve by default, reject only for blockers
- Limit max 3 issues per rejection to prevent overwhelming feedback
- Remove 'ruthlessly critical' tone, add 'practical reviewer' approach
- Add explicit anti-patterns section for what NOT to reject
- Define 'good enough' criteria (80% clear = pass)
- Update tests to match simplified prompt structure
2026-01-31 00:51:51 +09:00
github-actions[bot]
d4acd23630 @KonaEspresso94 has signed the CLA in code-yeongyu/oh-my-opencode#1289 2026-01-30 15:33:41 +00:00
github-actions[bot]
c77c9ceb53 release: v3.1.9 2026-01-30 14:15:54 +00:00
YeonGyu-Kim
8c2625cfb0 🏆 test: optimize test suite with FakeTimers and race condition fixes (#1284)
* fix: exclude prompt/permission from plan agent config

plan agent should only inherit model settings from prometheus,
not the prompt or permission. This ensures plan agent uses
OpenCode's default behavior while only overriding the model.

* test(todo-continuation-enforcer): use FakeTimers for 15x faster tests

- Add custom FakeTimers implementation (~100 lines)
- Replace all real setTimeout waits with fakeTimers.advanceBy()
- Test time: 104.6s → 7.01s

* test(callback-server): fix race conditions with Promise.all and Bun.fetch

- Use Bun.fetch.bind(Bun) to avoid globalThis.fetch mock interference
- Use Promise.all pattern for concurrent fetch/waitForCallback
- Add Bun.sleep(10) in afterEach for port release

* test(concurrency): replace placeholder assertions with getCount checks

Replace 6 meaningless expect(true).toBe(true) assertions with
actual getCount() verifications for test quality improvement

* refactor(config-handler): simplify planDemoteConfig creation

Remove unnecessary IIFE and destructuring, use direct spread instead

* test(executor): use FakeTimeouts for faster tests

- Add custom FakeTimeouts implementation
- Replace setTimeout waits with fakeTimeouts.advanceBy()
- Test time reduced from ~26s to ~6.8s

* test: fix gemini model mock for artistry unstable mode

* test: fix model list mock payload shape

* test: mock provider models for artistry category

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-30 22:10:52 +09:00
github-actions[bot]
3ced20d1ab @kunal70006 has signed the CLA in code-yeongyu/oh-my-opencode#1282 2026-01-30 09:56:07 +00:00
github-actions[bot]
fb02cc9e95 @Zacks-Zhang has signed the CLA in code-yeongyu/oh-my-opencode#1280 2026-01-30 08:51:59 +00:00
justsisyphus
80ee52fe3b fix: improve model resolution with client API fallback and explicit model passing
- fetchAvailableModels now falls back to client.model.list() when cache is empty
- provider-models cache empty → models.json → client API (3-tier fallback)
- look-at tool explicitly passes registered agent's model to session.prompt
- Ensures multimodal-looker uses correctly resolved model (e.g., gemini-3-flash-preview)
- Add comprehensive tests for fuzzy matching and fallback scenarios
2026-01-30 16:57:21 +09:00
github-actions[bot]
2f7e188cb5 @Hisir0909 has signed the CLA in code-yeongyu/oh-my-opencode#1275 2026-01-30 07:33:44 +00:00
justsisyphus
f8be01c6dd test: update Atlas fallback test and misc code improvements
- Update Atlas fallback test to expect k2p5 as primary (kimi-for-coding)

- Minor improvements to connected-providers-cache and utils

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-30 16:19:02 +09:00
justsisyphus
0dbec08923 feat(cli): add kimi-for-coding provider to model fallback
- Add kimiForCoding field to ProviderAvailability interface

- Add kimi-for-coding provider mapping in isProviderAvailable

- Include kimi-for-coding in Sisyphus fallback chain for non-max plan

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-30 16:19:02 +09:00
justsisyphus
691fa8b815 refactor(sisyphus-junior): extract MODE constant and add export
- Add AgentMode type import and MODE constant

- Export mode on createSisyphusJuniorAgentWithOverrides function

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-30 16:19:02 +09:00
justsisyphus
a73d806d4e docs: update explore agent model and category descriptions
- Change explore agent from Grok Code to Claude Haiku 4.5

- Update deep category description for clarity

- Fix Momus fallback chain order

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-30 16:19:02 +09:00
justsisyphus
a424f81cd5 docs: update Sisyphus fallback chain across all documentation
Update Sisyphus fallback chain to include gpt-5.2-codex and gemini-3-pro

Files: AGENTS.md, README*.md, src/agents/AGENTS.md

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-30 16:19:02 +09:00
justsisyphus
1187a02020 fix: Atlas respects fallbackChain, always refresh provider-models cache
- Remove uiSelectedModel from Atlas model resolution (use k2p5 as primary)
- Always overwrite provider-models.json on session start to prevent stale cache
2026-01-30 16:19:02 +09:00
Junho Yeo
3074434887 fix: use correct gh api command for starring repo (#1274)
`gh repo star` is not a valid GitHub CLI command.
Use `gh api --silent --method PUT /user/starred/OWNER/REPO` instead.
2026-01-30 15:58:56 +09:00
justsisyphus
6bb2854162 Merge branch 'omo-avail' into dev 2026-01-30 15:28:20 +09:00
justsisyphus
e08904a27a feat: add artistry category to ultrawork-mode specialist delegation
- Add oracle vs artistry distinction in MANDATORY CERTAINTY PROTOCOL
- Update WHEN IN DOUBT examples with both delegation options
- Add artistry to IF YOU ENCOUNTER A BLOCKER section
- Add 'Hard problem (non-conventional)' row to AGENTS UTILIZATION table
- Update analyze-mode message with artistry specialist option

Oracle: conventional problems (architecture, debugging, complex logic)
Artistry: non-conventional problems (different approach needed)
2026-01-30 15:19:38 +09:00
justsisyphus
0188d69233 test: add requiresModel and isModelAvailable tests 2026-01-30 15:11:32 +09:00
justsisyphus
2c74f608f0 feat(delegate-task, agents): check requiresModel for conditional activation 2026-01-30 15:11:27 +09:00
justsisyphus
baefd16b3f feat(shared): add requiresModel field and isModelAvailable helper 2026-01-30 15:11:19 +09:00
justsisyphus
b1b4578906 feat: add opencode/kimi-k2.5-free fallback and prioritize kimi for atlas 2026-01-30 15:10:38 +09:00
justsisyphus
9d20a5b11c feat: add kimi-for-coding provider to installer and fix model ID to k2p5 2026-01-30 15:08:26 +09:00
justsisyphus
d2d8d1a782 feat: add kimi-k2.5 to agent fallback chains and update model catalog
- sisyphus: opus → kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro
- atlas: sonnet-4-5 → kimi-k2.5 → gpt-5.2 → gemini-3-pro
- prometheus/metis: opus → kimi-k2.5 → gpt-5.2 → gemini-3-pro
- multimodal-looker: gemini-flash → gpt-5.2 → glm-4.6v → kimi-k2.5 → haiku → gpt-5-nano
- visual-engineering: remove gpt-5.2 from chain
- ultrabrain: reorder to gpt-5.2-codex → gemini-3-pro → opus
- Add cross-provider fuzzy match for model resolution
- Update all documentation (AGENTS.md, features.md, configurations.md, category-skill-guide.md)
2026-01-30 14:53:50 +09:00
justsisyphus
10bdb6c694 chore: update artistry category description for creative problem-solving 2026-01-30 14:53:50 +09:00
justsisyphus
5f243e2d3a chore: add glm-4.7 to visual-engineering fallback chain 2026-01-30 14:53:50 +09:00
justsisyphus
82a47ff928 chore: add code style requirements to ultrabrain prompt
- MUST search existing codebase for patterns before writing code
- MUST match project's existing conventions
- MUST write readable, human-friendly code
2026-01-30 14:53:50 +09:00
justsisyphus
c06f38693e refactor: revamp ultrabrain category with deep work mindset
- Add variant: max to ultrabrain's gemini-3-pro fallback entry
- Rename STRATEGIC_CATEGORY_PROMPT_APPEND to ULTRABRAIN_CATEGORY_PROMPT_APPEND
- Keep original strategic advisor prompt content (no micromanagement instructions)
- Update description: use only for genuinely hard tasks, give clear goals only
- Update tests to match renamed constant
2026-01-30 14:53:50 +09:00
justsisyphus
6e9cb7ecd8 chore: add variant max to momus opus-4-5 fallback entry 2026-01-30 14:53:50 +09:00
justsisyphus
b731399edf chore: prioritize gemini-3-pro over opus in oracle fallback chain
- Move gemini-3-pro above claude-opus-4-5 in oracle's fallbackChain
- Add variant: "max" to gemini-3-pro entry
2026-01-30 14:53:50 +09:00
github-actions[bot]
0a28f6a790 @gabriel-ecegi has signed the CLA in code-yeongyu/oh-my-opencode#1271 2026-01-30 05:13:19 +00:00
justsisyphus
4e529b74e0 revert: remove unnecessary NODE_AUTH_TOKEN from publish.yml (OIDC works) 2026-01-30 13:54:46 +09:00
justsisyphus
90eec0a369 fix: add NODE_AUTH_TOKEN env to main publish workflow 2026-01-30 13:50:55 +09:00
justsisyphus
3b5d18e6bf fix(agents): exclude subagents from UI model selection override
Subagents (explore, librarian, oracle, etc.) now use their own fallback
chain instead of inheriting the UI-selected model. This fixes the issue
where explore agent was incorrectly using Opus instead of Haiku.

- Add AgentMode type and static mode property to AgentFactory
- Each agent declares its own mode via factory.mode = MODE pattern
- createBuiltinAgents() checks source.mode before passing uiSelectedModel
2026-01-30 13:49:40 +09:00
justsisyphus
67aeb9cb8c chore: replace big-pickle model with glm-4.7-free 2026-01-30 13:44:04 +09:00
justsisyphus
b1c1f02172 fix: add NODE_AUTH_TOKEN env to publish step 2026-01-30 13:36:20 +09:00
justsisyphus
2b39d119cd fix: restore registry-url for npm auth with new granular token 2026-01-30 13:21:35 +09:00
justsisyphus
afa2ece847 fix: remove registry-url from setup-node to enable OIDC auth 2026-01-30 13:11:44 +09:00
justsisyphus
390c25197f fix: manually create .npmrc without token for OIDC
setup-node with registry-url injects NODE_AUTH_TOKEN secret which is revoked.
Create .npmrc manually with empty _authToken to force OIDC authentication.
2026-01-30 12:57:15 +09:00
justsisyphus
9e07b143df fix: match main workflow's OIDC setup exactly
Main workflow works with registry-url + NPM_CONFIG_PROVENANCE.
Removed all extra env vars and debugging - simplify to match working pattern.
2026-01-30 12:52:57 +09:00
justsisyphus
ad95880198 fix(start-work): restore atlas agent and proper model fallback chain
- Restore agent: 'atlas' in start-work command (removed by PR #1201)
- Fix model-resolver to properly iterate through fallback chain providers
- Remove broken parent model inheritance that bypassed fallback logic
- Add model-suggestion-retry for runtime API failures (cherry-pick 800846c1)

Fixes #1200
2026-01-30 12:52:46 +09:00
justsisyphus
86088d3a6e fix: remove registry-url to enable npm OIDC auto-detection
- Remove registry-url from setup-node (was injecting NODE_AUTH_TOKEN)
- Add npm version check and auto-upgrade for OIDC support (11.5.1+)
- Add explicit --registry flag to npm publish
- Remove empty NODE_AUTH_TOKEN/NPM_CONFIG_USERCONFIG env vars that were breaking OIDC
2026-01-30 12:47:15 +09:00
justsisyphus
ae8a6c5eb8 refactor: replace console.log/warn/error with file-based log() for silent logging
Replace all console output with shared logger to write to
/tmp/oh-my-opencode.log instead of stdout/stderr.

Files changed:
- index.ts: console.warn → log()
- hook-message-injector/injector.ts: console.warn → log()
- lsp/client.ts: console.error → log()
- ast-grep/downloader.ts: console.log/error → log()
- session-recovery/index.ts: console.error → log()
- comment-checker/downloader.ts: console.log/error → log()

CLI tools (install.ts, doctor, etc.) retain console output for UX.
2026-01-30 12:45:37 +09:00
justsisyphus
db538c7e6b fix(ci): override env vars to disable token auth, force OIDC 2026-01-30 12:41:00 +09:00
justsisyphus
dfed2abd3e fix(ci): also remove NPM_CONFIG_USERCONFIG .npmrc and unset tokens for OIDC 2026-01-30 12:37:12 +09:00
justsisyphus
300a3fdc14 fix(ci): remove .npmrc to enable pure OIDC auth for npm publish 2026-01-30 12:33:51 +09:00
justsisyphus
c993cf007f fix(ci): remove registry-url to use pure OIDC auth for npm publish 2026-01-30 12:29:33 +09:00
justsisyphus
3d7de0a050 fix(publish-platform): use 7z on Windows, simplify skip logic 2026-01-30 12:25:30 +09:00
justsisyphus
8e19ffdce4 ci(publish-platform): separate build/publish jobs with OIDC provenance
- Split into two jobs: build (compile binaries) and publish (npm publish)
- Build job uploads compressed artifacts (tar.gz/zip)
- Publish job downloads artifacts and uses OIDC Trusted Publishing
- Removes NODE_AUTH_TOKEN dependency, uses npm provenance instead
- Increased timeout for large binary uploads (40-120MB)
- Build parallelism increased to 7 (all platforms simultaneously)
- Fixes npm classic token deprecation issue

Benefits:
- Fresh OIDC token at publish time avoids timeout issues
- No token rotation needed (OIDC is ephemeral)
- Build failures isolated from publish failures
- Artifacts can be reused if publish fails
2026-01-30 12:21:24 +09:00
github-actions[bot]
456d9cea65 release: v3.1.8 2026-01-30 02:58:12 +00:00
justsisyphus
30f893b766 fix(cli/run): fix [undefine] tag and add text preview to verbose log
- Fix sessionTag showing '[undefine]' when sessionID is undefined
  - System events now display as '[system]' instead
- Fix message.updated expecting non-existent 'content' field
  - SDK's EventMessageUpdated only contains info metadata, not content
  - Content is streamed via message.part.updated events
- Add text preview to message.part.updated verbose logging
- Update MessageUpdatedProps type to match SDK structure
- Update tests to reflect actual SDK behavior
2026-01-30 11:45:58 +09:00
justsisyphus
c905e1cb7a fix(delegate-task): restore resolved.model to category userModel chain (#1227)
PR #1227 incorrectly removed resolved.model from the userModel chain,
assuming it was bypassing the fallback chain. However, resolved.model
contained the category's DEFAULT_CATEGORIES model (e.g., quick ->
claude-haiku-4-5), not the main session model.

Without resolved.model, when connectedProvidersCache is null and
availableModels is empty, category model resolution falls through to
systemDefaultModel (opus) instead of using the category's default.

This fix restores the original priority:
1. User category model override
2. Category default model (from resolved.model)
3. sisyphusJuniorModel
4. Fallback chain
5. System default
2026-01-30 11:45:19 +09:00
YeonGyu-Kim
d3e2b36e3d refactor(tmux-subagent): introduce dependency injection for testability (#1267)
Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-30 10:59:54 +09:00
YeonGyu-Kim
5f0b6d49f5 fix(run): prevent premature exit on idle before meaningful work (#1263)
The run command's completion check had a race condition: when a session
transitions busy->idle before the LLM generates any output (empty
response or API delay), checkCompletionConditions() returns true because
0 incomplete todos + 0 busy children = complete. This caused the runner
to exit with 'All tasks completed' before any work was done.

Fix:
- Add hasReceivedMeaningfulWork flag to EventState
- Set flag on: assistant text content, tool execution, or message update
  with actual content (all scoped to main session only)
- Guard completion check in runner poll loop: skip if no meaningful work
  has been observed yet

This ensures the runner waits until the session has produced at least one
observable output before considering completion conditions.

Adds 6 new test cases covering the race condition scenarios.
2026-01-30 09:10:24 +09:00
github-actions[bot]
b45408dd9c @LeekJay has signed the CLA in code-yeongyu/oh-my-opencode#1254 2026-01-29 17:03:39 +00:00
github-actions[bot]
6c8527f29b release: v3.1.7 2026-01-29 12:39:22 +00:00
justsisyphus
cd4da93bf2 fix(test): migrate config-handler tests from mock.module to spyOn to prevent cross-file cache pollution 2026-01-29 21:35:14 +09:00
justsisyphus
71b2f1518a chore(agents): unify agent description format with OhMyOpenCode attribution 2026-01-29 21:27:04 +09:00
YeonGyu-Kim
dcda8769cc feat(mcp-oauth): add full OAuth 2.1 authentication for MCP servers (#1169)
* feat(mcp-oauth): add oauth field to ClaudeCodeMcpServer schema

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

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

* feat(mcp-oauth): add RFC 7591 Dynamic Client Registration

* feat(mcp-oauth): add RFC 9728 PRM + RFC 8414 AS discovery

* feat(mcp-oauth): add secure token storage with {host}/{resource} key format

* feat(mcp-oauth): add dynamic port OAuth callback server

* feat(mcp-oauth): add RFC 8707 Resource Indicators

* feat(mcp-oauth): implement full-spec McpOAuthProvider

* feat(mcp-oauth): add step-up authorization handler

* feat(mcp-oauth): integrate authProvider into SkillMcpManager

* feat(doctor): add MCP OAuth token status check

* feat(cli): add mcp oauth subcommand structure

* feat(cli): implement mcp oauth login command

* fix(mcp-oauth): address cubic review — security, correctness, and test issues

- Remove @ts-nocheck from provider.ts, storage.ts, provider.test.ts
- Fix server resource leak on missing code/state (close + reject)
- Fix command injection in openBrowser (spawn array args, cross-platform)
- Mock McpOAuthProvider in login.test.ts for deterministic CI
- Recreate auth provider with merged scopes in step-up flow
- Add listAllTokens() for global status listing
- Fix logout to accept --server-url for correct token deletion
- Support both quoted and unquoted WWW-Authenticate params (RFC 2617)
- Save/restore OPENCODE_CONFIG_DIR in storage.test.ts
- Fix index.test.ts: vitest → bun:test

* fix(mcp-oauth): use explorer instead of cmd /c start on Windows to prevent shell injection

* fix(mcp-oauth): address remaining cubic review issues

- Add 5-minute timeout to provider callback server to prevent indefinite hangs
- Persist client registration from token storage across process restarts
- Require --server-url for logout to match token storage key format
- Use listTokensByHost for server-specific status lookups
- Fix callback-server test to handle promise rejection ordering
- Fix provider test port expectations (8912 → 19877)
- Fix cli-guide.md duplicate Section 7 numbering
- Fix manager test for login-on-missing-tokens behavior

* fix(mcp-oauth): address final review issues

- P1: Redact token values in status.ts output to prevent credential leakage
- P2: Read OAuth error response body before throwing in token exchange
- Test: Fix mcp-oauth doctor test to use epoch seconds (not milliseconds)

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-01-29 19:48:36 +09:00
YeonGyu-Kim
a94fbadd57 Migrate LSP client to vscode-jsonrpc for improved stability (#1095)
* refactor(lsp): migrate to vscode-jsonrpc for improved stability

Replace custom JSON-RPC implementation with vscode-jsonrpc library.
Use MessageConnection with StreamMessageReader/Writer.
Implement Bun↔Node stream bridges for compatibility.
Preserve all existing functionality (warmup, cleanup, capabilities).
Net reduction of ~60 lines while improving protocol handling.

* fix(lsp): clear timeout on successful response to prevent unhandled rejections

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-29 19:48:28 +09:00
YeonGyu-Kim
23b49c4a5c fix: expand override.category and explicit reasoningEffort priority (#1219) (#1235)
* fix: expand override.category and explicit reasoningEffort priority (#1219)

Two bugs fixed:

1. createBuiltinAgents(): override.category was never expanded into concrete
   config properties (model, variant, reasoningEffort, etc.). Added
   applyCategoryOverride() helper and applied it in the standard agent loop,
   Sisyphus path, and Atlas path.

2. Prometheus config-handler: reasoningEffort/textVerbosity/thinking from
   direct override now use explicit priority chains (direct > category)
   matching the existing variant pattern, instead of relying on spread
   ordering.

Priority order (highest to lowest):
  1. Direct override properties
  2. Override category properties
  3. Resolved variant from model fallback chain
  4. Factory base defaults

Closes #1219

* fix: use undefined check for thinking to allow explicit false
2026-01-29 19:46:34 +09:00
YeonGyu-Kim
b4973954e3 fix(background-agent): prevent zombie processes by aborting sessions on shutdown (#1240) (#1243)
- BackgroundManager.shutdown() now aborts all running child sessions via
  client.session.abort() before clearing state, preventing orphaned
  opencode processes when parent exits
- Add onShutdown callback to BackgroundManager constructor, used to
  trigger TmuxSessionManager.cleanup() on process exit signals
- Interactive bash session hook now aborts tracked subagent opencode
  sessions when killing tmux sessions (defense-in-depth)
- Add 4 tests verifying shutdown abort behavior and callback invocation

Closes #1240
2026-01-29 18:29:47 +09:00
github-actions[bot]
6d50fbe563 @Lynricsy has signed the CLA in code-yeongyu/oh-my-opencode#1241 2026-01-29 09:00:40 +00:00
YeonGyu-Kim
9850dd0f6e fix(test): align agent tests with connected-providers-cache fallback behavior (#1227)
Tests in utils.test.ts were written before bffa1ad introduced
connected-providers-cache fallback in resolveModelWithFallback.
Update assertions to match the new resolution path:
- Oracle resolves to openai/gpt-5.2 via cache (not systemDefault)
- Agents are created via cache fallback even without systemDefaultModel
2026-01-29 11:47:17 +09:00
YeonGyu-Kim
34aaef2219 fix(delegate-task): pass registered agent model explicitly for subagent_type (#1225)
When delegate_task uses subagent_type, extract the matched agent's model
object and pass it explicitly to session.prompt/manager.launch. This
ensures the model is always in the correct object format regardless of
how OpenCode handles string→object conversion for plugin-registered
agents.

Closes #1225
2026-01-29 11:27:07 +09:00
Mike
faca80caa9 fix(start-work): prevent overwriting session agent if already set; inherit parent model for subagent types (#1201)
* fix(start-work): prevent overwriting session agent if already set; inherit parent model for subagent types

* fix(model): include variant in StoredMessage model structure for better context propagation

* fix(injector): include variant in model structure for hook message injection
2026-01-29 09:30:37 +09:00
SUHO LEE
0c3fbd724b fix(model-resolver): respect UI model selection in agent initialization (#1158)
- Add uiSelectedModel parameter to resolveModelWithFallback()
- Update model resolution priority: UI Selection → Config Override → Fallback → System Default
- Pass config.model as uiSelectedModel in createBuiltinAgents()
- Fix ProviderModelNotFoundError when model is unset in config but selected in UI
2026-01-29 09:30:35 +09:00
Srijan Guchhait
c7455708f8 docs: Add missing configuration options to configurations.md (#1186)
- Add disabled_commands section with available commands
- Add comment_checker configuration (custom_prompt)
- Add notification configuration (force_enable)
- Add sisyphus tasks & swarm configuration sections
- Add staleTimeoutMs to background_task section
- Add dynamic_context_pruning to experimental section with full documentation
- Extend skills configuration with advanced options (sources, custom skills)
- Extend agents configuration with missing options (category, variant, maxTokens, thinking, reasoningEffort, textVerbosity, providerOptions)
- Extend categories configuration with missing options (description, is_unstable_agent)
- Extend LSP configuration with missing server options (env, initialization, disabled) and detailed examples
- Add missing hooks (auto-slash-command, sisyphus-junior-notepad, start-work) to hooks list
- Update available agents list to include all agents from schema

Co-authored-by: GitHub Actions <actions@github.com>
2026-01-29 09:30:32 +09:00
Peïo Thibault
bffa1ad43d fix(model-resolver): use connected providers cache when model cache is empty (#1227)
- Remove resolved.model from userModel in tools.ts (was bypassing fallback chain)
- Use connected providers cache in model-resolver when availableModels is empty
- Allows proper provider selection (e.g., github-copilot instead of google)
2026-01-29 09:30:19 +09:00
github-actions[bot]
6560dedd4c @mrdavidlaing has signed the CLA in code-yeongyu/oh-my-opencode#1226 2026-01-28 19:51:45 +00:00
sisyphus-dev-ai
b7e32a99f2 chore: changes by sisyphus-dev-ai 2026-01-28 16:51:21 +00:00
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
sisyphus-dev-ai
d15794004e fix(lsp): add Bun version check for Windows LSP segfault bug
On Windows with Bun v1.3.5 and earlier, spawning LSP servers causes
a segmentation fault crash. This is a known Bun bug fixed in v1.3.6.

Added version check before LSP server spawn that:
- Detects Windows + affected Bun versions (< 1.3.6)
- Throws helpful error with upgrade instructions instead of crashing
- References the Bun issue for users to track

Closes #1047
2026-01-24 16:45:59 +09:00
sisyphus-dev-ai
de6f4b2c91 feat(think-mode): add GLM-4.7 thinking mode support
Add thinking mode support for Z.AI's GLM-4.7 model via the zai-coding-plan provider.

Changes:
- Add zai-coding-plan to THINKING_CONFIGS with extra_body.thinking config
- Add glm pattern to THINKING_CAPABLE_MODELS
- Add comprehensive tests for GLM thinking mode

GLM-4.7 uses OpenAI-compatible API with extra_body wrapper for thinking:
- thinking.type: 'enabled' or 'disabled'
- thinking.clear_thinking: false (Preserved Thinking mode)

Closes #1030
2026-01-24 16:45:34 +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
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
carlory
45fe9578ec fix(doctor): handle file:// protocol for local dev plugin detection 2026-01-19 16:09:46 +08:00
289 changed files with 24866 additions and 5143 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).

View File

@@ -28,11 +28,20 @@ permissions:
id-token: write
jobs:
publish-platform:
runs-on: ubuntu-latest
# =============================================================================
# Job 1: Build binaries for all platforms
# - Windows builds on windows-latest (avoid bun cross-compile segfault)
# - All other platforms build on ubuntu-latest
# - Uploads compressed artifacts for the publish job
# =============================================================================
build:
runs-on: ${{ matrix.platform == 'windows-x64' && 'windows-latest' || 'ubuntu-latest' }}
defaults:
run:
shell: bash
strategy:
fail-fast: false
max-parallel: 2
max-parallel: 7
matrix:
platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64]
steps:
@@ -42,11 +51,6 @@ 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:
@@ -58,15 +62,20 @@ jobs:
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}")
# Convert platform name for output (replace - with _)
PLATFORM_KEY="${{ matrix.platform }}"
PLATFORM_KEY="${PLATFORM_KEY//-/_}"
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "skip_${PLATFORM_KEY}=true" >> $GITHUB_OUTPUT
echo "✓ ${PKG_NAME}@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
echo "skip_${PLATFORM_KEY}=false" >> $GITHUB_OUTPUT
echo "→ ${PKG_NAME}@${VERSION} needs publishing"
fi
- name: Update version
- name: Update version in package.json
if: steps.check.outputs.skip != 'true'
run: |
VERSION="${{ inputs.version }}"
@@ -94,15 +103,109 @@ jobs:
fi
bun build src/cli/index.ts --compile --minify --target=$TARGET --outfile=$OUTPUT
echo "Built binary:"
ls -lh "$OUTPUT"
- name: Compress binary
if: steps.check.outputs.skip != 'true'
run: |
PLATFORM="${{ matrix.platform }}"
cd packages/${PLATFORM}
if [ "$PLATFORM" = "windows-x64" ]; then
# Windows: use 7z (pre-installed on windows-latest)
7z a -tzip ../../binary-${PLATFORM}.zip bin/ package.json
else
# Unix: use tar.gz
tar -czvf ../../binary-${PLATFORM}.tar.gz bin/ package.json
fi
cd ../..
echo "Compressed artifact:"
ls -lh binary-${PLATFORM}.*
- name: Upload artifact
if: steps.check.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.platform }}
path: |
binary-${{ matrix.platform }}.tar.gz
binary-${{ matrix.platform }}.zip
retention-days: 1
if-no-files-found: error
# =============================================================================
# Job 2: Publish all platforms using OIDC/Provenance
# - Runs on ubuntu-latest for ALL platforms (just downloading artifacts)
# - Uses npm Trusted Publishing (OIDC) - no NODE_AUTH_TOKEN needed
# - Fresh OIDC token at publish time avoids timeout issues
# =============================================================================
publish:
needs: build
runs-on: ubuntu-latest
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:
- 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, skipping"
else
echo "skip=false" >> $GITHUB_OUTPUT
echo "→ ${PKG_NAME}@${VERSION} will be published"
fi
- name: Download artifact
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
name: binary-${{ matrix.platform }}
path: .
- name: Extract artifact
if: steps.check.outputs.skip != 'true'
run: |
PLATFORM="${{ matrix.platform }}"
mkdir -p packages/${PLATFORM}
if [ "$PLATFORM" = "windows-x64" ]; then
unzip binary-${PLATFORM}.zip -d packages/${PLATFORM}/
else
tar -xzvf binary-${PLATFORM}.tar.gz -C packages/${PLATFORM}/
fi
echo "Extracted contents:"
ls -la packages/${PLATFORM}/
ls -la packages/${PLATFORM}/bin/
- uses: actions/setup-node@v4
if: steps.check.outputs.skip != 'true'
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- 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
npm publish --access public --provenance $TAG_ARG
env:
NPM_CONFIG_PROVENANCE: false
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
timeout-minutes: 15

View File

@@ -45,8 +45,33 @@ 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/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

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

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ yarn.lock
test-injection/
notepad.md
oauth-success.html
*.bun-build

View File

@@ -1,6 +1,5 @@
---
description: Compare HEAD with the latest published npm version and list all unpublished changes
model: anthropic/claude-haiku-4-5
---
<command-instruction>
@@ -82,3 +81,68 @@ None 또는 목록
- **Recommendation**: patch|minor|major
- **Reason**: 이유
</output-format>
<oracle-safety-review>
## Oracle 배포 안전성 검토 (사용자가 명시적으로 요청 시에만)
**트리거 키워드**: "배포 가능", "배포해도 될까", "안전한지", "리뷰", "검토", "oracle", "오라클"
사용자가 위 키워드 중 하나라도 포함하여 요청하면:
### 1. 사전 검증 실행
```bash
bun run typecheck
bun test
```
- 실패 시 → Oracle 소환 없이 즉시 "❌ 배포 불가" 보고
### 2. Oracle 소환 프롬프트
다음 정보를 수집하여 Oracle에게 전달:
```
## 배포 안전성 검토 요청
### 변경사항 요약
{위에서 분석한 변경사항 테이블}
### 주요 diff (기능별로 정리)
{각 feat/fix/refactor의 핵심 코드 변경 - 전체 diff가 아닌 핵심만}
### 검증 결과
- Typecheck: ✅/❌
- Tests: {pass}/{total} (✅/❌)
### 검토 요청사항
1. **리그레션 위험**: 기존 기능에 영향을 줄 수 있는 변경이 있는가?
2. **사이드이펙트**: 예상치 못한 부작용이 발생할 수 있는 부분은?
3. **Breaking Changes**: 외부 사용자에게 영향을 주는 변경이 있는가?
4. **Edge Cases**: 놓친 엣지 케이스가 있는가?
5. **배포 권장 여부**: SAFE / CAUTION / UNSAFE
### 요청
위 변경사항을 깊이 분석하고, 배포 안전성에 대해 판단해주세요.
리스크가 있다면 구체적인 시나리오와 함께 설명해주세요.
배포 후 모니터링해야 할 키워드가 있다면 제안해주세요.
```
### 3. Oracle 응답 후 출력 포맷
## 🔍 Oracle 배포 안전성 검토 결과
### 판정: ✅ SAFE / ⚠️ CAUTION / ❌ UNSAFE
### 리스크 분석
| 영역 | 리스크 레벨 | 설명 |
|------|-------------|------|
| ... | 🟢/🟡/🔴 | ... |
### 권장 사항
- ...
### 배포 후 모니터링 키워드
- ...
### 결론
{Oracle의 최종 판단}
</oracle-safety-review>

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>

View File

@@ -0,0 +1,519 @@
---
name: github-issue-triage
description: "Triage GitHub issues with parallel analysis. 1 issue = 1 background agent. Exhaustive pagination. Analyzes: question vs bug, project validity, resolution status, community engagement, linked PRs. Triggers: 'triage issues', 'analyze issues', 'issue report'."
---
# GitHub Issue Triage Specialist
You are a GitHub issue triage automation agent. Your job is to:
1. Fetch **EVERY SINGLE ISSUE** within a specified time range using **EXHAUSTIVE PAGINATION**
2. Launch ONE background agent PER issue for parallel analysis
3. Collect results and generate a comprehensive triage report
---
# CRITICAL: EXHAUSTIVE PAGINATION IS MANDATORY
**THIS IS THE MOST IMPORTANT RULE. VIOLATION = COMPLETE FAILURE.**
## YOU MUST FETCH ALL ISSUES. PERIOD.
| WRONG | CORRECT |
|----------|------------|
| `gh issue list --limit 100` and stop | Paginate until ZERO results returned |
| "I found 16 issues" (first page only) | "I found 61 issues after 5 pages" |
| Assuming first page is enough | Using `--limit 500` and verifying count |
| Stopping when you "feel" you have enough | Stopping ONLY when API returns empty |
### WHY THIS MATTERS
- GitHub API returns **max 100 issues per request** by default
- A busy repo can have **50-100+ issues** in 48 hours
- **MISSING ISSUES = MISSING CRITICAL BUGS = PRODUCTION OUTAGES**
- The user asked for triage, not "sample triage"
### THE ONLY ACCEPTABLE APPROACH
```bash
# ALWAYS use --limit 500 (maximum allowed)
# ALWAYS check if more pages exist
# ALWAYS continue until empty result
gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author
```
**If the result count equals your limit, THERE ARE MORE ISSUES. KEEP FETCHING.**
---
## PHASE 1: Issue Collection (EXHAUSTIVE Pagination)
### 1.1 Determine Repository and Time Range
Extract from user request:
- `REPO`: Repository in `owner/repo` format (default: current repo via `gh repo view --json nameWithOwner -q .nameWithOwner`)
- `TIME_RANGE`: Hours to look back (default: 48)
---
## AGENT CATEGORY RATIO RULES
**Philosophy**: Use the cheapest agent that can do the job. Expensive agents = waste unless necessary.
### Default Ratio: `unspecified-low:8, quick:1, writing:1`
| Category | Ratio | Use For | Cost |
|----------|-------|---------|------|
| `unspecified-low` | 80% | Standard issue analysis - read issue, fetch comments, categorize | $ |
| `quick` | 10% | Trivial issues - obvious duplicates, spam, clearly resolved | ¢ |
| `writing` | 10% | Report generation, response drafting, summary synthesis | $$ |
### When to Override Default Ratio
| Scenario | Recommended Ratio | Reason |
|----------|-------------------|--------|
| Bug-heavy triage | `unspecified-low:7, quick:2, writing:1` | More simple duplicates |
| Feature request triage | `unspecified-low:6, writing:3, quick:1` | More response drafting needed |
| Security audit | `unspecified-high:5, unspecified-low:4, writing:1` | Deeper analysis required |
| First-pass quick filter | `quick:8, unspecified-low:2` | Just categorize, don't analyze deeply |
### Agent Assignment Algorithm
```typescript
function assignAgentCategory(issues: Issue[], ratio: Record<string, number>): Map<Issue, string> {
const assignments = new Map<Issue, string>();
const total = Object.values(ratio).reduce((a, b) => a + b, 0);
// Calculate counts for each category
const counts: Record<string, number> = {};
for (const [category, weight] of Object.entries(ratio)) {
counts[category] = Math.floor(issues.length * (weight / total));
}
// Assign remaining to largest category
const assigned = Object.values(counts).reduce((a, b) => a + b, 0);
const remaining = issues.length - assigned;
const largestCategory = Object.entries(ratio).sort((a, b) => b[1] - a[1])[0][0];
counts[largestCategory] += remaining;
// Distribute issues
let issueIndex = 0;
for (const [category, count] of Object.entries(counts)) {
for (let i = 0; i < count && issueIndex < issues.length; i++) {
assignments.set(issues[issueIndex++], category);
}
}
return assignments;
}
```
### Category Selection Heuristics
**Before launching agents, pre-classify issues for smarter category assignment:**
| Issue Signal | Assign To | Reason |
|--------------|-----------|--------|
| Has `duplicate` label | `quick` | Just confirm and close |
| Has `wontfix` label | `quick` | Just confirm and close |
| No comments, < 50 char body | `quick` | Likely spam or incomplete |
| Has linked PR | `quick` | Already being addressed |
| Has `bug` label + long body | `unspecified-low` | Needs proper analysis |
| Has `feature` label | `unspecified-low` or `writing` | May need response |
| User is maintainer | `quick` | They know what they're doing |
| 5+ comments | `unspecified-low` | Complex discussion |
| Needs response drafted | `writing` | Prose quality matters |
---
### 1.2 Exhaustive Pagination Loop
# STOP. READ THIS BEFORE EXECUTING.
**YOU WILL FETCH EVERY. SINGLE. ISSUE. NO EXCEPTIONS.**
## THE GOLDEN RULE
```
NEVER use --limit 100. ALWAYS use --limit 500.
NEVER stop at first result. ALWAYS verify you got everything.
NEVER assume "that's probably all". ALWAYS check if more exist.
```
## MANDATORY PAGINATION LOOP (COPY-PASTE THIS EXACTLY)
You MUST execute this EXACT pagination loop. DO NOT simplify. DO NOT skip iterations.
```bash
#!/bin/bash
# MANDATORY PAGINATION - Execute this EXACTLY as written
REPO="code-yeongyu/oh-my-opencode" # or use: gh repo view --json nameWithOwner -q .nameWithOwner
TIME_RANGE=48 # hours
CUTOFF_DATE=$(date -v-${TIME_RANGE}H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -d "${TIME_RANGE} hours ago" -Iseconds)
echo "=== EXHAUSTIVE PAGINATION START ==="
echo "Repository: $REPO"
echo "Cutoff date: $CUTOFF_DATE"
echo ""
# STEP 1: First fetch with --limit 500
echo "[Page 1] Fetching issues..."
FIRST_FETCH=$(gh issue list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author)
FIRST_COUNT=$(echo "$FIRST_FETCH" | jq 'length')
echo "[Page 1] Raw count: $FIRST_COUNT"
# STEP 2: Filter by time range
ALL_ISSUES=$(echo "$FIRST_FETCH" | jq --arg cutoff "$CUTOFF_DATE" \
'[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]')
FILTERED_COUNT=$(echo "$ALL_ISSUES" | jq 'length')
echo "[Page 1] After time filter: $FILTERED_COUNT issues"
# STEP 3: CHECK IF MORE PAGES NEEDED
# If we got exactly 500, there are MORE issues!
if [ "$FIRST_COUNT" -eq 500 ]; then
echo ""
echo "WARNING: Got exactly 500 results. MORE PAGES EXIST!"
echo "Continuing pagination..."
PAGE=2
LAST_ISSUE_NUMBER=$(echo "$FIRST_FETCH" | jq '.[- 1].number')
# Keep fetching until we get less than 500
while true; do
echo ""
echo "[Page $PAGE] Fetching more issues..."
# Use search API with pagination for more results
NEXT_FETCH=$(gh issue list --repo $REPO --state all --limit 500 \
--json number,title,state,createdAt,updatedAt,labels,author \
--search "created:<$(echo "$FIRST_FETCH" | jq -r '.[-1].createdAt')")
NEXT_COUNT=$(echo "$NEXT_FETCH" | jq 'length')
echo "[Page $PAGE] Raw count: $NEXT_COUNT"
if [ "$NEXT_COUNT" -eq 0 ]; then
echo "[Page $PAGE] No more results. Pagination complete."
break
fi
# Filter and merge
NEXT_FILTERED=$(echo "$NEXT_FETCH" | jq --arg cutoff "$CUTOFF_DATE" \
'[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]')
ALL_ISSUES=$(echo "$ALL_ISSUES $NEXT_FILTERED" | jq -s 'add | unique_by(.number)')
CURRENT_TOTAL=$(echo "$ALL_ISSUES" | jq 'length')
echo "[Page $PAGE] Running total: $CURRENT_TOTAL issues"
if [ "$NEXT_COUNT" -lt 500 ]; then
echo "[Page $PAGE] Less than 500 results. Pagination complete."
break
fi
PAGE=$((PAGE + 1))
# Safety limit
if [ $PAGE -gt 20 ]; then
echo "SAFETY LIMIT: Stopped at page 20"
break
fi
done
fi
# STEP 4: FINAL COUNT
FINAL_COUNT=$(echo "$ALL_ISSUES" | jq 'length')
echo ""
echo "=== EXHAUSTIVE PAGINATION COMPLETE ==="
echo "Total issues found: $FINAL_COUNT"
echo ""
# STEP 5: Verify we got everything
if [ "$FINAL_COUNT" -lt 10 ]; then
echo "WARNING: Only $FINAL_COUNT issues found. Double-check time range!"
fi
```
## VERIFICATION CHECKLIST (MANDATORY)
BEFORE proceeding to Phase 2, you MUST verify:
```
CHECKLIST:
[ ] Executed the FULL pagination loop above (not just --limit 500 once)
[ ] Saw "EXHAUSTIVE PAGINATION COMPLETE" in output
[ ] Counted total issues: _____ (fill this in)
[ ] If first fetch returned 500, continued to page 2+
[ ] Used --state all (not just open)
```
**If you did NOT see "EXHAUSTIVE PAGINATION COMPLETE", you did it WRONG. Start over.**
## ANTI-PATTERNS (WILL CAUSE FAILURE)
| NEVER DO THIS | Why It Fails |
|------------------|--------------|
| Single `gh issue list --limit 500` | If 500 returned, you missed the rest! |
| `--limit 100` | Misses 80%+ of issues in active repos |
| Stopping at first fetch | GitHub paginates - you got 1 page of N |
| Not counting results | Can't verify completeness |
| Filtering only by createdAt | Misses updated issues |
| Assuming small repos have few issues | Even small repos can have bursts |
**THE LOOP MUST RUN UNTIL:**
1. Fetch returns 0 results, OR
2. Fetch returns less than 500 results
**IF FIRST FETCH RETURNS EXACTLY 500 = YOU MUST CONTINUE FETCHING.**
### 1.3 Also Fetch All PRs (For Bug Correlation)
```bash
# Same pagination logic for PRs
gh pr list --repo $REPO --state all --limit 500 --json number,title,state,createdAt,updatedAt,labels,author,body,headRefName | \
jq --arg cutoff "$CUTOFF_DATE" '[.[] | select(.createdAt >= $cutoff or .updatedAt >= $cutoff)]'
```
---
## PHASE 2: Parallel Issue Analysis (1 Issue = 1 Agent)
### 2.1 Agent Distribution Formula
```
Total issues: N
Agent categories based on ratio:
- unspecified-low: floor(N * 0.8)
- quick: floor(N * 0.1)
- writing: ceil(N * 0.1) # For report generation
```
### 2.2 Launch Background Agents
**MANDATORY: Each issue gets its own dedicated background agent.**
For each issue, launch:
```typescript
delegate_task(
category="unspecified-low", // or quick/writing per ratio
load_skills=[],
run_in_background=true,
prompt=`
## TASK
Analyze GitHub issue #${issue.number} for ${REPO}.
## ISSUE DATA
- Number: #${issue.number}
- Title: ${issue.title}
- State: ${issue.state}
- Author: ${issue.author.login}
- Created: ${issue.createdAt}
- Updated: ${issue.updatedAt}
- Labels: ${issue.labels.map(l => l.name).join(', ')}
## ISSUE BODY
${issue.body}
## FETCH COMMENTS
Use: gh issue view ${issue.number} --repo ${REPO} --json comments
## ANALYSIS CHECKLIST
1. **TYPE**: Is this a BUG, QUESTION, FEATURE request, or INVALID?
2. **PROJECT_VALID**: Is this issue relevant to OUR project? (YES/NO/UNCLEAR)
3. **STATUS**:
- RESOLVED: Already fixed (check for linked PRs, owner comments)
- NEEDS_ACTION: Requires maintainer attention
- CAN_CLOSE: Can be closed (duplicate, out of scope, stale, answered)
- NEEDS_INFO: Missing reproduction steps or details
4. **COMMUNITY_RESPONSE**:
- NONE: No comments
- HELPFUL: Useful workarounds or info provided
- WAITING: Awaiting user response
5. **LINKED_PR**: If bug, search PRs that might fix this issue
## PR CORRELATION
Check these PRs for potential fixes:
${PR_LIST}
## RETURN FORMAT
\`\`\`
#${issue.number}: ${issue.title}
TYPE: [BUG|QUESTION|FEATURE|INVALID]
VALID: [YES|NO|UNCLEAR]
STATUS: [RESOLVED|NEEDS_ACTION|CAN_CLOSE|NEEDS_INFO]
COMMUNITY: [NONE|HELPFUL|WAITING]
LINKED_PR: [#NUMBER or NONE]
SUMMARY: [1-2 sentence summary]
ACTION: [Recommended maintainer action]
DRAFT_RESPONSE: [If auto-answerable, provide English draft. Otherwise "NEEDS_MANUAL_REVIEW"]
\`\`\`
`
)
```
### 2.3 Collect All Results
Wait for all background agents to complete, then collect:
```typescript
// Store all task IDs
const taskIds: string[] = []
// Launch all agents
for (const issue of issues) {
const result = await delegate_task(...)
taskIds.push(result.task_id)
}
// Collect results
const results = []
for (const taskId of taskIds) {
const output = await background_output(task_id=taskId)
results.push(output)
}
```
---
## PHASE 3: Report Generation
### 3.1 Categorize Results
Group analyzed issues by status:
| Category | Criteria |
|----------|----------|
| **CRITICAL** | Blocking bugs, security issues, data loss |
| **CLOSE_IMMEDIATELY** | Resolved, duplicate, out of scope, stale |
| **AUTO_RESPOND** | Can answer with template (version update, docs link) |
| **NEEDS_INVESTIGATION** | Requires manual debugging or design decision |
| **FEATURE_BACKLOG** | Feature requests for prioritization |
| **NEEDS_INFO** | Missing details, request more info |
### 3.2 Generate Report
```markdown
# Issue Triage Report
**Repository:** ${REPO}
**Time Range:** Last ${TIME_RANGE} hours
**Generated:** ${new Date().toISOString()}
**Total Issues Analyzed:** ${issues.length}
## Summary
| Category | Count |
|----------|-------|
| CRITICAL | N |
| Close Immediately | N |
| Auto-Respond | N |
| Needs Investigation | N |
| Feature Requests | N |
| Needs Info | N |
---
## 1. CRITICAL (Immediate Action Required)
[List issues with full details]
## 2. Close Immediately
[List with closing reason and template response]
## 3. Auto-Respond (Template Answers)
[List with draft responses ready to post]
## 4. Needs Investigation
[List with investigation notes]
## 5. Feature Backlog
[List for prioritization]
## 6. Needs More Info
[List with template questions to ask]
---
## Response Templates
### Fixed in Version X
\`\`\`
This issue was resolved in vX.Y.Z via PR #NNN.
Please update: \`bunx oh-my-opencode@X.Y.Z install\`
If the issue persists, please reopen with \`opencode --print-logs\` output.
\`\`\`
### Needs More Info
\`\`\`
Thank you for reporting. To investigate, please provide:
1. \`opencode --print-logs\` output
2. Your configuration file
3. Minimal reproduction steps
Labeling as \`needs-info\`. Auto-closes in 7 days without response.
\`\`\`
### Out of Scope
\`\`\`
Thank you for reaching out. This request falls outside the scope of this project.
[Suggest alternative or explanation]
\`\`\`
```
---
## ANTI-PATTERNS (BLOCKING VIOLATIONS)
## IF YOU DO ANY OF THESE, THE TRIAGE IS INVALID
| Violation | Why It's Wrong | Severity |
|-----------|----------------|----------|
| **Using `--limit 100`** | Misses 80%+ of issues in active repos | CRITICAL |
| **Stopping at first fetch** | GitHub paginates - you only got page 1 | CRITICAL |
| **Not counting results** | Can't verify completeness | CRITICAL |
| Batching issues (7 per agent) | Loses detail, harder to track | HIGH |
| Sequential agent calls | Slow, doesn't leverage parallelism | HIGH |
| Skipping PR correlation | Misses linked fixes for bugs | MEDIUM |
| Generic responses | Each issue needs specific analysis | MEDIUM |
## MANDATORY VERIFICATION BEFORE PHASE 2
```
CHECKLIST:
[ ] Used --limit 500 (not 100)
[ ] Used --state all (not just open)
[ ] Counted issues: _____ total
[ ] Verified: if count < 500, all issues fetched
[ ] If count = 500, fetched additional pages
```
**DO NOT PROCEED TO PHASE 2 UNTIL ALL BOXES ARE CHECKED.**
---
## EXECUTION CHECKLIST
- [ ] Fetched ALL pages of issues (pagination complete)
- [ ] Fetched ALL pages of PRs for correlation
- [ ] Launched 1 agent per issue (not batched)
- [ ] All agents ran in background (parallel)
- [ ] Collected all results before generating report
- [ ] Report includes draft responses where applicable
- [ ] Critical issues flagged at top
---
## Quick Start
When invoked, immediately:
1. `gh repo view --json nameWithOwner -q .nameWithOwner` (get current repo)
2. Parse user's time range request (default: 48 hours)
3. Exhaustive pagination for issues AND PRs
4. Launch N background agents (1 per issue)
5. Collect all results
6. Generate categorized report with action items

View File

@@ -1,12 +1,24 @@
# PROJECT KNOWLEDGE BASE
**Generated:** 2026-01-23T02:09:00+09:00
**Commit:** 0e18efc7
**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
OpenCode plugin: 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, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
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
@@ -14,14 +26,14 @@ OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemi
oh-my-opencode/
├── src/
│ ├── agents/ # 10 AI agents - see src/agents/AGENTS.md
│ ├── hooks/ # 31 lifecycle hooks - see src/hooks/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/ # 50 cross-cutting utilities - see src/shared/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 (590 lines)
│ └── 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)
@@ -36,9 +48,10 @@ oh-my-opencode/
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
| Add MCP | `src/mcp/` | Create config, add to index.ts |
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
| Add command | `src/features/builtin-commands/` | Add template + register in commands.ts |
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` |
| Background agents | `src/features/background-agent/` | manager.ts (1335 lines) |
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (771 lines) |
| Background agents | `src/features/background-agent/` | manager.ts (1377 lines) |
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (752 lines) |
## TDD (Test-Driven Development)
@@ -50,8 +63,8 @@ oh-my-opencode/
**Rules:**
- NEVER write implementation before test
- NEVER delete failing tests - fix the code
- Test file: `*.test.ts` alongside source
- BDD comments: `#given`, `#when`, `#then`
- Test file: `*.test.ts` alongside source (100 test files)
- BDD comments: `//#given`, `//#when`, `//#then`
## CONVENTIONS
@@ -60,7 +73,7 @@ oh-my-opencode/
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
- **Exports**: Barrel pattern via index.ts
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
- **Testing**: BDD comments, 90 test files
- **Testing**: BDD comments, 100 test files
- **Temperature**: 0.1 for code agents, max 0.3
## ANTI-PATTERNS
@@ -85,13 +98,13 @@ oh-my-opencode/
| Agent | Model | Purpose |
|-------|-------|---------|
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator |
| Atlas | anthropic/claude-opus-4-5 | Master orchestrator |
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro) |
| Atlas | anthropic/claude-sonnet-4-5 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
| oracle | openai/gpt-5.2 | Consultation, debugging |
| librarian | opencode/glm-4.7-free | Docs, GitHub search |
| explore | opencode/grok-code | Fast codebase grep |
| librarian | zai-coding-plan/glm-4.7 | Docs, GitHub search (fallback: glm-4.7-free) |
| explore | anthropic/claude-haiku-4-5 | Fast codebase grep (fallback: gpt-5-mini → gpt-5-nano) |
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning |
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
## COMMANDS
@@ -99,7 +112,7 @@ oh-my-opencode/
bun run typecheck # Type check
bun run build # ESM + declarations + schema
bun run rebuild # Clean + Build
bun test # 90 test files
bun test # 100 test files
```
## DEPLOYMENT
@@ -113,12 +126,14 @@ bun test # 90 test files
| File | Lines | Description |
|------|-------|-------------|
| `src/agents/atlas.ts` | 1383 | Orchestrator, 7-section delegation |
| `src/features/background-agent/manager.ts` | 1335 | Task lifecycle, concurrency |
| `src/features/builtin-skills/skills.ts` | 1203 | Skill definitions |
| `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` | 1038 | Category-based delegation |
| `src/hooks/atlas/index.ts` | 771 | Orchestrator hook |
| `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

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>
@@ -187,7 +189,7 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
- Oracle: 設計、デバッグ (GPT 5.2 Medium)
- Frontend UI/UX Engineer: フロントエンド開発 (Gemini 3 Pro)
- Librarian: 公式ドキュメント、オープンソース実装、コードベース探索 (Claude Sonnet 4.5)
- Explore: 超高速コードベース探索 (Contextual Grep) (Grok Code)
- Explore: 超高速コードベース探索 (Contextual Grep) (Claude Haiku 4.5)
- Full LSP / AstGrep Support: 決定的にリファクタリングしましょう。
- Todo Continuation Enforcer: 途中で諦めたら、続行を強制します。これがシジフォスに岩を転がし続けさせる秘訣です。
- Comment Checker: AIが過剰なコメントを付けないようにします。シジフォスが生成したコードは、人間が書いたものと区別がつかないべきです。

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,10 +73,11 @@
[![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)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-opencode)
[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> -->
@@ -196,7 +197,7 @@ Hey please read this readme and tell me why it is different from other agent har
- Oracle: 디자인, 디버깅 (GPT 5.2 Medium)
- Frontend UI/UX Engineer: 프론트엔드 개발 (Gemini 3 Pro)
- Librarian: 공식 문서, 오픈 소스 구현, 코드베이스 탐색 (Claude Sonnet 4.5)
- Explore: 엄청나게 빠른 코드베이스 탐색 (Contextual Grep) (Grok Code)
- Explore: 엄청나게 빠른 코드베이스 탐색 (Contextual Grep) (Claude Haiku 4.5)
- 완전한 LSP / AstGrep 지원: 결정적으로 리팩토링합니다.
- TODO 연속 강제: 에이전트가 중간에 멈추면 계속하도록 강제합니다. **이것이 Sisyphus가 그 바위를 굴리게 하는 것입니다.**
- 주석 검사기: AI가 과도한 주석을 추가하는 것을 방지합니다. Sisyphus가 생성한 코드는 인간이 작성한 것과 구별할 수 없어야 합니다.

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!
>
@@ -196,7 +196,7 @@ Meet our main agent: Sisyphus (Opus 4.5 High). Below are the tools Sisyphus uses
- Oracle: Design, debugging (GPT 5.2 Medium)
- Frontend UI/UX Engineer: Frontend development (Gemini 3 Pro)
- Librarian: Official docs, open source implementations, codebase exploration (Claude Sonnet 4.5)
- Explore: Blazing fast codebase exploration (Contextual Grep) (Grok Code)
- Explore: Blazing fast codebase exploration (Contextual Grep) (Claude Haiku 4.5)
- Full LSP / AstGrep Support: Refactor decisively.
- Todo Continuation Enforcer: Forces the agent to continue if it quits halfway. **This is what keeps Sisyphus rolling that boulder.**
- Comment Checker: Prevents AI from adding excessive comments. Code generated by Sisyphus should be indistinguishable from human-written code.

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>
@@ -191,7 +193,7 @@
- Oracle设计、调试 (GPT 5.2 Medium)
- Frontend UI/UX Engineer前端开发 (Gemini 3 Pro)
- Librarian官方文档、开源实现、代码库探索 (Claude Sonnet 4.5)
- Explore极速代码库探索上下文感知 Grep(Grok Code)
- Explore极速代码库探索上下文感知 Grep(Claude Haiku 4.5)
- 完整 LSP / AstGrep 支持:果断重构。
- Todo 继续执行器:如果智能体中途退出,强制它继续。**这就是让 Sisyphus 继续推动巨石的关键。**
- 注释检查器:防止 AI 添加过多注释。Sisyphus 生成的代码应该与人类编写的代码无法区分。

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,14 +71,17 @@
"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"
"atlas",
"stop-continuation-guard"
]
}
},
@@ -216,6 +221,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 +392,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 +563,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 +734,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 +905,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 +1076,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 +1247,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 +1418,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 +1589,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 +1760,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 +1931,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 +2102,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 +2273,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": {}
}
}
}
@@ -1786,7 +2376,8 @@
"enum": [
"low",
"medium",
"high"
"high",
"xhigh"
]
},
"textVerbosity": {
@@ -2169,6 +2760,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

@@ -1,6 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"configVersion": 1,
"workspaces": {
"": {
"name": "oh-my-opencode",
@@ -18,6 +18,7 @@
"jsonc-parser": "^3.3.1",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"vscode-jsonrpc": "^8.2.0",
"zod": "^4.1.8",
},
"devDependencies": {
@@ -27,13 +28,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.10",
"oh-my-opencode-darwin-x64": "3.1.10",
"oh-my-opencode-linux-arm64": "3.1.10",
"oh-my-opencode-linux-arm64-musl": "3.1.10",
"oh-my-opencode-linux-x64": "3.1.10",
"oh-my-opencode-linux-x64-musl": "3.1.10",
"oh-my-opencode-windows-x64": "3.1.10",
},
},
},
@@ -43,41 +44,41 @@
"@code-yeongyu/comment-checker",
],
"packages": {
"@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="],
"@ast-grep/cli": ["@ast-grep/cli@0.40.5", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.5", "@ast-grep/cli-darwin-x64": "0.40.5", "@ast-grep/cli-linux-arm64-gnu": "0.40.5", "@ast-grep/cli-linux-x64-gnu": "0.40.5", "@ast-grep/cli-win32-arm64-msvc": "0.40.5", "@ast-grep/cli-win32-ia32-msvc": "0.40.5", "@ast-grep/cli-win32-x64-msvc": "0.40.5" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-yVXL7Gz0WIHerQLf+MVaVSkhIhidtWReG5akNVr/JS9OVCVkSdz7gWm7H8jVv2M9OO1tauuG76K3UaRGBPu5lQ=="],
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UehY2MMUkdJbsriP7NKc6+uojrqPn7d1Cl0em+WAkee7Eij81VdyIjRsRxtZSLh440ZWQBHI3PALZ9RkOO8pKQ=="],
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T9CzwJ1GqQhnANdsu6c7iT1akpvTVMK+AZrxnhIPv33Ze5hrXUUkqan+j4wUAukRJDqU7u94EhXLSLD+5tcJ8g=="],
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RFDJ2ZxUbT0+grntNlOLJx7wa9/ciVCeaVtQpQy8WJJTvXvkY0etl8Qlh2TmO2x2yr+i0Z6aMJi4IG/Yx5ghTQ=="],
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ez9b2zKvXU8f4ghhjlqYvbx6tWCKJTuVlNVqDDfjqwwhGeiTYfnzMlSVat4ElYRMd21gLtXZIMy055v2f21Ztg=="],
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4p55gnTQ1mMFCyqjtM7bH9SB9r16mkwXtUcJQGX1YgFG4WD+QG8rC4GwSuNNZcdlYaOQuTWrgUEQ9z5K06UXfg=="],
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VXa2L1IEYD66AMb0GuG7VlMMbPmEGoJUySWDcwSZo/D9neiry3MJ41LQR5oTG2HyhIPBsf9umrXnmuRq66BviA=="],
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2MXFceuwvrO+OQ6zFGoJ6wbATXn46HWwW79j4UPrXYJzVl97jRyjJOIQTJOzTflsk02fjP98DQkfvbXt2dl3Q=="],
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-GQC5162eIOWXR2eQQ6Knzg7/8Trp5E1ODJkaErf0IubdQrZBGqj5AAcQPcWgPbbnmktjIp0H4NraPpOJ9eJ22A=="],
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-E/I1xpF/RQL2fo1CQsQfTxyDLnChsbZ+ERrQHKuF1FI4WrkaPOBibpqda60QgVmUcgOGZyZ/GRb3iKEVWPsQNQ=="],
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-YiZdnQZsSlXQTMsZJop/Ux9MmUGfuRvC2x/UbFgrt5OBSYxND+yoiMc0WcA3WG+wU+tt4ZkB5HUea3r/IkOLYA=="],
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9h12OQu1BR0GxHEtT+Z4QkJk3LLWLiKwjBkjXUGlASHYDPTyLcs85KwDLeFHs4BwarF8TDdF+KySvB9WPGl/nQ=="],
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-MHkCxCITVTr8sY9CcVqNKbfUzMa3Hc6IilGXad0Clnw2vNmPfWqSky+hU/UTerr5YHWwWfAVURH7ANZgirtx0Q=="],
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n2+3WynEWFHhXg6KDgjwWQ0UEtIvqUITFbKEk5cDkUYrzYhg/A6kj0qauPwRbVMoJms49vtsNpLkzzqyunio5g=="],
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/MJ5un7yxlClaaxou9eYl+Kr2xr/yTtYtTq5aLBWjPWA6dmmJ1nAJgx5zKHVuplFXFBrFDQk3paEgAETMTGcrA=="],
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
"@ast-grep/napi": ["@ast-grep/napi@0.40.5", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.5", "@ast-grep/napi-darwin-x64": "0.40.5", "@ast-grep/napi-linux-arm64-gnu": "0.40.5", "@ast-grep/napi-linux-arm64-musl": "0.40.5", "@ast-grep/napi-linux-x64-gnu": "0.40.5", "@ast-grep/napi-linux-x64-musl": "0.40.5", "@ast-grep/napi-win32-arm64-msvc": "0.40.5", "@ast-grep/napi-win32-ia32-msvc": "0.40.5", "@ast-grep/napi-win32-x64-msvc": "0.40.5" } }, "sha512-hJA62OeBKUQT68DD2gDyhOqJxZxycqg8wLxbqjgqSzYttCMSDL9tiAQ9abgekBYNHudbJosm9sWOEbmCDfpX2A=="],
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F072fGN0WTq7KI3okuEnkGJVEHLbi56Bw1H6NAMf7j2mJJeQWsRyGOMcyNnUXZDeNdvoMH0OB2a5wwUegY/nQ=="],
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-dJMidHZhhxuLBYNi6/FKI812jQ7wcFPSKkVPwviez2D+KvYagapUMAV/4dJ7FCORfguVk8Y0jpPAlYmWRT5nvA=="],
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tO+VW5GDhT9jGkKOK+3b8+ohKjC98WTzn7wSskd/myyhK3oYL1WTKqCm07WSYBZOJvb3z+WaX+wOUrc4bvtyQ=="],
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-nBRCbyoS87uqkaw4Oyfe5VO+SRm2B+0g0T8ME69Qry9ShMf41a2bTdpcQx9e8scZPogq+CTwDHo3THyBV71l9w=="],
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A=="],
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA=="],
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DP4oDbq7f/1A2hRTFLhJfDFR6aI5mRWdEfKfHzRItmlKsR9WlcEl1qDJs/zX9R2EEtIDsSKRzuJNfJllY3/W8Q=="],
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-BRZUvVBPUNpWPo6Ns8chXVzxHPY+k9gpsubGTHy92Q26ecZULd/dTkWWdnvfhRqttsSQ9Pe/XQdi5+hDQ6RYcg=="],
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w=="],
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-y95zSEwc7vhxmcrcH0GnK4ZHEBQrmrszRBNQovzaciF9GUqEcCACNLoBesn4V47IaOp4fYgD2/EhGRTIBFb2Ug=="],
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-K/u8De62iUnFCzVUs7FBdTZ2Jrgc5/DLHqjpup66KxZ7GIM9/HGME/O8aSoPkpcAeCD4TiTZ11C1i5p5H98hTg=="],
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-dqm5zg/o4Nh4VOQPEpMS23ot8HVd22gG0eg01t4CFcZeuzyuSgBlOL3N7xLbz3iH2sVkk7keuBwAzOIpTqziNQ=="],
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
@@ -85,17 +86,17 @@
"@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.6.1", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-BBremX+Y5aW8sTzlhHrLsKParupYkPOVUYmq9STrlWvBvfAme6w5IWuZCLl6nHIQScRDdvGdrAjPycJC86EZFA=="],
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.19", "", { "dependencies": { "@opencode-ai/sdk": "1.1.19", "zod": "4.1.8" } }, "sha512-Q6qBEjHb/dJMEw4BUqQxEswTMxCCHUpFMMb6jR8HTTs8X/28XRkKt5pHNPA82GU65IlSoPRph+zd8LReBDN53Q=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.47", "", { "dependencies": { "@opencode-ai/sdk": "1.1.47", "zod": "4.1.8" } }, "sha512-gNMPz72altieDfLhUw3VAT1xbduKi3w3wZ57GLeS7qU9W474HdvdIiLBnt2Xq3U7Ko0/0tvK3nzCker6IIDqmQ=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.47", "", {}, "sha512-s3PBHwk1sP6Zt/lJxIWSBWZ1TnrI1nFxSP97LCODUytouAQgbygZ1oDH7O2sGMBEuGdA8B1nNSPla0aRSN3IpA=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
"@types/picomatch": ["@types/picomatch@3.0.2", "", {}, "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA=="],
@@ -107,9 +108,9 @@
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -117,7 +118,7 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
@@ -127,7 +128,7 @@
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -183,11 +184,11 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -225,19 +226,19 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.0.0-beta.11", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7cFv2bbz9HTY7sshgVTu+IhvYf7CT0czDYqHEB+dYfEqFU6TaoSMimq6uHqcWegUUR1T7PNmc0dyjYVw69FeVA=="],
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.1.10", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-6qsZQtrtBYZLufcXTTuUUMEG9PoG9Y98pX+HFVn2xHIEc6GpwR6i5xY8McFHmqPkC388tzybD556JhKqPX7Pnw=="],
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.0.0-beta.11", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-rGAbDdUySWITIdm2yiuNFB9lFYaSXT8LMtg97LTlOO5vZbI3M+obIS3QlIkBtAhgOTIPB7Ni+T0W44OmJpHoYA=="],
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.1.10", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-I1tQQbcpSBvLGXTO652mBqlyIpwYhYuIlSJmrSM33YRGBiaUuhMASnHQsms+E0eC3U/TOyqomU/4KPnbWyxs4w=="],
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.0.0-beta.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-F9dqwWwGAdqeSkE7Tre5DmHQXwDpU2Z8Jk0lwTJMLj+kMqYFDVPjLPo4iVUdwPpxpmm0pR84u/oonG/2+84/zw=="],
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.1.10", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-r6Rm5Ru/WwcBKKuPIP0RreI0gnf+MYRV0mmzPBVhMZdPWSC/eTT3GdyqFDZ4cCN76n5aea0sa5PPW7iPF+Uw6Q=="],
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.0.0-beta.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-H+zOtHkHd+TmdPj64M1A0zLOk7OHIK4C8yqfLFhfizOIBffT1yOhAs6EpK3EqPhfPLu54ADgcQcu8W96VP24UA=="],
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.1.10", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UVo5OWO92DPIFhoEkw0tj8IcZyUKOG6NlFs1+tSExz7qrgkr0IloxpLslGMmdc895xxpljrr/FobYktLxyJbcg=="],
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.0.0-beta.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-IG+KODTJ8rs6cEJ2wN6Zpr6YtvCS5OpYP6jBdGJltmUpjQdMhdMsaY3ysZk+9Vxpx2KC3xj5KLHV1USg3uBTeg=="],
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.1.10", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-3g99z2FweMzHSUYuzgU0E2H0kjVmtOhPZdavwVqcHQtLQ9NNhwfnIvj3yFBif+kGJphP9RDnByC1oA8Q26UrCg=="],
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.0.0-beta.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-irV+AuWrHqNm7VT7HO56qgymR0+vEfJbtB3vCq68kprH2V4NQmGp2MNKIYPnUCYL7NEK3H2NX+h06YFZJ/8ELQ=="],
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.1.10", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-2HS9Ju0Cr433lMFJtu/7bShApOJywp+zmVCduQUBWFi3xbX1nm5sJwWDhw1Wx+VcqHEuJl/SQzWPE4vaqkEQng=="],
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.0.0-beta.11", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-exZ/NEwGBlxyWszN7dvOfzbYX0cuhBZXftqAAFOlVP26elDHdo+AmSmLR/4cJyzpR9nCWz4xvl/RYF84bY6OEA=="],
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.1.10", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-QLncZJSlWmmcuXrAVKIH6a9Om1Ym6pkhG4hAxaD5K5aF1jw2QFsadjoT12VNq2WzQb+Pg5Y6IWvoow0ZR0aEvw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -303,12 +304,16 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
"@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
}
}

View File

@@ -21,13 +21,14 @@ A Category is an agent configuration preset optimized for specific domains.
| Category | Default Model | Use Cases |
|----------|---------------|-----------|
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
| `artistry` | `google/gemini-3-pro-preview` (max) | Highly creative/artistic tasks, novel ideas |
| `deep` | `openai/gpt-5.2-codex` (medium) | Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding. |
| `artistry` | `google/gemini-3-pro` (max) | Highly creative/artistic tasks, novel ideas |
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
| `writing` | `google/gemini-3-flash-preview` | Documentation, prose, technical writing |
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
### Usage
@@ -70,12 +71,12 @@ A Skill is a mechanism that injects **specialized knowledge (Context)** and **to
### Usage
Add desired skill names to the `skills` array.
Add desired skill names to the `load_skills` array.
```typescript
delegate_task(
category="quick",
skills=["git-master"],
load_skills=["git-master"],
prompt="Commit current changes. Follow commit message style."
)
```
@@ -110,17 +111,17 @@ You can create powerful specialized agents by combining Categories and Skills.
### 🎨 The Designer (UI Implementation)
- **Category**: `visual-engineering`
- **Skills**: `["frontend-ui-ux", "playwright"]`
- **load_skills**: `["frontend-ui-ux", "playwright"]`
- **Effect**: Implements aesthetic UI and verifies rendering results directly in browser.
### 🏗️ The Architect (Design Review)
- **Category**: `ultrabrain`
- **Skills**: `[]` (pure reasoning)
- **load_skills**: `[]` (pure reasoning)
- **Effect**: Leverages GPT-5.2's logical reasoning for in-depth system architecture analysis.
### ⚡ The Maintainer (Quick Fixes)
- **Category**: `quick`
- **Skills**: `["git-master"]`
- **load_skills**: `["git-master"]`
- **Effect**: Uses cost-effective models to quickly fix code and generate clean commits.
---
@@ -131,7 +132,7 @@ When delegating, **clear and specific** prompts are essential. Include these 7 e
1. **TASK**: What needs to be done? (single objective)
2. **EXPECTED OUTCOME**: What is the deliverable?
3. **REQUIRED SKILLS**: Which skills should be used?
3. **REQUIRED SKILLS**: Which skills should be loaded via `load_skills`?
4. **REQUIRED TOOLS**: Which tools must be used? (whitelist)
5. **MUST DO**: What must be done (constraints)
6. **MUST NOT DO**: What must never be done
@@ -177,7 +178,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

@@ -134,7 +134,41 @@ bunx oh-my-opencode run [prompt]
---
## 6. `auth` - Authentication Management
## 6. `mcp oauth` - MCP OAuth Management
Manages OAuth 2.1 authentication for remote MCP servers.
### Usage
```bash
# Login to an OAuth-protected MCP server
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
# Login with explicit client ID and scopes
bunx oh-my-opencode mcp oauth login my-api --server-url https://api.example.com --client-id my-client --scopes "read,write"
# Remove stored OAuth tokens
bunx oh-my-opencode mcp oauth logout <server-name>
# Check OAuth token status
bunx oh-my-opencode mcp oauth status [server-name]
```
### Options
| Option | Description |
|--------|-------------|
| `--server-url <url>` | MCP server URL (required for login) |
| `--client-id <id>` | OAuth client ID (optional if server supports Dynamic Client Registration) |
| `--scopes <scopes>` | Comma-separated OAuth scopes |
### Token Storage
Tokens are stored in `~/.config/opencode/mcp-oauth.json` with `0600` permissions (owner read/write only). Key format: `{serverHost}/{resource}`.
---
## 7. `auth` - Authentication Management
Manages Google Antigravity OAuth authentication. Required for using Gemini models.
@@ -153,7 +187,7 @@ bunx oh-my-opencode auth status
---
## 7. Configuration Files
## 8. Configuration Files
The CLI searches for configuration files in the following locations (in priority order):
@@ -175,7 +209,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",
},
},
}
@@ -183,7 +217,7 @@ Configuration files support **JSONC (JSON with Comments)** format. You can use c
---
## 8. Troubleshooting
## 9. Troubleshooting
### "OpenCode version too old" Error
@@ -213,7 +247,7 @@ bunx oh-my-opencode doctor --category authentication
---
## 9. Non-Interactive Mode
## 10. Non-Interactive Mode
Use the `--no-tui` option for CI/CD environments.
@@ -227,7 +261,7 @@ bunx oh-my-opencode doctor --json > doctor-report.json
---
## 10. Developer Information
## 11. Developer Information
### CLI Structure

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
@@ -70,7 +163,39 @@ Override built-in agent settings:
}
```
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`, `category`, `variant`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `providerOptions`.
### Additional Agent Options
| Option | Type | Description |
| ------------------- | ------- | ----------------------------------------------------------------------------------------------- |
| `category` | string | Category name to inherit model and other settings from category defaults |
| `variant` | string | Model variant (e.g., `max`, `high`, `medium`, `low`, `xhigh`) |
| `maxTokens` | number | Maximum tokens for response. Passed directly to OpenCode SDK. |
| `thinking` | object | Extended thinking configuration for Anthropic models. See [Thinking Options](#thinking-options) below. |
| `reasoningEffort` | string | OpenAI reasoning effort level. Values: `low`, `medium`, `high`, `xhigh`. |
| `textVerbosity` | string | Text verbosity level. Values: `low`, `medium`, `high`. |
| `providerOptions` | object | Provider-specific options passed directly to OpenCode SDK. |
#### Thinking Options (Anthropic)
```json
{
"agents": {
"oracle": {
"thinking": {
"type": "enabled",
"budgetTokens": 200000
}
}
}
}
```
| Option | Type | Default | Description |
| ------------- | ------- | ------- | -------------------------------------------- |
| `type` | string | - | `enabled` or `disabled` |
| `budgetTokens`| number | - | Maximum budget tokens for extended thinking |
Use `prompt_append` to add extra instructions without replacing the default system prompt:
@@ -120,14 +245,14 @@ Or disable via `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or
}
```
Available agents: `oracle`, `librarian`, `explore`, `multimodal-looker`
Available agents: `sisyphus`, `prometheus`, `oracle`, `librarian`, `explore`, `multimodal-looker`, `metis`, `momus`, `atlas`
## Built-in Skills
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 +262,330 @@ 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`
## Skills Configuration
Configure advanced skills settings including custom skill sources, enabling/disabling specific skills, and defining custom skills.
```json
{
"skills": {
"sources": [
{ "path": "./custom-skills", "recursive": true },
"https://example.com/skill.yaml"
],
"enable": ["my-custom-skill"],
"disable": ["other-skill"],
"my-skill": {
"description": "Custom skill description",
"template": "Custom prompt template",
"from": "source-file.ts",
"model": "custom/model",
"agent": "custom-agent",
"subtask": true,
"argument-hint": "usage hint",
"license": "MIT",
"compatibility": ">= 3.0.0",
"metadata": {
"author": "Your Name"
},
"allowed-tools": ["tool1", "tool2"]
}
}
}
```
### Sources
Load skills from local directories or remote URLs:
```json
{
"skills": {
"sources": [
{ "path": "./custom-skills", "recursive": true },
{ "path": "./single-skill.yaml" },
"https://example.com/skill.yaml",
"https://raw.githubusercontent.com/user/repo/main/skills/*"
]
}
}
```
| Option | Default | Description |
| ----------- | ------- | ---------------------------------------------- |
| `path` | - | Local file/directory path or remote URL |
| `recursive` | `false` | Recursively load from directory |
| `glob` | - | Glob pattern for file selection |
### Enable/Disable Skills
```json
{
"skills": {
"enable": ["skill-1", "skill-2"],
"disable": ["disabled-skill"]
}
}
```
### Custom Skill Definition
Define custom skills directly in your config:
| Option | Default | Description |
| ---------------- | ------- | ------------------------------------------------------------------------------------ |
| `description` | - | Human-readable description of the skill |
| `template` | - | Custom prompt template for the skill |
| `from` | - | Source file to load template from |
| `model` | - | Override model for this skill |
| `agent` | - | Override agent for this skill |
| `subtask` | `false` | Whether to run as a subtask |
| `argument-hint` | - | Hint for how to use the skill |
| `license` | - | Skill license |
| `compatibility` | - | Required oh-my-opencode version compatibility |
| `metadata` | - | Additional metadata as key-value pairs |
| `allowed-tools` | - | Array of tools this skill is allowed to use |
**Example: Custom skill**
```json
{
"skills": {
"data-analyst": {
"description": "Specialized for data analysis tasks",
"template": "You are a data analyst. Focus on statistical analysis, visualization, and data interpretation.",
"model": "openai/gpt-5.2",
"allowed-tools": ["read", "bash", "lsp_diagnostics"]
}
}
}
```
## 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
@@ -238,6 +686,7 @@ Configure concurrency limits for background agent tasks. This controls how many
{
"background_task": {
"defaultConcurrency": 5,
"staleTimeoutMs": 180000,
"providerConcurrency": {
"anthropic": 3,
"openai": 5,
@@ -254,6 +703,7 @@ Configure concurrency limits for background agent tasks. This controls how many
| Option | Default | Description |
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
| `defaultConcurrency` | - | Default maximum concurrent background tasks for all providers/models |
| `staleTimeoutMs` | `180000` | Stale timeout in milliseconds - interrupt tasks with no activity for this duration (minimum: 60000 = 1 minute) |
| `providerConcurrency` | - | Per-provider concurrency limits. Keys are provider names (e.g., `anthropic`, `openai`, `google`) |
| `modelConcurrency` | - | Per-model concurrency limits. Keys are full model names (e.g., `anthropic/claude-opus-4-5`). Overrides provider limits. |
@@ -268,27 +718,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 +817,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,7 +825,14 @@ 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`, `description`, `is_unstable_agent`.
### Additional Category Options
| Option | Type | Default | Description |
| ------------------ | ------- | ------- | --------------------------------------------------------------------------------------------------- |
| `description` | string | - | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
| `is_unstable_agent`| boolean | `false` | Mark agent as unstable - forces background mode for monitoring. Auto-enabled for gemini models. |
## Model Resolution System
@@ -368,15 +894,15 @@ Each agent has a defined provider priority chain. The system tries providers in
| 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** | `glm-4.7-free` | opencode → github-copilot → anthropic |
| **explore** | `grok-code` | opencode → anthropic → github-copilot |
| **multimodal-looker** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → 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 |
| **Sisyphus** | `claude-opus-4-5` | anthropic → kimi-for-coding → zai-coding-plan → openai → google |
| **oracle** | `gpt-5.2` | openai → google → anthropic |
| **librarian** | `glm-4.7` | zai-coding-plan → opencode → anthropic |
| **explore** | `claude-haiku-4-5` | anthropic → github-copilot → opencode |
| **multimodal-looker** | `gemini-3-flash` | google → openai → zai-coding-plan → kimi-for-coding → anthropic → opencode |
| **Prometheus (Planner)** | `claude-opus-4-5` | anthropic → kimi-for-coding → openai → google |
| **Metis (Plan Consultant)** | `claude-opus-4-5` | anthropic → kimi-for-coding → openai → google |
| **Momus (Plan Reviewer)** | `gpt-5.2` | openai → anthropic → google |
| **Atlas** | `claude-sonnet-4-5` | anthropic → kimi-for-coding → openai → google |
### Category Provider Chains
@@ -384,13 +910,14 @@ Categories follow the same resolution logic:
| Category | Model (no prefix) | Provider Priority Chain |
|----------|-------------------|-------------------------|
| **visual-engineering** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → opencode |
| **ultrabrain** | `gpt-5.2-codex` | openai → anthropic → google → github-copilot → opencode |
| **artistry** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → opencode |
| **quick** | `claude-haiku-4-5` | anthropic → github-copilot → opencode → antigravity → google |
| **unspecified-low** | `claude-sonnet-4-5` | anthropic → github-copilot → opencode → antigravity → google |
| **unspecified-high** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
| **writing** | `gemini-3-flash-preview` | google → openai → anthropic → github-copilot → opencode |
| **visual-engineering** | `gemini-3-pro` | google → anthropic → zai-coding-plan |
| **ultrabrain** | `gpt-5.2-codex` | openai → google → anthropic |
| **deep** | `gpt-5.2-codex` | openai → anthropic → google |
| **artistry** | `gemini-3-pro` | google → anthropic → openai |
| **quick** | `claude-haiku-4-5` | anthropic → google → opencode |
| **unspecified-low** | `claude-sonnet-4-5` | anthropic → openai → google |
| **unspecified-high** | `claude-opus-4-5` | anthropic → openai → google |
| **writing** | `gemini-3-flash` | google → anthropic → zai-coding-plan → openai |
### Checking Your Configuration
@@ -440,10 +967,93 @@ 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`
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`, `auto-slash-command`, `sisyphus-junior-notepad`, `start-work`
**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`.
## Disabled Commands
Disable specific built-in commands via `disabled_commands` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
```json
{
"disabled_commands": ["init-deep", "start-work"]
}
```
Available commands: `init-deep`, `start-work`
## Comment Checker
Configure comment-checker hook behavior. The comment checker warns when excessive comments are added to code.
```json
{
"comment_checker": {
"custom_prompt": "Your custom warning message. Use {{comments}} placeholder for detected comments XML."
}
}
```
| Option | Default | Description |
| ------------- | ------- | -------------------------------------------------------------------------- |
| `custom_prompt` | - | Custom warning message to replace the default. Use `{{comments}}` placeholder. |
## Notification
Configure notification behavior for background task completion.
```json
{
"notification": {
"force_enable": true
}
}
```
| Option | Default | Description |
| -------------- | ------- | ---------------------------------------------------------------------------------------------- |
| `force_enable` | `false` | Force enable session-notification even if external notification plugins are detected. Default: `false`. |
## Sisyphus Tasks & Swarm
Configure Sisyphus Tasks and Swarm systems for advanced task management and multi-agent orchestration.
```json
{
"sisyphus": {
"tasks": {
"enabled": false,
"storage_path": ".sisyphus/tasks",
"claude_code_compat": false
},
"swarm": {
"enabled": false,
"storage_path": ".sisyphus/teams",
"ui_mode": "toast"
}
}
}
```
### Tasks Configuration
| Option | Default | Description |
| -------------------- | ------------------ | ------------------------------------------------------------------------- |
| `enabled` | `false` | Enable Sisyphus Tasks system |
| `storage_path` | `.sisyphus/tasks` | Storage path for tasks (relative to project root) |
| `claude_code_compat` | `false` | Enable Claude Code path compatibility mode |
### Swarm Configuration
| Option | Default | Description |
| -------------- | ------------------ | -------------------------------------------------------------- |
| `enabled` | `false` | Enable Sisyphus Swarm system for multi-agent orchestration |
| `storage_path` | `.sisyphus/teams` | Storage path for teams (relative to project root) |
| `ui_mode` | `toast` | UI mode: `toast` (notifications), `tmux` (panes), or `both` |
## MCPs
Exa, Context7 and grep.app MCP enabled by default.
@@ -485,6 +1095,38 @@ Add LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.json`
Each server supports: `command`, `extensions`, `priority`, `env`, `initialization`, `disabled`.
| Option | Type | Default | Description |
| -------------- | -------- | ------- | ---------------------------------------------------------------------- |
| `command` | array | - | Command to start the LSP server (executable + args) |
| `extensions` | array | - | File extensions this server handles (e.g., `[".ts", ".tsx"]`) |
| `priority` | number | - | Server priority when multiple servers match a file |
| `env` | object | - | Environment variables for the LSP server (key-value pairs) |
| `initialization`| object | - | Custom initialization options passed to the LSP server |
| `disabled` | boolean | `false` | Whether to disable this LSP server |
**Example with advanced options:**
```json
{
"lsp": {
"typescript-language-server": {
"command": ["typescript-language-server", "--stdio"],
"extensions": [".ts", ".tsx"],
"priority": 10,
"env": {
"NODE_OPTIONS": "--max-old-space-size=4096"
},
"initialization": {
"preferences": {
"includeInlayParameterNameHints": "all",
"includeInlayFunctionParameterTypeHints": true
}
}
}
}
}
```
## Experimental
Opt-in experimental features that may change or be removed in future versions. Use with caution.
@@ -494,7 +1136,29 @@ Opt-in experimental features that may change or be removed in future versions. U
"experimental": {
"truncate_all_tool_outputs": true,
"aggressive_truncation": true,
"auto_resume": true
"auto_resume": true,
"dynamic_context_pruning": {
"enabled": false,
"notification": "detailed",
"turn_protection": {
"enabled": true,
"turns": 3
},
"protected_tools": ["task", "todowrite", "lsp_rename"],
"strategies": {
"deduplication": {
"enabled": true
},
"supersede_writes": {
"enabled": true,
"aggressive": false
},
"purge_errors": {
"enabled": true,
"turns": 5
}
}
}
}
}
```
@@ -503,7 +1167,72 @@ Opt-in experimental features that may change or be removed in future versions. U
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. |
| `aggressive_truncation` | `false` | When token limit is exceeded, aggressively truncates tool outputs to fit within limits. More aggressive than the default truncation behavior. Falls back to summarize/revert if insufficient. |
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts the last user message and continues. |
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts last user message and continues. |
| `dynamic_context_pruning` | See below | Dynamic context pruning configuration for managing context window usage automatically. See [Dynamic Context Pruning](#dynamic-context-pruning) below. |
### Dynamic Context Pruning
Dynamic context pruning automatically manages context window by intelligently pruning old tool outputs. This feature helps maintain performance in long sessions.
```json
{
"experimental": {
"dynamic_context_pruning": {
"enabled": false,
"notification": "detailed",
"turn_protection": {
"enabled": true,
"turns": 3
},
"protected_tools": ["task", "todowrite", "todoread", "lsp_rename", "session_read", "session_write", "session_search"],
"strategies": {
"deduplication": {
"enabled": true
},
"supersede_writes": {
"enabled": true,
"aggressive": false
},
"purge_errors": {
"enabled": true,
"turns": 5
}
}
}
}
}
```
| Option | Default | Description |
| ----------------- | ------- | ----------------------------------------------------------------------------------------- |
| `enabled` | `false` | Enable dynamic context pruning |
| `notification` | `detailed` | Notification level: `off`, `minimal`, or `detailed` |
| `turn_protection` | See below | Turn protection settings - prevent pruning recent tool outputs |
#### Turn Protection
| Option | Default | Description |
| --------- | ------- | ------------------------------------------------------------ |
| `enabled` | `true` | Enable turn protection |
| `turns` | `3` | Number of recent turns to protect from pruning (1-10) |
#### Protected Tools
Tools that should never be pruned (default):
```json
["task", "todowrite", "todoread", "lsp_rename", "session_read", "session_write", "session_search"]
```
#### Pruning Strategies
| Strategy | Option | Default | Description |
| ------------------- | ------------ | ------- | ---------------------------------------------------------------------------- |
| **deduplication** | `enabled` | `true` | Remove duplicate tool calls (same tool + same args) |
| **supersede_writes**| `enabled` | `true` | Prune write inputs when file subsequently read |
| | `aggressive` | `false` | Aggressive mode: prune any write if ANY subsequent read |
| **purge_errors** | `enabled` | `true` | Prune errored tool inputs after N turns |
| | `turns` | `5` | Number of turns before pruning errors (1-20) |
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.

View File

@@ -10,19 +10,19 @@ Oh-My-OpenCode provides 10 specialized AI agents. Each has distinct expertise, o
| Agent | Model | Purpose |
|-------|-------|---------|
| **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). |
| **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). Fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro. |
| **oracle** | `openai/gpt-5.2` | Architecture decisions, code review, debugging. Read-only consultation - stellar logical reasoning and deep analysis. Inspired by AmpCode. |
| **librarian** | `opencode/glm-4.7-free` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Inspired by AmpCode. |
| **explore** | `opencode/grok-code` | Fast codebase exploration and contextual grep. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code. |
| **multimodal-looker** | `google/gemini-3-flash` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Saves tokens by having another agent process media. |
| **librarian** | `zai-coding-plan/glm-4.7` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: glm-4.7-free → claude-sonnet-4-5. |
| **explore** | `anthropic/claude-haiku-4-5` | Fast codebase exploration and contextual grep. Fallback: gpt-5-mini → gpt-5-nano. |
| **multimodal-looker** | `google/gemini-3-flash` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Fallback: gpt-5.2 → glm-4.6v → kimi-k2.5 → claude-haiku-4-5 → gpt-5-nano. |
### Planning Agents
| Agent | Model | Purpose |
|-------|-------|---------|
| **Prometheus** | `anthropic/claude-opus-4-5` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. |
| **Metis** | `anthropic/claude-sonnet-4-5` | Plan consultant - pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. |
| **Momus** | `anthropic/claude-sonnet-4-5` | Plan reviewer - validates plans against clarity, verifiability, and completeness standards. |
| **Prometheus** | `anthropic/claude-opus-4-5` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
| **Metis** | `anthropic/claude-opus-4-5` | Plan consultant - pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
| **Momus** | `openai/gpt-5.2` | Plan reviewer - validates plans against clarity, verifiability, and completeness standards. Fallback: gpt-5.2 → claude-opus-4-5 → gemini-3-pro. |
### Invoking 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
@@ -450,6 +521,37 @@ mcp:
The `skill_mcp` tool invokes these operations with full schema discovery.
#### OAuth-Enabled MCPs
Skills can define OAuth-protected remote MCP servers. OAuth 2.1 with full RFC compliance (RFC 9728, 8414, 8707, 7591) is supported:
```yaml
---
description: My API skill
mcp:
my-api:
url: https://api.example.com/mcp
oauth:
clientId: ${CLIENT_ID}
scopes: ["read", "write"]
---
```
When a skill MCP has `oauth` configured:
- **Auto-discovery**: Fetches `/.well-known/oauth-protected-resource` (RFC 9728), falls back to `/.well-known/oauth-authorization-server` (RFC 8414)
- **Dynamic Client Registration**: Auto-registers with servers supporting RFC 7591 (clientId becomes optional)
- **PKCE**: Mandatory for all flows
- **Resource Indicators**: Auto-generated from MCP URL per RFC 8707
- **Token Storage**: Persisted in `~/.config/opencode/mcp-oauth.json` (chmod 0600)
- **Auto-refresh**: Tokens refresh on 401; step-up authorization on 403 with `WWW-Authenticate`
- **Dynamic Port**: OAuth callback server uses an auto-discovered available port
Pre-authenticate via CLI:
```bash
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
```
---
## Context Injection

View File

@@ -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/glm-4.7-free`.
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
@@ -211,7 +221,7 @@ 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` |
| **Explore** | `opencode/gpt-5-nano` |
| **Librarian** | `opencode/glm-4.7-free` |
##### Setup
@@ -268,7 +278,7 @@ After everything is set up and working, ask the user:
If the user agrees, run:
```bash
gh repo star code-yeongyu/oh-my-opencode
gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true
```
**Important**: Only run the command if the user explicitly says yes. Never run it automatically without consent.

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"]
@@ -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.13",
"version": "3.1.11",
"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",
@@ -64,6 +64,7 @@
"jsonc-parser": "^3.3.1",
"picocolors": "^1.1.1",
"picomatch": "^4.0.2",
"vscode-jsonrpc": "^8.2.0",
"zod": "^4.1.8"
},
"devDependencies": {
@@ -73,13 +74,13 @@
"typescript": "^5.7.3"
},
"optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.0.0-beta.13",
"oh-my-opencode-darwin-x64": "3.0.0-beta.13",
"oh-my-opencode-linux-arm64": "3.0.0-beta.13",
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.13",
"oh-my-opencode-linux-x64": "3.0.0-beta.13",
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.13",
"oh-my-opencode-windows-x64": "3.0.0-beta.13"
"oh-my-opencode-darwin-arm64": "3.1.11",
"oh-my-opencode-darwin-x64": "3.1.11",
"oh-my-opencode-linux-arm64": "3.1.11",
"oh-my-opencode-linux-arm64-musl": "3.1.11",
"oh-my-opencode-linux-x64": "3.1.11",
"oh-my-opencode-linux-x64-musl": "3.1.11",
"oh-my-opencode-windows-x64": "3.1.11"
},
"trustedDependencies": [
"@ast-grep/cli",

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-arm64",
"version": "3.0.0-beta.13",
"version": "3.1.11",
"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.13",
"version": "3.1.11",
"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.13",
"version": "3.1.11",
"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.13",
"version": "3.1.11",
"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.13",
"version": "3.1.11",
"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.13",
"version": "3.1.11",
"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.13",
"version": "3.1.11",
"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

@@ -711,6 +711,358 @@
"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
},
{
"name": "mrdavidlaing",
"id": 227505,
"comment_id": 3813542625,
"created_at": "2026-01-28T19:51:34Z",
"repoId": 1108837393,
"pullRequestNo": 1226
},
{
"name": "Lynricsy",
"id": 62173814,
"comment_id": 3816370548,
"created_at": "2026-01-29T09:00:28Z",
"repoId": 1108837393,
"pullRequestNo": 1241
},
{
"name": "LeekJay",
"id": 39609783,
"comment_id": 3819009761,
"created_at": "2026-01-29T17:03:24Z",
"repoId": 1108837393,
"pullRequestNo": 1254
},
{
"name": "gabriel-ecegi",
"id": 35489017,
"comment_id": 3821842363,
"created_at": "2026-01-30T05:13:15Z",
"repoId": 1108837393,
"pullRequestNo": 1271
},
{
"name": "Hisir0909",
"id": 76634394,
"comment_id": 3822248445,
"created_at": "2026-01-30T07:20:09Z",
"repoId": 1108837393,
"pullRequestNo": 1275
},
{
"name": "Zacks-Zhang",
"id": 16462428,
"comment_id": 3822585754,
"created_at": "2026-01-30T08:51:49Z",
"repoId": 1108837393,
"pullRequestNo": 1280
},
{
"name": "kunal70006",
"id": 62700112,
"comment_id": 3822849937,
"created_at": "2026-01-30T09:55:57Z",
"repoId": 1108837393,
"pullRequestNo": 1282
},
{
"name": "KonaEspresso94",
"id": 140197941,
"comment_id": 3824340432,
"created_at": "2026-01-30T15:33:28Z",
"repoId": 1108837393,
"pullRequestNo": 1289
},
{
"name": "khduy",
"id": 48742864,
"comment_id": 3825103158,
"created_at": "2026-01-30T18:35:34Z",
"repoId": 1108837393,
"pullRequestNo": 1297
},
{
"name": "robin-watcha",
"id": 90032965,
"comment_id": 3826133640,
"created_at": "2026-01-30T22:37:32Z",
"repoId": 1108837393,
"pullRequestNo": 1303
},
{
"name": "taetaetae",
"id": 10969354,
"comment_id": 3828900888,
"created_at": "2026-01-31T17:44:09Z",
"repoId": 1108837393,
"pullRequestNo": 1333
},
{
"name": "taetaetae",
"id": 10969354,
"comment_id": 3828909557,
"created_at": "2026-01-31T17:47:21Z",
"repoId": 1108837393,
"pullRequestNo": 1333
},
{
"name": "dmealing",
"id": 1153509,
"comment_id": 3829284275,
"created_at": "2026-01-31T20:23:51Z",
"repoId": 1108837393,
"pullRequestNo": 1296
},
{
"name": "edxeth",
"id": 105494645,
"comment_id": 3829930814,
"created_at": "2026-02-01T00:58:26Z",
"repoId": 1108837393,
"pullRequestNo": 1348
}
]
}

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,53 +1,48 @@
# AGENTS KNOWLEDGE BASE
## OVERVIEW
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 # Master Orchestrator (1383 lines)
├── sisyphus.ts # Main prompt (615 lines)
├── sisyphus-junior.ts # Delegated task executor
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
├── atlas.ts # Master Orchestrator (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)
├── metis.ts # Plan consultant
├── momus.ts # Plan reviewer
├── 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 | Temp | Purpose |
|-------|-------|------|---------|
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator |
| Atlas | anthropic/claude-opus-4-5 | 0.1 | Master orchestrator |
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro) |
| Atlas | anthropic/claude-sonnet-4-5 | 0.1 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
| oracle | openai/gpt-5.2 | 0.1 | Consultation, debugging |
| librarian | opencode/glm-4.7-free | 0.1 | Docs, GitHub search |
| explore | opencode/grok-code | 0.1 | Fast contextual grep |
| librarian | zai-coding-plan/glm-4.7 | 0.1 | Docs, GitHub search (fallback: glm-4.7-free) |
| explore | anthropic/claude-haiku-4-5 | 0.1 | Fast contextual grep (fallback: gpt-5-mini → gpt-5-nano) |
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning |
| Metis | anthropic/claude-sonnet-4-5 | 0.3 | Pre-planning analysis |
| Momus | anthropic/claude-sonnet-4-5 | 0.1 | Plan validation |
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
| Metis | anthropic/claude-opus-4-5 | 0.3 | Pre-planning analysis (fallback: kimi-k2.5 → gpt-5.2) |
| Momus | openai/gpt-5.2 | 0.1 | Plan validation (fallback: claude-opus-4-5) |
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
## HOW TO ADD
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
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 |
@@ -57,14 +52,13 @@ agents/
| Sisyphus-Junior | task, delegate_task |
## 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
- **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 "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

@@ -1,7 +1,9 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
category: "exploration",
cost: "FREE",
@@ -33,8 +35,8 @@ export function createExploreAgent(model: string): AgentConfig {
return {
description:
'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.',
mode: "subagent" as const,
'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. (Explore - OhMyOpenCode)',
mode: MODE,
model,
temperature: 0.1,
...restrictions,
@@ -119,4 +121,4 @@ Use the right tool for the job:
Flood with parallel calls. Cross-validate findings across multiple tools.`,
}
}
createExploreAgent.mode = MODE

View File

@@ -1,7 +1,9 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
category: "exploration",
cost: "CHEAP",
@@ -30,8 +32,8 @@ export function createLibrarianAgent(model: string): AgentConfig {
return {
description:
"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.",
mode: "subagent" as const,
"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. (Librarian - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.1,
...restrictions,
@@ -323,4 +325,4 @@ grep_app_searchGitHub(query: "useQuery")
`,
}
}
createLibrarianAgent.mode = MODE

View File

@@ -1,7 +1,9 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
/**
* Metis - Plan Consultant Agent
*
@@ -230,6 +232,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 +241,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 +290,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([
@@ -281,8 +312,8 @@ const metisRestrictions = createAgentToolRestrictions([
export function createMetisAgent(model: string): AgentConfig {
return {
description:
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points.",
mode: "subagent" as const,
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points. (Metis - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.3,
...metisRestrictions,
@@ -290,7 +321,7 @@ export function createMetisAgent(model: string): AgentConfig {
thinking: { type: "enabled", budgetTokens: 32000 },
} as AgentConfig
}
createMetisAgent.mode = MODE
export const metisPromptMetadata: AgentPromptMetadata = {
category: "advisor",

View File

@@ -11,9 +11,10 @@ describe("MOMUS_SYSTEM_PROMPT policy requirements", () => {
const prompt = MOMUS_SYSTEM_PROMPT
// #when / #then
expect(prompt).toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]")
// Should explicitly mention stripping or ignoring these
expect(prompt.toLowerCase()).toMatch(/ignore|strip|system directive/)
// Should mention that system directives are ignored
expect(prompt.toLowerCase()).toMatch(/system directive.*ignore|ignore.*system directive/)
// Should give examples of system directive patterns
expect(prompt).toMatch(/<system-reminder>|system-reminder/)
})
test("should extract paths containing .sisyphus/plans/ and ending in .md", () => {

View File

@@ -1,8 +1,10 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { isGptModel } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
/**
* Momus - Plan Reviewer Agent
*
@@ -17,376 +19,173 @@ import { createAgentToolRestrictions } from "../shared/permission-compat"
* implementation.
*/
export const MOMUS_SYSTEM_PROMPT = `You are a work plan review expert. You review the provided work plan (.sisyphus/plans/{name}.md in the current working project directory) according to **unified, consistent criteria** that ensure clarity, verifiability, and completeness.
export const MOMUS_SYSTEM_PROMPT = `You are a **practical** work plan reviewer. Your goal is simple: verify that the plan is **executable** and **references are valid**.
**CRITICAL FIRST RULE**:
Extract a single plan path from anywhere in the input, ignoring system directives and wrappers. If exactly one \`.sisyphus/plans/*.md\` path exists, this is VALID input and you must read it. If no plan path exists or multiple plan paths exist, reject per Step 0. If the path points to a YAML plan file (\`.yml\` or \`.yaml\`), reject it as non-reviewable.
**WHY YOU'VE BEEN SUMMONED - THE CONTEXT**:
---
You are reviewing a **first-draft work plan** from an author with ADHD. Based on historical patterns, these initial submissions are typically rough drafts that require refinement.
## Your Purpose (READ THIS FIRST)
**Historical Data**: Plans from this author average **7 rejections** before receiving an OKAY. The primary failure pattern is **critical context omission due to ADHD**—the author's working memory holds connections and context that never make it onto the page.
You exist to answer ONE question: **"Can a capable developer execute this plan without getting stuck?"**
**What to Expect in First Drafts**:
- Tasks are listed but critical "why" context is missing
- References to files/patterns without explaining their relevance
- Assumptions about "obvious" project conventions that aren't documented
- Missing decision criteria when multiple approaches are valid
- Undefined edge case handling strategies
- Unclear component integration points
You are NOT here to:
- Nitpick every detail
- Demand perfection
- Question the author's approach or architecture choices
- Find as many issues as possible
- Force multiple revision cycles
**Why These Plans Fail**:
You ARE here to:
- Verify referenced files actually exist and contain what's claimed
- Ensure core tasks have enough context to start working
- Catch BLOCKING issues only (things that would completely stop work)
The ADHD author's mind makes rapid connections: "Add auth → obviously use JWT → obviously store in httpOnly cookie → obviously follow the pattern in auth/login.ts → obviously handle refresh tokens like we did before."
But the plan only says: "Add authentication following auth/login.ts pattern."
**Everything after the first arrow is missing.** The author's working memory fills in the gaps automatically, so they don't realize the plan is incomplete.
**Your Critical Role**: Catch these ADHD-driven omissions. The author genuinely doesn't realize what they've left out. Your ruthless review forces them to externalize the context that lives only in their head.
**APPROVAL BIAS**: When in doubt, APPROVE. A plan that's 80% clear is good enough. Developers can figure out minor gaps.
---
## Your Core Review Principle
## What You Check (ONLY THESE)
**ABSOLUTE CONSTRAINT - RESPECT THE IMPLEMENTATION DIRECTION**:
You are a REVIEWER, not a DESIGNER. The implementation direction in the plan is **NOT NEGOTIABLE**. Your job is to evaluate whether the plan documents that direction clearly enough to execute—NOT whether the direction itself is correct.
### 1. Reference Verification (CRITICAL)
- Do referenced files exist?
- Do referenced line numbers contain relevant code?
- If "follow pattern in X" is mentioned, does X actually demonstrate that pattern?
**What you MUST NOT do**:
- Question or reject the overall approach/architecture chosen in the plan
- Suggest alternative implementations that differ from the stated direction
- Reject because you think there's a "better way" to achieve the goal
- Override the author's technical decisions with your own preferences
**PASS even if**: Reference exists but isn't perfect. Developer can explore from there.
**FAIL only if**: Reference doesn't exist OR points to completely wrong content.
**What you MUST do**:
- Accept the implementation direction as a given constraint
- Evaluate only: "Is this direction documented clearly enough to execute?"
- Focus on gaps IN the chosen approach, not gaps in choosing the approach
### 2. Executability Check (PRACTICAL)
- Can a developer START working on each task?
- Is there at least a starting point (file, pattern, or clear description)?
**REJECT if**: When you simulate actually doing the work **within the stated approach**, you cannot obtain clear information needed for implementation, AND the plan does not specify reference materials to consult.
**PASS even if**: Some details need to be figured out during implementation.
**FAIL only if**: Task is so vague that developer has NO idea where to begin.
**ACCEPT if**: You can obtain the necessary information either:
1. Directly from the plan itself, OR
2. By following references provided in the plan (files, docs, patterns) and tracing through related materials
### 3. Critical Blockers Only
- Missing information that would COMPLETELY STOP work
- Contradictions that make the plan impossible to follow
**The Test**: "Given the approach the author chose, can I implement this by starting from what's written in the plan and following the trail of information it provides?"
**WRONG mindset**: "This approach is suboptimal. They should use X instead." → **YOU ARE OVERSTEPPING**
**RIGHT mindset**: "Given their choice to use Y, the plan doesn't explain how to handle Z within that approach." → **VALID CRITICISM**
**NOT blockers** (do not reject for these):
- Missing edge case handling
- Incomplete acceptance criteria
- Stylistic preferences
- "Could be clearer" suggestions
- Minor ambiguities a developer can resolve
---
## Common Failure Patterns (What the Author Typically Forgets)
## What You Do NOT Check
The plan author is intelligent but has ADHD. They constantly skip providing:
- Whether the approach is optimal
- Whether there's a "better way"
- Whether all edge cases are documented
- Whether acceptance criteria are perfect
- Whether the architecture is ideal
- Code quality concerns
- Performance considerations
- Security unless explicitly broken
**1. Reference Materials**
- FAIL: Says "implement authentication" but doesn't point to any existing code, docs, or patterns
- FAIL: Says "follow the pattern" but doesn't specify which file contains the pattern
- FAIL: Says "similar to X" but X doesn't exist or isn't documented
**2. Business Requirements**
- FAIL: Says "add feature X" but doesn't explain what it should do or why
- FAIL: Says "handle errors" but doesn't specify which errors or how users should experience them
- FAIL: Says "optimize" but doesn't define success criteria
**3. Architectural Decisions**
- FAIL: Says "add to state" but doesn't specify which state management system
- FAIL: Says "integrate with Y" but doesn't explain the integration approach
- FAIL: Says "call the API" but doesn't specify which endpoint or data flow
**4. Critical Context**
- FAIL: References files that don't exist
- FAIL: Points to line numbers that don't contain relevant code
- FAIL: Assumes you know project-specific conventions that aren't documented anywhere
**What You Should NOT Reject**:
- PASS: Plan says "follow auth/login.ts pattern" → you read that file → it has imports → you follow those → you understand the full flow
- PASS: Plan says "use Redux store" → you find store files by exploring codebase structure → standard Redux patterns apply
- PASS: Plan provides clear starting point → you trace through related files and types → you gather all needed details
- PASS: The author chose approach X when you think Y would be better → **NOT YOUR CALL**. Evaluate X on its own merits.
- PASS: The architecture seems unusual or non-standard → If the author chose it, your job is to ensure it's documented, not to redesign it.
**The Difference**:
- FAIL/REJECT: "Add authentication" (no starting point provided)
- PASS/ACCEPT: "Add authentication following pattern in auth/login.ts" (starting point provided, you can trace from there)
- **WRONG/REJECT**: "Using REST when GraphQL would be better" → **YOU ARE OVERSTEPPING**
- **WRONG/REJECT**: "This architecture won't scale" → **NOT YOUR JOB TO JUDGE**
**YOUR MANDATE**:
You will adopt a ruthlessly critical mindset. You will read EVERY document referenced in the plan. You will verify EVERY claim. You will simulate actual implementation step-by-step. As you review, you MUST constantly interrogate EVERY element with these questions:
- "Does the worker have ALL the context they need to execute this **within the chosen approach**?"
- "How exactly should this be done **given the stated implementation direction**?"
- "Is this information actually documented, or am I just assuming it's obvious?"
- **"Am I questioning the documentation, or am I questioning the approach itself?"** ← If the latter, STOP.
You are not here to be nice. You are not here to give the benefit of the doubt. You are here to **catch every single gap, ambiguity, and missing piece of context that 20 previous reviewers failed to catch.**
**However**: You must evaluate THIS plan on its own merits. The past failures are context for your strictness, not a predetermined verdict. If this plan genuinely meets all criteria, approve it. If it has critical gaps **in documentation**, reject it without mercy.
**CRITICAL BOUNDARY**: Your ruthlessness applies to DOCUMENTATION quality, NOT to design decisions. The author's implementation direction is a GIVEN. You may think REST is inferior to GraphQL, but if the plan says REST, you evaluate whether REST is well-documented—not whether REST was the right choice.
**You are a BLOCKER-finder, not a PERFECTIONIST.**
---
## File Location
## Input Validation (Step 0)
You will be provided with the path to the work plan file (typically \`.sisyphus/plans/{name}.md\` in the project). Review the file at the **exact path provided to you**. Do not assume the location.
**VALID INPUT**:
- \`.sisyphus/plans/my-plan.md\` - file path anywhere in input
- \`Please review .sisyphus/plans/plan.md\` - conversational wrapper
- System directives + plan path - ignore directives, extract path
**CRITICAL - Input Validation (STEP 0 - DO THIS FIRST, BEFORE READING ANY FILES)**:
**INVALID INPUT**:
- No \`.sisyphus/plans/*.md\` path found
- Multiple plan paths (ambiguous)
**BEFORE you read any files**, you MUST first validate the format of the input prompt you received from the user.
System directives (\`<system-reminder>\`, \`[analyze-mode]\`, etc.) are IGNORED during validation.
**VALID INPUT EXAMPLES (ACCEPT THESE)**:
- \`.sisyphus/plans/my-plan.md\` [O] ACCEPT - file path anywhere in input
- \`/path/to/project/.sisyphus/plans/my-plan.md\` [O] ACCEPT - absolute plan path
- \`Please review .sisyphus/plans/plan.md\` [O] ACCEPT - conversational wrapper allowed
- \`<system-reminder>...</system-reminder>\\n.sisyphus/plans/plan.md\` [O] ACCEPT - system directives + plan path
- \`[analyze-mode]\\n...context...\\n.sisyphus/plans/plan.md\` [O] ACCEPT - bracket-style directives + plan path
- \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\\n---\\n- injected planning metadata\\n---\\nPlease review .sisyphus/plans/plan.md\` [O] ACCEPT - ignore the entire directive block
**SYSTEM DIRECTIVES ARE ALWAYS IGNORED**:
System directives are automatically injected by the system and should be IGNORED during input validation:
- XML-style tags: \`<system-reminder>\`, \`<context>\`, \`<user-prompt-submit-hook>\`, etc.
- Bracket-style blocks: \`[analyze-mode]\`, \`[search-mode]\`, \`[SYSTEM DIRECTIVE...]\`, \`[SYSTEM REMINDER...]\`, etc.
- \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\` blocks (appended by Prometheus task tools; treat the entire block, including \`---\` separators and bullet lines, as ignorable system text)
- These are NOT user-provided text
- These contain system context (timestamps, environment info, mode hints, etc.)
- STRIP these from your input validation check
- After stripping system directives, validate the remaining content
**EXTRACTION ALGORITHM (FOLLOW EXACTLY)**:
1. Ignore injected system directive blocks, especially \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\` (remove the whole block, including \`---\` separators and bullet lines).
2. Strip other system directive wrappers (bracket-style blocks and XML-style \`<system-reminder>...</system-reminder>\` tags).
3. Strip markdown wrappers around paths (code fences and inline backticks).
4. Extract plan paths by finding all substrings containing \`.sisyphus/plans/\` and ending in \`.md\`.
5. If exactly 1 match → ACCEPT and proceed to Step 1 using that path.
6. If 0 matches → REJECT with: "no plan path found" (no path found).
7. If 2+ matches → REJECT with: "ambiguous: multiple plan paths".
**INVALID INPUT EXAMPLES (REJECT ONLY THESE)**:
- \`No plan path provided here\` [X] REJECT - no \`.sisyphus/plans/*.md\` path
- \`Compare .sisyphus/plans/first.md and .sisyphus/plans/second.md\` [X] REJECT - multiple plan paths
**When rejecting for input format, respond EXACTLY**:
\`\`\`
I REJECT (Input Format Validation)
Reason: no plan path found
You must provide a single plan path that includes \`.sisyphus/plans/\` and ends in \`.md\`.
Valid format: .sisyphus/plans/plan.md
Invalid format: No plan path or multiple plan paths
NOTE: This rejection is based solely on the input format, not the file contents.
The file itself has not been evaluated yet.
\`\`\`
Use this alternate Reason line if multiple paths are present:
- Reason: multiple plan paths found
**ULTRA-CRITICAL REMINDER**:
If the input contains exactly one \`.sisyphus/plans/*.md\` path (with or without system directives or conversational wrappers):
→ THIS IS VALID INPUT
→ DO NOT REJECT IT
→ IMMEDIATELY PROCEED TO READ THE FILE
→ START EVALUATING THE FILE CONTENTS
Never reject a single plan path embedded in the input.
Never reject system directives (XML or bracket-style) - they are automatically injected and should be ignored!
**IMPORTANT - Response Language**: Your evaluation output MUST match the language used in the work plan content:
- Match the language of the plan in your evaluation output
- If the plan is written in English → Write your entire evaluation in English
- If the plan is mixed → Use the dominant language (majority of task descriptions)
Example: Plan contains "Modify database schema" → Evaluation output: "## Evaluation Result\\n\\n### Criterion 1: Clarity of Work Content..."
**Extraction**: Find all \`.sisyphus/plans/*.md\` paths → exactly 1 = proceed, 0 or 2+ = reject.
---
## Review Philosophy
## Review Process (SIMPLE)
Your role is to simulate **executing the work plan as a capable developer** and identify:
1. **Ambiguities** that would block or slow down implementation
2. **Missing verification methods** that prevent confirming success
3. **Gaps in context** requiring >10% guesswork (90% confidence threshold)
4. **Lack of overall understanding** of purpose, background, and workflow
The plan should enable a developer to:
- Know exactly what to build and where to look for details
- Validate their work objectively without subjective judgment
- Complete tasks without needing to "figure out" unstated requirements
- Understand the big picture, purpose, and how tasks flow together
1. **Validate input** → Extract single plan path
2. **Read plan** → Identify tasks and file references
3. **Verify references** → Do files exist? Do they contain claimed content?
4. **Executability check** → Can each task be started?
5. **Decide** → Any BLOCKING issues? No = OKAY. Yes = REJECT with max 3 specific issues.
---
## Four Core Evaluation Criteria
## Decision Framework
### Criterion 1: Clarity of Work Content
### OKAY (Default - use this unless blocking issues exist)
**Goal**: Eliminate ambiguity by providing clear reference sources for each task.
Issue the verdict **OKAY** when:
- Referenced files exist and are reasonably relevant
- Tasks have enough context to start (not complete, just start)
- No contradictions or impossible requirements
- A capable developer could make progress
**Evaluation Method**: For each task, verify:
- **Does the task specify WHERE to find implementation details?**
- [PASS] Good: "Follow authentication flow in \`docs/auth-spec.md\` section 3.2"
- [PASS] Good: "Implement based on existing pattern in \`src/services/payment.ts:45-67\`"
- [FAIL] Bad: "Add authentication" (no reference source)
- [FAIL] Bad: "Improve error handling" (vague, no examples)
**Remember**: "Good enough" is good enough. You're not blocking publication of a NASA manual.
- **Can the developer reach 90%+ confidence by reading the referenced source?**
- [PASS] Good: Reference to specific file/section that contains concrete examples
- [FAIL] Bad: "See codebase for patterns" (too broad, requires extensive exploration)
### REJECT (Only for true blockers)
### Criterion 2: Verification & Acceptance Criteria
Issue **REJECT** ONLY when:
- Referenced file doesn't exist (verified by reading)
- Task is completely impossible to start (zero context)
- Plan contains internal contradictions
**Goal**: Ensure every task has clear, objective success criteria.
**Maximum 3 issues per rejection.** If you found more, list only the top 3 most critical.
**Evaluation Method**: For each task, verify:
- **Is there a concrete way to verify completion?**
- [PASS] Good: "Verify: Run \`npm test\` → all tests pass. Manually test: Open \`/login\` → OAuth button appears → Click → redirects to Google → successful login"
- [PASS] Good: "Acceptance: API response time < 200ms for 95th percentile (measured via \`k6 run load-test.js\`)"
- [FAIL] Bad: "Test the feature" (how?)
- [FAIL] Bad: "Make sure it works properly" (what defines "properly"?)
- **Are acceptance criteria measurable/observable?**
- [PASS] Good: Observable outcomes (UI elements, API responses, test results, metrics)
- [FAIL] Bad: Subjective terms ("clean code", "good UX", "robust implementation")
### Criterion 3: Context Completeness
**Goal**: Minimize guesswork by providing all necessary context (90% confidence threshold).
**Evaluation Method**: Simulate task execution and identify:
- **What information is missing that would cause ≥10% uncertainty?**
- [PASS] Good: Developer can proceed with <10% guesswork (or natural exploration)
- [FAIL] Bad: Developer must make assumptions about business requirements, architecture, or critical context
- **Are implicit assumptions stated explicitly?**
- [PASS] Good: "Assume user is already authenticated (session exists in context)"
- [PASS] Good: "Note: Payment processing is handled by background job, not synchronously"
- [FAIL] Bad: Leaving critical architectural decisions or business logic unstated
### Criterion 4: Big Picture & Workflow Understanding
**Goal**: Ensure the developer understands WHY they're building this, WHAT the overall objective is, and HOW tasks flow together.
**Evaluation Method**: Assess whether the plan provides:
- **Clear Purpose Statement**: Why is this work being done? What problem does it solve?
- **Background Context**: What's the current state? What are we changing from?
- **Task Flow & Dependencies**: How do tasks connect? What's the logical sequence?
- **Success Vision**: What does "done" look like from a product/user perspective?
**Each issue must be**:
- Specific (exact file path, exact task)
- Actionable (what exactly needs to change)
- Blocking (work cannot proceed without this)
---
## Review Process
## Anti-Patterns (DO NOT DO THESE)
### Step 0: Validate Input Format (MANDATORY FIRST STEP)
Extract the plan path from anywhere in the input. If exactly one \`.sisyphus/plans/*.md\` path is found, ACCEPT and continue. If none are found, REJECT with "no plan path found". If multiple are found, REJECT with "ambiguous: multiple plan paths".
❌ "Task 3 could be clearer about error handling" → NOT a blocker
❌ "Consider adding acceptance criteria for..." → NOT a blocker
❌ "The approach in Task 5 might be suboptimal" → NOT YOUR JOB
❌ "Missing documentation for edge case X" → NOT a blocker unless X is the main case
❌ Rejecting because you'd do it differently → NEVER
❌ Listing more than 3 issues → OVERWHELMING, pick top 3
### Step 1: Read the Work Plan
- Load the file from the path provided
- Identify the plan's language
- Parse all tasks and their descriptions
- Extract ALL file references
### Step 2: MANDATORY DEEP VERIFICATION
For EVERY file reference, library mention, or external resource:
- Read referenced files to verify content
- Search for related patterns/imports across codebase
- Verify line numbers contain relevant code
- Check that patterns are clear enough to follow
### Step 3: Apply Four Criteria Checks
For **the overall plan and each task**, evaluate:
1. **Clarity Check**: Does the task specify clear reference sources?
2. **Verification Check**: Are acceptance criteria concrete and measurable?
3. **Context Check**: Is there sufficient context to proceed without >10% guesswork?
4. **Big Picture Check**: Do I understand WHY, WHAT, and HOW?
### Step 4: Active Implementation Simulation
For 2-3 representative tasks, simulate execution using actual files.
### Step 5: Check for Red Flags
Scan for auto-fail indicators:
- Vague action verbs without concrete targets
- Missing file paths for code changes
- Subjective success criteria
- Tasks requiring unstated assumptions
**SELF-CHECK - Are you overstepping?**
Before writing any criticism, ask yourself:
- "Am I questioning the APPROACH or the DOCUMENTATION of the approach?"
- "Would my feedback change if I accepted the author's direction as a given?"
If you find yourself writing "should use X instead" or "this approach won't work because..." → **STOP. You are overstepping your role.**
Rephrase to: "Given the chosen approach, the plan doesn't clarify..."
### Step 6: Write Evaluation Report
Use structured format, **in the same language as the work plan**.
✅ "Task 3 references \`auth/login.ts\` but file doesn't exist" → BLOCKER
✅ "Task 5 says 'implement feature' with no context, files, or description" → BLOCKER
✅ "Tasks 2 and 4 contradict each other on data flow" → BLOCKER
---
## Approval Criteria
## Output Format
### OKAY Requirements (ALL must be met)
1. **100% of file references verified**
2. **Zero critically failed file verifications**
3. **Critical context documented**
4. **≥80% of tasks** have clear reference sources
5. **≥90% of tasks** have concrete acceptance criteria
6. **Zero tasks** require assumptions about business logic or critical architecture
7. **Plan provides clear big picture**
8. **Zero critical red flags** detected
9. **Active simulation** shows core tasks are executable
**[OKAY]** or **[REJECT]**
### REJECT Triggers (Critical issues only)
- Referenced file doesn't exist or contains different content than claimed
- Task has vague action verbs AND no reference source
- Core tasks missing acceptance criteria entirely
- Task requires assumptions about business requirements or critical architecture **within the chosen approach**
- Missing purpose statement or unclear WHY
- Critical task dependencies undefined
**Summary**: 1-2 sentences explaining the verdict.
### NOT Valid REJECT Reasons (DO NOT REJECT FOR THESE)
- You disagree with the implementation approach
- You think a different architecture would be better
- The approach seems non-standard or unusual
- You believe there's a more optimal solution
- The technology choice isn't what you would pick
**Your role is DOCUMENTATION REVIEW, not DESIGN REVIEW.**
If REJECT:
**Blocking Issues** (max 3):
1. [Specific issue + what needs to change]
2. [Specific issue + what needs to change]
3. [Specific issue + what needs to change]
---
## Final Verdict Format
## Final Reminders
**[OKAY / REJECT]**
1. **APPROVE by default**. Reject only for true blockers.
2. **Max 3 issues**. More than that is overwhelming and counterproductive.
3. **Be specific**. "Task X needs Y" not "needs more clarity".
4. **No design opinions**. The author's approach is not your concern.
5. **Trust developers**. They can figure out minor gaps.
**Justification**: [Concise explanation]
**Your job is to UNBLOCK work, not to BLOCK it with perfectionism.**
**Summary**:
- Clarity: [Brief assessment]
- Verifiability: [Brief assessment]
- Completeness: [Brief assessment]
- Big Picture: [Brief assessment]
[If REJECT, provide top 3-5 critical improvements needed]
---
**Your Success Means**:
- **Immediately actionable** for core business logic and architecture
- **Clearly verifiable** with objective success criteria
- **Contextually complete** with critical information documented
- **Strategically coherent** with purpose, background, and flow
- **Reference integrity** with all files verified
- **Direction-respecting** - you evaluated the plan WITHIN its stated approach
**Strike the right balance**: Prevent critical failures while empowering developer autonomy.
**FINAL REMINDER**: You are a DOCUMENTATION reviewer, not a DESIGN consultant. The author's implementation direction is SACRED. Your job ends at "Is this well-documented enough to execute?" - NOT "Is this the right approach?"
**Response Language**: Match the language of the plan content.
`
export function createMomusAgent(model: string): AgentConfig {
@@ -399,8 +198,8 @@ export function createMomusAgent(model: string): AgentConfig {
const base = {
description:
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards.",
mode: "subagent" as const,
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards. (Momus - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.1,
...restrictions,
@@ -413,7 +212,7 @@ export function createMomusAgent(model: string): AgentConfig {
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
}
createMomusAgent.mode = MODE
export const momusPromptMetadata: AgentPromptMetadata = {
category: "advisor",

View File

@@ -1,7 +1,9 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { createAgentToolAllowlist } from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
category: "utility",
cost: "CHEAP",
@@ -14,8 +16,8 @@ export function createMultimodalLookerAgent(model: string): AgentConfig {
return {
description:
"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.",
mode: "subagent" as const,
"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. (Multimodal-Looker - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.1,
...restrictions,
@@ -53,4 +55,4 @@ Response rules:
Your output goes straight to the main agent for continued work.`,
}
}
createMultimodalLookerAgent.mode = MODE

View File

@@ -1,8 +1,10 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentPromptMetadata } from "./types"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { isGptModel } from "./types"
import { createAgentToolRestrictions } from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
export const ORACLE_PROMPT_METADATA: AgentPromptMetadata = {
category: "advisor",
cost: "EXPENSIVE",
@@ -105,8 +107,8 @@ export function createOracleAgent(model: string): AgentConfig {
const base = {
description:
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design.",
mode: "subagent" as const,
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design. (Oracle - OhMyOpenCode)",
mode: MODE,
model,
temperature: 0.1,
...restrictions,
@@ -119,4 +121,5 @@ export function createOracleAgent(model: string): AgentConfig {
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
}
createOracleAgent.mode = MODE

View File

@@ -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,7 +631,7 @@ 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}
@@ -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
)
\`\`\`
@@ -797,9 +797,9 @@ 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") {
@@ -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,7 +1046,21 @@ 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):
@@ -1029,53 +1093,76 @@ Task 1 → Task 2 → Task 3
**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)
**Manual Execution Verification (ALWAYS include, even with tests):**
**Automated Verification (ALWAYS include, choose by deliverable type):**
*Choose based on 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 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):
\\\`\\\`\\\`
# 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 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** (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 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** (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"
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('invalid'))"
# Assert: Output is "false"
\\\`\\\`\\\`
**For Library/Module changes:**
- [ ] REPL verification:
\`\`\`
> import { [function] } from '[module]'
> [function]([args])
Expected: [output]
\`\`\`
**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"
\\\`\\\`\\\`
**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)
**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\`

View File

@@ -1,4 +1,5 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode } from "./types"
import { isGptModel } from "./types"
import type { AgentOverrideConfig } from "../config/schema"
import {
@@ -6,6 +7,8 @@ import {
type PermissionValue,
} from "../shared/permission-compat"
const MODE: AgentMode = "subagent"
const SISYPHUS_JUNIOR_PROMPT = `<Role>
Sisyphus-Junior - Focused executor from OhMyOpenCode.
Execute tasks directly. NEVER delegate or spawn other agents.
@@ -20,32 +23,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.
IMPORTANT: Always APPEND to notepad files - never overwrite or use Edit tool.
## 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
@@ -110,8 +87,8 @@ export function createSisyphusJuniorAgentWithOverrides(
const base: AgentConfig = {
description: override?.description ??
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
mode: "subagent" as const,
"Focused task executor. Same discipline, no delegation. (Sisyphus-Junior - OhMyOpenCode)",
mode: MODE,
model,
temperature,
maxTokens: 64000,
@@ -133,3 +110,5 @@ export function createSisyphusJuniorAgentWithOverrides(
thinking: { type: "enabled", budgetTokens: 32000 },
} as AgentConfig
}
createSisyphusJuniorAgentWithOverrides.mode = MODE

View File

@@ -1,5 +1,14 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode, AgentPromptMetadata } from "./types"
import { isGptModel } from "./types"
const MODE: AgentMode = "primary"
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
category: "utility",
cost: "EXPENSIVE",
promptAlias: "Sisyphus",
triggers: [],
}
import type { AvailableAgent, AvailableTool, AvailableSkill, AvailableCategory } from "./dynamic-agent-prompt-builder"
import {
buildKeyTriggersSection,
@@ -144,11 +153,11 @@ ${librarianSection}
\`\`\`typescript
// CORRECT: Always background, always parallel
// Contextual Grep (internal)
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find auth implementations in our codebase...")
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find error handling patterns here...")
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
// Reference Grep (external)
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find JWT best practices in official docs...")
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find how production apps handle auth in Express...")
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
// Continue working immediately. Collect with background_output when needed.
// WRONG: Sequential or blocking
@@ -205,6 +214,34 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
**Vague prompts = rejected. Be exhaustive.**
### Session Continuity (MANDATORY)
Every \`delegate_task()\` output includes a session_id. **USE IT.**
**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."\` |
**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
\`\`\`typescript
// WRONG: Starting fresh loses all context
delegate_task(category="quick", prompt="Fix the type error in auth.ts...")
// CORRECT: Resume preserves everything
delegate_task(session_id="ses_abc123", prompt="Fix: Type error on line 42")
\`\`\`
**After EVERY delegation, STORE the session_id for potential continuation.**
### Code Changes:
- Match existing patterns (if codebase is disciplined)
- Propose approach first (if codebase is chaotic)
@@ -405,8 +442,8 @@ export function createSisyphusAgent(
const permission = { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"]
const base = {
description:
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs.",
mode: "primary" as const,
"Powerful AI orchestrator. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs. (Sisyphus - OhMyOpenCode)",
mode: MODE,
model,
maxTokens: 64000,
prompt,
@@ -420,3 +457,4 @@ export function createSisyphusAgent(
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
}
createSisyphusAgent.mode = MODE

View File

@@ -1,6 +1,20 @@
import type { AgentConfig } from "@opencode-ai/sdk"
export type AgentFactory = (model: string) => AgentConfig
/**
* Agent mode determines UI model selection behavior:
* - "primary": Respects user's UI-selected model (sisyphus, atlas)
* - "subagent": Uses own fallback chain, ignores UI selection (oracle, explore, etc.)
* - "all": Available in both contexts (OpenCode compatibility)
*/
export type AgentMode = "primary" | "subagent" | "all"
/**
* Agent factory function with static mode property.
* Mode is exposed as static property for pre-instantiation access.
*/
export type AgentFactory = ((model: string) => AgentConfig) & {
mode: AgentMode
}
/**
* Agent category for grouping in Sisyphus prompt sections
@@ -57,14 +71,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,6 +1,9 @@
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"
import * as modelAvailability from "../shared/model-availability"
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-5"
@@ -12,51 +15,65 @@ describe("createBuiltinAgents with model overrides", () => {
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", async () => {
// #given
const overrides = {
Sisyphus: { model: "github-copilot/gpt-5.2" },
sisyphus: { model: "github-copilot/gpt-5.2" },
}
// #when
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 uses first fallbackChain entry when no availableModels provided", async () => {
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 = await createBuiltinAgents([], {}, undefined, systemDefaultModel)
// #then - Sisyphus first fallbackChain entry is anthropic/claude-opus-4-5
expect(agents.Sisyphus.model).toBe("anthropic/claude-opus-4-5")
expect(agents.Sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
expect(agents.Sisyphus.reasoningEffort).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 uses first fallbackChain entry when no availableModels provided", async () => {
// #given - Oracle's first fallbackChain entry is openai/gpt-5.2
test("Oracle uses connected provider fallback when availableModels is empty and cache exists", async () => {
// #given - connected providers cache has "openai", which matches oracle's first fallback entry
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
// #when
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #when
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
// #then - Oracle first fallbackChain entry is openai/gpt-5.2
expect(agents.oracle.model).toBe("openai/gpt-5.2")
expect(agents.oracle.reasoningEffort).toBe("medium")
expect(agents.oracle.textVerbosity).toBe("high")
expect(agents.oracle.thinking).toBeUndefined()
})
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
expect(agents.oracle.model).toBe("openai/gpt-5.2")
expect(agents.oracle.reasoningEffort).toBe("medium")
expect(agents.oracle.thinking).toBeUndefined()
cacheSpy.mockRestore?.()
})
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
@@ -90,25 +107,69 @@ describe("createBuiltinAgents with model overrides", () => {
expect(agents.oracle.textVerbosity).toBeUndefined()
})
test("non-model overrides are still applied after factory rebuild", async () => {
// #given
const overrides = {
Sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
}
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)
// #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)
})
// #then
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
expect(agents.sisyphus.temperature).toBe(0.5)
})
})
describe("createBuiltinAgents without systemDefaultModel", () => {
test("agents created via connected cache fallback even without systemDefaultModel", async () => {
// #given - connected cache has "openai", which matches oracle's fallback chain
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
// #when
const agents = await createBuiltinAgents([], {}, undefined, undefined)
// #then - connected cache enables model resolution despite no systemDefaultModel
expect(agents.oracle).toBeDefined()
expect(agents.oracle.model).toBe("openai/gpt-5.2")
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 created via connected cache fallback even without systemDefaultModel", async () => {
// #given - connected cache has "anthropic", which matches sisyphus's first fallback entry
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
// #when
const agents = await createBuiltinAgents([], {}, undefined, undefined)
// #then - connected cache enables model resolution despite no systemDefaultModel
expect(agents.sisyphus).toBeDefined()
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
cacheSpy.mockRestore?.()
})
})
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 +184,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 +369,196 @@ 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")
})
})
describe("override.category expansion in createBuiltinAgents", () => {
test("standard agent override with category expands category properties", async () => {
// #given
const overrides = {
oracle: { category: "ultrabrain" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
expect(agents.oracle).toBeDefined()
expect(agents.oracle.model).toBe("openai/gpt-5.2-codex")
expect(agents.oracle.variant).toBe("xhigh")
})
test("standard agent override with category AND direct variant - direct wins", async () => {
// #given - ultrabrain has variant=xhigh, but direct override says "max"
const overrides = {
oracle: { category: "ultrabrain", variant: "max" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then - direct variant overrides category variant
expect(agents.oracle).toBeDefined()
expect(agents.oracle.variant).toBe("max")
})
test("standard agent override with category AND direct reasoningEffort - direct wins", async () => {
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
const categories = {
"test-cat": {
model: "openai/gpt-5.2",
reasoningEffort: "xhigh" as const,
},
}
const overrides = {
oracle: { category: "test-cat", reasoningEffort: "low" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, categories)
// #then - direct reasoningEffort wins over category
expect(agents.oracle).toBeDefined()
expect(agents.oracle.reasoningEffort).toBe("low")
})
test("standard agent override with category applies reasoningEffort from category when no direct override", async () => {
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
const categories = {
"reasoning-cat": {
model: "openai/gpt-5.2",
reasoningEffort: "high" as const,
},
}
const overrides = {
oracle: { category: "reasoning-cat" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, categories)
// #then - category reasoningEffort is applied
expect(agents.oracle).toBeDefined()
expect(agents.oracle.reasoningEffort).toBe("high")
})
test("sisyphus override with category expands category properties", async () => {
// #given
const overrides = {
sisyphus: { category: "ultrabrain" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
expect(agents.sisyphus).toBeDefined()
expect(agents.sisyphus.model).toBe("openai/gpt-5.2-codex")
expect(agents.sisyphus.variant).toBe("xhigh")
})
test("atlas override with category expands category properties", async () => {
// #given
const overrides = {
atlas: { category: "ultrabrain" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
expect(agents.atlas).toBeDefined()
expect(agents.atlas.model).toBe("openai/gpt-5.2-codex")
expect(agents.atlas.variant).toBe("xhigh")
})
test("override with non-existent category has no effect on config", async () => {
// #given
const overrides = {
oracle: { category: "non-existent-category" } as any,
}
// #when
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
// #then - no category-specific variant/reasoningEffort applied from non-existent category
expect(agents.oracle).toBeDefined()
const agentsWithoutOverride = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
expect(agents.oracle.model).toBe(agentsWithoutOverride.oracle.model)
})
})
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
test("createBuiltinAgents should call fetchAvailableModels with undefined client to prevent deadlock", async () => {
// #given - This test ensures we don't regress on issue #1301
// Passing client to fetchAvailableModels during createBuiltinAgents (called from config handler)
// causes deadlock:
// - Plugin init waits for server response (client.provider.list())
// - Server waits for plugin init to complete before handling requests
const fetchSpy = spyOn(modelAvailability, "fetchAvailableModels").mockResolvedValue(new Set<string>())
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
const mockClient = {
provider: { list: () => Promise.resolve({ data: { connected: [] } }) },
model: { list: () => Promise.resolve({ data: [] }) },
}
// #when - Even when client is provided, fetchAvailableModels must be called with undefined
await createBuiltinAgents(
[],
{},
undefined,
TEST_DEFAULT_MODEL,
undefined,
undefined,
[],
mockClient // client is passed but should NOT be forwarded to fetchAvailableModels
)
// #then - fetchAvailableModels must be called with undefined as first argument (no client)
// This prevents the deadlock described in issue #1301
expect(fetchSpy).toHaveBeenCalled()
const firstCallArgs = fetchSpy.mock.calls[0]
expect(firstCallArgs[0]).toBeUndefined()
fetchSpy.mockRestore?.()
cacheSpy.mockRestore?.()
})
})

View File

@@ -6,29 +6,30 @@ import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
import { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
import { createMetisAgent } from "./metis"
import { createAtlasAgent } from "./atlas"
import { createMomusAgent } from "./momus"
import { createMetisAgent, metisPromptMetadata } from "./metis"
import { createAtlasAgent, atlasPromptMetadata } from "./atlas"
import { createMomusAgent, momusPromptMetadata } from "./momus"
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive } from "../shared"
import { deepMerge, fetchAvailableModels, resolveModelPipeline, AGENT_MODEL_REQUIREMENTS, readConnectedProvidersCache, isModelAvailable } 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,
}
/**
@@ -40,6 +41,9 @@ const agentMetadata: Partial<Record<BuiltinAgentName, AgentPromptMetadata>> = {
librarian: LIBRARIAN_PROMPT_METADATA,
explore: EXPLORE_PROMPT_METADATA,
"multimodal-looker": MULTIMODAL_LOOKER_PROMPT_METADATA,
metis: metisPromptMetadata,
momus: momusPromptMetadata,
atlas: atlasPromptMetadata,
}
function isFactory(source: AgentSource): source is AgentFactory {
@@ -50,7 +54,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
@@ -74,7 +79,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 : "")
@@ -118,6 +123,72 @@ export function createEnvContext(): string {
</omo-env>`
}
/**
* Expands a category reference from an agent override into concrete config properties.
* Category properties are applied unconditionally (overwriting factory defaults),
* because the user's chosen category should take priority over factory base values.
* Direct override properties applied later via mergeAgentConfig() will supersede these.
*/
function applyCategoryOverride(
config: AgentConfig,
categoryName: string,
mergedCategories: Record<string, CategoryConfig>
): AgentConfig {
const categoryConfig = mergedCategories[categoryName]
if (!categoryConfig) return config
const result = { ...config } as AgentConfig & Record<string, unknown>
if (categoryConfig.model) result.model = categoryConfig.model
if (categoryConfig.variant !== undefined) result.variant = categoryConfig.variant
if (categoryConfig.temperature !== undefined) result.temperature = categoryConfig.temperature
if (categoryConfig.reasoningEffort !== undefined) result.reasoningEffort = categoryConfig.reasoningEffort
if (categoryConfig.textVerbosity !== undefined) result.textVerbosity = categoryConfig.textVerbosity
if (categoryConfig.thinking !== undefined) result.thinking = categoryConfig.thinking
if (categoryConfig.top_p !== undefined) result.top_p = categoryConfig.top_p
if (categoryConfig.maxTokens !== undefined) result.maxTokens = categoryConfig.maxTokens
return result as AgentConfig
}
function applyModelResolution(input: {
uiSelectedModel?: string
userModel?: string
requirement?: { fallbackChain?: { providers: string[]; model: string; variant?: string }[] }
availableModels: Set<string>
systemDefaultModel?: string
}) {
const { uiSelectedModel, userModel, requirement, availableModels, systemDefaultModel } = input
return resolveModelPipeline({
intent: { uiSelectedModel, userModel },
constraints: { availableModels },
policy: { fallbackChain: requirement?.fallbackChain, systemDefaultModel },
})
}
function applyEnvironmentContext(config: AgentConfig, directory?: string): AgentConfig {
if (!directory || !config.prompt) return config
const envContext = createEnvContext()
return { ...config, prompt: config.prompt + envContext }
}
function applyOverrides(
config: AgentConfig,
override: AgentOverrideConfig | undefined,
mergedCategories: Record<string, CategoryConfig>
): AgentConfig {
let result = config
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
if (overrideCategory) {
result = applyCategoryOverride(result, overrideCategory, mergedCategories)
}
if (override) {
result = mergeAgentConfig(result, override)
}
return result
}
function mergeAgentConfig(
base: AgentConfig,
override: AgentOverrideConfig
@@ -139,21 +210,24 @@ function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
}
export async function createBuiltinAgents(
disabledAgents: BuiltinAgentName[] = [],
disabledAgents: string[] = [],
agentOverrides: AgentOverrides = {},
directory?: string,
systemDefaultModel?: string,
categories?: CategoriesConfig,
gitMasterConfig?: GitMasterConfig,
discoveredSkills: LoadedSkill[] = [],
client?: any
client?: any,
browserProvider?: BrowserAutomationProvider,
uiSelectedModel?: string
): Promise<Record<string, AgentConfig>> {
if (!systemDefaultModel) {
throw new Error("createBuiltinAgents requires systemDefaultModel")
}
// Fetch available models at plugin init
const availableModels = client ? await fetchAvailableModels(client) : new Set<string>()
const connectedProviders = readConnectedProvidersCache()
// IMPORTANT: Do NOT pass client to fetchAvailableModels during plugin initialization.
// This function is called from config handler, and calling client API causes deadlock.
// See: https://github.com/code-yeongyu/oh-my-opencode/issues/1301
const availableModels = await fetchAvailableModels(undefined, {
connectedProviders: connectedProviders ?? undefined,
})
const result: Record<string, AgentConfig> = {}
const availableAgents: AvailableAgent[] = []
@@ -167,7 +241,7 @@ export async function createBuiltinAgents(
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
}))
const builtinSkills = createBuiltinSkills()
const builtinSkills = createBuiltinSkills({ browserProvider })
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
@@ -186,42 +260,55 @@ export async function createBuiltinAgents(
const availableSkills: AvailableSkill[] = [...builtinAvailable, ...discoveredAvailable]
for (const [name, source] of Object.entries(agentSources)) {
const agentName = name as BuiltinAgentName
for (const [name, source] of Object.entries(agentSources)) {
const agentName = name as BuiltinAgentName
if (agentName === "Sisyphus") continue
if (agentName === "Atlas") continue
if (includesCaseInsensitive(disabledAgents, agentName)) continue
if (agentName === "sisyphus") continue
if (agentName === "atlas") continue
if (disabledAgents.some((name) => name.toLowerCase() === agentName.toLowerCase())) continue
const override = findCaseInsensitive(agentOverrides, agentName)
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
// Use resolver to determine model
const { model } = resolveModelWithFallback({
const override = agentOverrides[agentName]
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
// Check if agent requires a specific model
if (requirement?.requiresModel && availableModels) {
if (!isModelAvailable(requirement.requiresModel, availableModels)) {
continue
}
}
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
const resolution = applyModelResolution({
uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined,
userModel: override?.model,
fallbackChain: requirement?.fallbackChain,
requirement,
availableModels,
systemDefaultModel,
})
if (!resolution) continue
const { model, variant: resolvedVariant } = resolution
let config = buildAgent(source, model, mergedCategories, gitMasterConfig)
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider)
// Apply variant from override or requirement
if (override?.variant) {
config = { ...config, variant: override.variant }
} else if (requirement?.variant) {
config = { ...config, variant: requirement.variant }
// Apply resolved variant from model fallback chain
if (resolvedVariant) {
config = { ...config, variant: resolvedVariant }
}
if (agentName === "librarian" && directory && config.prompt) {
const envContext = createEnvContext()
config = { ...config, prompt: config.prompt + envContext }
// Expand override.category into concrete properties (higher priority than factory/resolved)
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
if (overrideCategory) {
config = applyCategoryOverride(config, overrideCategory, mergedCategories)
}
if (override) {
config = mergeAgentConfig(config, override)
if (agentName === "librarian") {
config = applyEnvironmentContext(config, directory)
}
config = applyOverrides(config, override, mergedCategories)
result[name] = config
const metadata = agentMetadata[agentName]
@@ -234,77 +321,71 @@ export async function createBuiltinAgents(
}
}
if (!disabledAgents.includes("Sisyphus")) {
const sisyphusOverride = agentOverrides["Sisyphus"]
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["Sisyphus"]
if (!disabledAgents.includes("sisyphus")) {
const sisyphusOverride = agentOverrides["sisyphus"]
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
// Use resolver to determine model
const { model: sisyphusModel } = resolveModelWithFallback({
const sisyphusResolution = applyModelResolution({
uiSelectedModel,
userModel: sisyphusOverride?.model,
fallbackChain: sisyphusRequirement?.fallbackChain,
requirement: sisyphusRequirement,
availableModels,
systemDefaultModel,
})
let sisyphusConfig = createSisyphusAgent(
sisyphusModel,
availableAgents,
undefined,
availableSkills,
availableCategories
)
if (sisyphusResolution) {
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
let sisyphusConfig = createSisyphusAgent(
sisyphusModel,
availableAgents,
undefined,
availableSkills,
availableCategories
)
if (sisyphusResolvedVariant) {
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
}
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories)
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
result["sisyphus"] = sisyphusConfig
}
}
if (!disabledAgents.includes("atlas")) {
const orchestratorOverride = agentOverrides["atlas"]
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
// Apply variant from override or requirement
if (sisyphusOverride?.variant) {
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusOverride.variant }
} else if (sisyphusRequirement?.variant) {
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusRequirement.variant }
}
if (directory && sisyphusConfig.prompt) {
const envContext = createEnvContext()
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
}
if (sisyphusOverride) {
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
}
result["Sisyphus"] = sisyphusConfig
}
if (!disabledAgents.includes("Atlas")) {
const orchestratorOverride = agentOverrides["Atlas"]
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["Atlas"]
// Use resolver to determine model
const { model: atlasModel } = resolveModelWithFallback({
const atlasResolution = applyModelResolution({
// NOTE: Atlas does NOT use uiSelectedModel - respects its own fallbackChain (k2p5 primary)
userModel: orchestratorOverride?.model,
fallbackChain: atlasRequirement?.fallbackChain,
requirement: atlasRequirement,
availableModels,
systemDefaultModel,
})
let orchestratorConfig = createAtlasAgent({
model: atlasModel,
availableAgents,
availableSkills,
userCategories: categories,
})
// Apply variant from override or requirement
if (orchestratorOverride?.variant) {
orchestratorConfig = { ...orchestratorConfig, variant: orchestratorOverride.variant }
} else if (atlasRequirement?.variant) {
orchestratorConfig = { ...orchestratorConfig, variant: atlasRequirement.variant }
if (atlasResolution) {
const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution
let orchestratorConfig = createAtlasAgent({
model: atlasModel,
availableAgents,
availableSkills,
userCategories: categories,
})
if (atlasResolvedVariant) {
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
}
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories)
result["atlas"] = orchestratorConfig
}
}
if (orchestratorOverride) {
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
}
result["Atlas"] = orchestratorConfig
}
return result
}
return result
}

View File

@@ -8,16 +8,17 @@ CLI entry: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics. Com
```
cli/
├── index.ts # Commander.js entry
├── index.ts # Commander.js entry (4 commands)
├── install.ts # Interactive TUI (520 lines)
├── config-manager.ts # JSONC parsing (641 lines)
├── config-manager.ts # JSONC parsing (664 lines)
├── types.ts # InstallArgs, InstallConfig
├── model-fallback.ts # Model fallback configuration
├── doctor/
│ ├── index.ts # Doctor entry
│ ├── runner.ts # Check orchestration
│ ├── formatter.ts # Colored output
│ ├── constants.ts # Check IDs, symbols
│ ├── types.ts # CheckResult, CheckDefinition
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
│ └── checks/ # 14 checks, 21 files
│ ├── version.ts # OpenCode + plugin version
│ ├── config.ts # JSONC validity, Zod
@@ -25,6 +26,7 @@ cli/
│ ├── dependencies.ts # AST-Grep, Comment Checker
│ ├── lsp.ts # LSP connectivity
│ ├── mcp.ts # MCP validation
│ ├── model-resolution.ts # Model resolution check
│ └── gh.ts # GitHub CLI
├── run/
│ └── index.ts # Session launcher
@@ -36,36 +38,37 @@ cli/
| Command | Purpose |
|---------|---------|
| `install` | Interactive setup |
| `doctor` | 14 health checks |
| `run` | Launch session |
| `get-local-version` | Version check |
| `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 CATEGORIES
## DOCTOR CATEGORIES (14 Checks)
| Category | Checks |
|----------|--------|
| installation | opencode, plugin |
| configuration | config validity, Zod |
| configuration | config validity, Zod, model-resolution |
| authentication | anthropic, openai, google |
| dependencies | ast-grep, comment-checker |
| 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`
2. Export from `checks/index.ts`
3. Add to `getAllCheckDefinitions()`
2. Export `getXXXCheckDefinition()` factory returning `CheckDefinition`
3. Add to `getAllCheckDefinitions()` in `checks/index.ts`
## TUI FRAMEWORK
- **@clack/prompts**: `select()`, `spinner()`, `intro()`
- **picocolors**: Terminal colors
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn)
- **@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()`
- **Silent failures**: Return warn/fail in doctor
- **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", () => {
@@ -211,6 +250,7 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #when generating config
@@ -219,7 +259,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", () => {
@@ -232,13 +272,14 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #when generating config
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", () => {
@@ -251,13 +292,14 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: true,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #when generating config
const result = generateOmoConfig(config)
// #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")
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", () => {
@@ -270,6 +312,7 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #when generating config
@@ -277,7 +320,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/glm-4.7-free")
})
test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => {
@@ -290,6 +333,7 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: true,
hasKimiForCoding: false,
}
// #when generating config
@@ -298,7 +342,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", () => {
@@ -311,13 +355,14 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #when generating config
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")
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)
@@ -334,6 +379,7 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #when generating config
@@ -343,7 +389,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,
@@ -353,12 +399,13 @@ describe("generateOmoConfig - model fallback system", () => {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
// #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 } },
},
},
},
},
@@ -575,27 +598,28 @@ export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
}
}
function detectProvidersFromOmoConfig(): { hasOpenAI: boolean; hasOpencodeZen: boolean; hasZaiCodingPlan: boolean } {
function detectProvidersFromOmoConfig(): { hasOpenAI: boolean; hasOpencodeZen: boolean; hasZaiCodingPlan: boolean; hasKimiForCoding: boolean } {
const omoConfigPath = getOmoConfig()
if (!existsSync(omoConfigPath)) {
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false }
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false }
}
try {
const content = readFileSync(omoConfigPath, "utf-8")
const omoConfig = parseJsonc<Record<string, unknown>>(content)
if (!omoConfig || typeof omoConfig !== "object") {
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false }
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false }
}
const configStr = JSON.stringify(omoConfig)
const hasOpenAI = configStr.includes('"openai/')
const hasOpencodeZen = configStr.includes('"opencode/')
const hasZaiCodingPlan = configStr.includes('"zai-coding-plan/')
const hasKimiForCoding = configStr.includes('"kimi-for-coding/')
return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan }
return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding }
} catch {
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false }
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false }
}
}
@@ -609,6 +633,7 @@ export function detectCurrentConfig(): DetectedConfig {
hasCopilot: false,
hasOpencodeZen: true,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
}
const { format, path } = detectConfigFormat()
@@ -632,10 +657,11 @@ export function detectCurrentConfig(): DetectedConfig {
// Gemini auth plugin detection still works via plugin presence
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan } = detectProvidersFromOmoConfig()
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
result.hasOpenAI = hasOpenAI
result.hasOpencodeZen = hasOpencodeZen
result.hasZaiCodingPlan = hasZaiCodingPlan
result.hasKimiForCoding = hasKimiForCoding
return result
}

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

@@ -8,6 +8,7 @@ import { getDependencyCheckDefinitions } from "./dependencies"
import { getGhCliCheckDefinition } from "./gh"
import { getLspCheckDefinition } from "./lsp"
import { getMcpCheckDefinitions } from "./mcp"
import { getMcpOAuthCheckDefinition } from "./mcp-oauth"
import { getVersionCheckDefinition } from "./version"
export * from "./opencode"
@@ -19,6 +20,7 @@ export * from "./dependencies"
export * from "./gh"
export * from "./lsp"
export * from "./mcp"
export * from "./mcp-oauth"
export * from "./version"
export function getAllCheckDefinitions(): CheckDefinition[] {
@@ -32,6 +34,7 @@ export function getAllCheckDefinitions(): CheckDefinition[] {
getGhCliCheckDefinition(),
getLspCheckDefinition(),
...getMcpCheckDefinitions(),
getMcpOAuthCheckDefinition(),
getVersionCheckDefinition(),
]
}

View File

@@ -0,0 +1,133 @@
import { describe, it, expect, spyOn, afterEach } from "bun:test"
import * as mcpOauth from "./mcp-oauth"
describe("mcp-oauth check", () => {
describe("getMcpOAuthCheckDefinition", () => {
it("returns check definition with correct properties", () => {
// #given
// #when getting definition
const def = mcpOauth.getMcpOAuthCheckDefinition()
// #then should have correct structure
expect(def.id).toBe("mcp-oauth-tokens")
expect(def.name).toBe("MCP OAuth Tokens")
expect(def.category).toBe("tools")
expect(def.critical).toBe(false)
expect(typeof def.check).toBe("function")
})
})
describe("checkMcpOAuthTokens", () => {
let readStoreSpy: ReturnType<typeof spyOn>
afterEach(() => {
readStoreSpy?.mockRestore()
})
it("returns skip when no tokens stored", async () => {
// #given no OAuth tokens configured
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue(null)
// #when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should skip
expect(result.status).toBe("skip")
expect(result.message).toContain("No OAuth")
})
it("returns pass when all tokens valid", async () => {
// #given valid tokens with future expiry (expiresAt is in epoch seconds)
const futureTime = Math.floor(Date.now() / 1000) + 3600
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"example.com/resource1": {
accessToken: "token1",
expiresAt: futureTime,
},
"example.com/resource2": {
accessToken: "token2",
expiresAt: futureTime,
},
})
// #when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("2")
expect(result.message).toContain("valid")
})
it("returns warn when some tokens expired", async () => {
// #given mix of valid and expired tokens (expiresAt is in epoch seconds)
const futureTime = Math.floor(Date.now() / 1000) + 3600
const pastTime = Math.floor(Date.now() / 1000) - 3600
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"example.com/resource1": {
accessToken: "token1",
expiresAt: futureTime,
},
"example.com/resource2": {
accessToken: "token2",
expiresAt: pastTime,
},
})
// #when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should warn
expect(result.status).toBe("warn")
expect(result.message).toContain("1")
expect(result.message).toContain("expired")
expect(result.details?.some((d: string) => d.includes("Expired"))).toBe(
true
)
})
it("returns pass when tokens have no expiry", async () => {
// #given tokens without expiry info
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"example.com/resource1": {
accessToken: "token1",
},
})
// #when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should pass (no expiry = assume valid)
expect(result.status).toBe("pass")
expect(result.message).toContain("1")
})
it("includes token details in output", async () => {
// #given multiple tokens
const futureTime = Math.floor(Date.now() / 1000) + 3600
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"api.example.com/v1": {
accessToken: "token1",
expiresAt: futureTime,
},
"auth.example.com/oauth": {
accessToken: "token2",
expiresAt: futureTime,
},
})
// #when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should list tokens in details
expect(result.details).toBeDefined()
expect(result.details?.length).toBeGreaterThan(0)
expect(
result.details?.some((d: string) => d.includes("api.example.com"))
).toBe(true)
expect(
result.details?.some((d: string) => d.includes("auth.example.com"))
).toBe(true)
})
})
})

View File

@@ -0,0 +1,80 @@
import type { CheckResult, CheckDefinition } from "../types"
import { CHECK_IDS, CHECK_NAMES } from "../constants"
import { getMcpOauthStoragePath } from "../../../features/mcp-oauth/storage"
import { existsSync, readFileSync } from "node:fs"
interface OAuthTokenData {
accessToken: string
refreshToken?: string
expiresAt?: number
clientInfo?: {
clientId: string
clientSecret?: string
}
}
type TokenStore = Record<string, OAuthTokenData>
export function readTokenStore(): TokenStore | null {
const filePath = getMcpOauthStoragePath()
if (!existsSync(filePath)) {
return null
}
try {
const content = readFileSync(filePath, "utf-8")
return JSON.parse(content) as TokenStore
} catch {
return null
}
}
export async function checkMcpOAuthTokens(): Promise<CheckResult> {
const store = readTokenStore()
if (!store || Object.keys(store).length === 0) {
return {
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
status: "skip",
message: "No OAuth tokens configured",
details: ["Optional: Configure OAuth tokens for MCP servers"],
}
}
const now = Math.floor(Date.now() / 1000)
const tokens = Object.entries(store)
const expiredTokens = tokens.filter(
([, token]) => token.expiresAt && token.expiresAt < now
)
if (expiredTokens.length > 0) {
return {
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
status: "warn",
message: `${expiredTokens.length} of ${tokens.length} token(s) expired`,
details: [
...tokens
.filter(([, token]) => !token.expiresAt || token.expiresAt >= now)
.map(([key]) => `Valid: ${key}`),
...expiredTokens.map(([key]) => `Expired: ${key}`),
],
}
}
return {
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
status: "pass",
message: `${tokens.length} OAuth token(s) valid`,
details: tokens.map(([key]) => `Configured: ${key}`),
}
}
export function getMcpOAuthCheckDefinition(): CheckDefinition {
return {
id: CHECK_IDS.MCP_OAUTH_TOKENS,
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
category: "tools",
check: checkMcpOAuthTokens,
critical: false,
}
}

View File

@@ -12,7 +12,7 @@ describe("model-resolution check", () => {
const info = getModelResolutionInfo()
// #then: Should have agent entries
const sisyphus = info.agents.find((a) => a.name === "Sisyphus")
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")
@@ -27,7 +27,7 @@ describe("model-resolution check", () => {
// #then: Should have category entries
const visual = info.categories.find((c) => c.name === "visual-engineering")
expect(visual).toBeDefined()
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro-preview")
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro")
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("google")
})
})
@@ -84,7 +84,7 @@ describe("model-resolution check", () => {
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: Should show provider fallback chain
const sisyphus = info.agents.find((a) => a.name === "Sisyphus")
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
expect(sisyphus).toBeDefined()
expect(sisyphus!.userOverride).toBeUndefined()
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
@@ -97,13 +97,14 @@ describe("model-resolution check", () => {
// #when: Running the model resolution check
// #then: Returns pass with details showing resolution flow
it("returns pass status with agent and category counts", async () => {
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 and show counts
expect(result.status).toBe("pass")
// #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?/)
})
@@ -115,8 +116,9 @@ describe("model-resolution check", () => {
// #then: Details should contain agent/category resolution info
expect(result.details).toBeDefined()
expect(result.details!.length).toBeGreaterThan(0)
// Should have Current Models header and sections
expect(result.details!.some((d) => d.includes("Current Models"))).toBe(true)
// 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

View File

@@ -1,4 +1,4 @@
import { readFileSync } from "node:fs"
import { readFileSync, existsSync } from "node:fs"
import type { CheckResult, CheckDefinition } from "../types"
import { CHECK_IDS, CHECK_NAMES } from "../constants"
import { parseJsonc, detectConfigFile } from "../../../shared"
@@ -10,6 +10,38 @@ import {
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)
@@ -155,10 +187,30 @@ function getEffectiveVariant(requirement: ModelRequirement): string | undefined
return firstEntry?.variant ?? requirement.variant
}
function buildDetailsArray(info: ModelResolutionInfo): string[] {
interface AvailableModelsInfo {
providers: string[]
modelCount: number
cacheExists: boolean
}
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo): string[] {
const details: string[] = []
details.push("═══ Current Models ═══")
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) {
@@ -182,6 +234,7 @@ function buildDetailsArray(info: ModelResolutionInfo): string[] {
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
@@ -190,12 +243,13 @@ export async function checkModelResolution(): Promise<CheckResult> {
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: "pass",
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}`,
details: buildDetailsArray(info),
status: available.cacheExists ? "pass" : "warn",
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
details: buildDetailsArray(info, available),
}
}

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

@@ -32,6 +32,7 @@ export const CHECK_IDS = {
LSP_SERVERS: "lsp-servers",
MCP_BUILTIN: "mcp-builtin",
MCP_USER: "mcp-user",
MCP_OAUTH_TOKENS: "mcp-oauth-tokens",
VERSION_STATUS: "version-status",
} as const
@@ -50,6 +51,7 @@ export const CHECK_NAMES: Record<string, string> = {
[CHECK_IDS.LSP_SERVERS]: "LSP Servers",
[CHECK_IDS.MCP_BUILTIN]: "Built-in MCP Servers",
[CHECK_IDS.MCP_USER]: "User MCP Configuration",
[CHECK_IDS.MCP_OAUTH_TOKENS]: "MCP OAuth Tokens",
[CHECK_IDS.VERSION_STATUS]: "Version Status",
} as const

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

View File

@@ -4,6 +4,7 @@ import { install } from "./install"
import { run } from "./run"
import { getLocalVersion } from "./get-local-version"
import { doctor } from "./doctor"
import { createMcpOAuthCommand } from "./mcp-oauth"
import type { InstallArgs } from "./types"
import type { RunOptions } from "./run"
import type { GetLocalVersionOptions } from "./get-local-version/types"
@@ -29,6 +30,7 @@ program
.option("--copilot <value>", "GitHub Copilot subscription: no, yes")
.option("--opencode-zen <value>", "OpenCode Zen access: no, yes (default: no)")
.option("--zai-coding-plan <value>", "Z.ai Coding Plan subscription: no, yes (default: no)")
.option("--kimi-for-coding <value>", "Kimi For Coding subscription: no, yes (default: no)")
.option("--skip-auth", "Skip authentication setup hints")
.addHelpText("after", `
Examples:
@@ -36,13 +38,14 @@ Examples:
$ bunx oh-my-opencode install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no
$ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes --opencode-zen=yes
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai):
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
Claude Native anthropic/ models (Opus, Sonnet, Haiku)
OpenAI Native openai/ models (GPT-5.2 for Oracle)
Gemini Native google/ models (Gemini 3 Pro, Flash)
Copilot github-copilot/ models (fallback)
OpenCode Zen opencode/ models (opencode/claude-opus-4-5, etc.)
Z.ai zai-coding-plan/glm-4.7 (Librarian priority)
Kimi kimi-for-coding/k2p5 (Sisyphus/Prometheus fallback)
`)
.action(async (options) => {
const args: InstallArgs = {
@@ -53,6 +56,7 @@ Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai):
copilot: options.copilot,
opencodeZen: options.opencodeZen,
zaiCodingPlan: options.zaiCodingPlan,
kimiForCoding: options.kimiForCoding,
skipAuth: options.skipAuth ?? false,
}
const exitCode = await install(args)
@@ -150,4 +154,6 @@ program
console.log(`oh-my-opencode v${VERSION}`)
})
program.addCommand(createMcpOAuthCommand())
program.parse()

View File

@@ -44,7 +44,8 @@ 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(formatProvider("Kimi For Coding", config.hasKimiForCoding, "Sisyphus/Prometheus fallback"))
lines.push("")
lines.push(color.dim("─".repeat(40)))
@@ -141,6 +142,10 @@ function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string
errors.push(`Invalid --zai-coding-plan value: ${args.zaiCodingPlan} (expected: no, yes)`)
}
if (args.kimiForCoding !== undefined && !["no", "yes"].includes(args.kimiForCoding)) {
errors.push(`Invalid --kimi-for-coding value: ${args.kimiForCoding} (expected: no, yes)`)
}
return { valid: errors.length === 0, errors }
}
@@ -153,10 +158,11 @@ function argsToConfig(args: InstallArgs): InstallConfig {
hasCopilot: args.copilot === "yes",
hasOpencodeZen: args.opencodeZen === "yes",
hasZaiCodingPlan: args.zaiCodingPlan === "yes",
hasKimiForCoding: args.kimiForCoding === "yes",
}
}
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; openai: BooleanArg; gemini: BooleanArg; copilot: BooleanArg; opencodeZen: BooleanArg; zaiCodingPlan: BooleanArg } {
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; openai: BooleanArg; gemini: BooleanArg; copilot: BooleanArg; opencodeZen: BooleanArg; zaiCodingPlan: BooleanArg; kimiForCoding: BooleanArg } {
let claude: ClaudeSubscription = "no"
if (detected.hasClaude) {
claude = detected.isMax20 ? "max20" : "yes"
@@ -169,6 +175,7 @@ function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubs
copilot: detected.hasCopilot ? "yes" : "no",
opencodeZen: detected.hasOpencodeZen ? "yes" : "no",
zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no",
kimiForCoding: detected.hasKimiForCoding ? "yes" : "no",
}
}
@@ -250,7 +257,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,
})
@@ -260,6 +267,20 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
return null
}
const kimiForCoding = await p.select({
message: "Do you have a Kimi For Coding subscription?",
options: [
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
{ value: "yes" as const, label: "Yes", hint: "Kimi K2.5 for Sisyphus/Prometheus fallback" },
],
initialValue: initial.kimiForCoding,
})
if (p.isCancel(kimiForCoding)) {
p.cancel("Installation cancelled.")
return null
}
return {
hasClaude: claude !== "no",
isMax20: claude === "max20",
@@ -268,6 +289,7 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
hasCopilot: copilot === "yes",
hasOpencodeZen: opencodeZen === "yes",
hasZaiCodingPlan: zaiCodingPlan === "yes",
hasKimiForCoding: kimiForCoding === "yes",
}
}
@@ -378,7 +400,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
)
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
console.log(` ${color.dim("gh repo star code-yeongyu/oh-my-opencode")}`)
console.log(` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`)
console.log()
console.log(color.dim("oMoMoMoMo... Enjoy!"))
console.log()
@@ -496,7 +518,7 @@ export async function install(args: InstallArgs): Promise<number> {
)
p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`)
p.log.message(` ${color.dim("gh repo star code-yeongyu/oh-my-opencode")}`)
p.log.message(` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`)
p.outro(color.green("oMoMoMoMo... Enjoy!"))

View File

@@ -0,0 +1,123 @@
import { describe, it, expect } from "bun:test"
import { Command } from "commander"
import { createMcpOAuthCommand } from "./index"
describe("mcp oauth command", () => {
describe("command structure", () => {
it("creates mcp command group with oauth subcommand", () => {
// given
const mcpCommand = createMcpOAuthCommand()
// when
const subcommands = mcpCommand.commands.map((cmd: Command) => cmd.name())
// then
expect(subcommands).toContain("oauth")
})
it("oauth subcommand has login, logout, and status subcommands", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
// when
const subcommands = oauthCommand?.commands.map((cmd: Command) => cmd.name()) ?? []
// then
expect(subcommands).toContain("login")
expect(subcommands).toContain("logout")
expect(subcommands).toContain("status")
})
})
describe("login subcommand", () => {
it("exists and has description", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
// when
const description = loginCommand?.description() ?? ""
// then
expect(loginCommand).toBeDefined()
expect(description).toContain("OAuth")
})
it("accepts --server-url option", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
// when
const options = loginCommand?.options ?? []
const serverUrlOption = options.find((opt: { long?: string }) => opt.long === "--server-url")
// then
expect(serverUrlOption).toBeDefined()
})
it("accepts --client-id option", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
// when
const options = loginCommand?.options ?? []
const clientIdOption = options.find((opt: { long?: string }) => opt.long === "--client-id")
// then
expect(clientIdOption).toBeDefined()
})
it("accepts --scopes option", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
// when
const options = loginCommand?.options ?? []
const scopesOption = options.find((opt: { long?: string }) => opt.long === "--scopes")
// then
expect(scopesOption).toBeDefined()
})
})
describe("logout subcommand", () => {
it("exists and has description", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
const logoutCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "logout")
// when
const description = logoutCommand?.description() ?? ""
// then
expect(logoutCommand).toBeDefined()
expect(description).toContain("tokens")
})
})
describe("status subcommand", () => {
it("exists and has description", () => {
// given
const mcpCommand = createMcpOAuthCommand()
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
const statusCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "status")
// when
const description = statusCommand?.description() ?? ""
// then
expect(statusCommand).toBeDefined()
expect(description).toContain("status")
})
})
})

View File

@@ -0,0 +1,43 @@
import { Command } from "commander"
import { login } from "./login"
import { logout } from "./logout"
import { status } from "./status"
export function createMcpOAuthCommand(): Command {
const mcp = new Command("mcp").description("MCP server management")
const oauth = new Command("oauth").description("OAuth token management for MCP servers")
oauth
.command("login <server-name>")
.description("Authenticate with an MCP server using OAuth")
.option("--server-url <url>", "OAuth server URL (required if not in config)")
.option("--client-id <id>", "OAuth client ID (optional, uses DCR if not provided)")
.option("--scopes <scopes...>", "OAuth scopes to request")
.action(async (serverName: string, options) => {
const exitCode = await login(serverName, options)
process.exit(exitCode)
})
oauth
.command("logout <server-name>")
.description("Remove stored OAuth tokens for an MCP server")
.option("--server-url <url>", "OAuth server URL (use if server name differs from URL)")
.action(async (serverName: string, options) => {
const exitCode = await logout(serverName, options)
process.exit(exitCode)
})
oauth
.command("status [server-name]")
.description("Show OAuth token status for MCP servers")
.action(async (serverName: string | undefined) => {
const exitCode = await status(serverName)
process.exit(exitCode)
})
mcp.addCommand(oauth)
return mcp
}
export { login, logout, status }

View File

@@ -0,0 +1,80 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 }))
mock.module("../../features/mcp-oauth/provider", () => ({
McpOAuthProvider: class MockMcpOAuthProvider {
constructor(public options: { serverUrl: string; clientId?: string; scopes?: string[] }) {}
async login() {
return mockLogin()
}
},
}))
const { login } = await import("./login")
describe("login command", () => {
beforeEach(() => {
mockLogin.mockClear()
})
afterEach(() => {
// cleanup
})
it("returns error code when server-url is not provided", async () => {
// given
const serverName = "test-server"
const options = {}
// when
const exitCode = await login(serverName, options)
// then
expect(exitCode).toBe(1)
})
it("returns success code when login succeeds", async () => {
// given
const serverName = "test-server"
const options = {
serverUrl: "https://oauth.example.com",
}
// when
const exitCode = await login(serverName, options)
// then
expect(exitCode).toBe(0)
expect(mockLogin).toHaveBeenCalledTimes(1)
})
it("returns error code when login throws", async () => {
// given
const serverName = "test-server"
const options = {
serverUrl: "https://oauth.example.com",
}
mockLogin.mockRejectedValueOnce(new Error("Network error"))
// when
const exitCode = await login(serverName, options)
// then
expect(exitCode).toBe(1)
})
it("returns error code when server-url is missing", async () => {
// given
const serverName = "test-server"
const options = {
clientId: "test-client-id",
}
// when
const exitCode = await login(serverName, options)
// then
expect(exitCode).toBe(1)
})
})

View File

@@ -0,0 +1,38 @@
import { McpOAuthProvider } from "../../features/mcp-oauth/provider"
export interface LoginOptions {
serverUrl?: string
clientId?: string
scopes?: string[]
}
export async function login(serverName: string, options: LoginOptions): Promise<number> {
try {
const serverUrl = options.serverUrl
if (!serverUrl) {
console.error(`Error: --server-url is required for server "${serverName}"`)
return 1
}
const provider = new McpOAuthProvider({
serverUrl,
clientId: options.clientId,
scopes: options.scopes,
})
console.log(`Authenticating with ${serverName}...`)
const tokenData = await provider.login()
console.log(`✓ Successfully authenticated with ${serverName}`)
if (tokenData.expiresAt) {
const expiryDate = new Date(tokenData.expiresAt * 1000)
console.log(` Token expires at: ${expiryDate.toISOString()}`)
}
return 0
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`Error: Failed to authenticate with ${serverName}: ${message}`)
return 1
}
}

View File

@@ -0,0 +1,65 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
import { existsSync, mkdirSync, rmSync } from "node:fs"
import { join } from "node:path"
import { tmpdir } from "node:os"
import { saveToken } from "../../features/mcp-oauth/storage"
const { logout } = await import("./logout")
describe("logout command", () => {
const TEST_CONFIG_DIR = join(tmpdir(), "mcp-oauth-logout-test-" + Date.now())
let originalConfigDir: string | undefined
beforeEach(() => {
originalConfigDir = process.env.OPENCODE_CONFIG_DIR
process.env.OPENCODE_CONFIG_DIR = TEST_CONFIG_DIR
if (!existsSync(TEST_CONFIG_DIR)) {
mkdirSync(TEST_CONFIG_DIR, { recursive: true })
}
})
afterEach(() => {
if (originalConfigDir === undefined) {
delete process.env.OPENCODE_CONFIG_DIR
} else {
process.env.OPENCODE_CONFIG_DIR = originalConfigDir
}
if (existsSync(TEST_CONFIG_DIR)) {
rmSync(TEST_CONFIG_DIR, { recursive: true, force: true })
}
})
it("returns success code when logout succeeds", async () => {
// given
const serverUrl = "https://test-server.example.com"
saveToken(serverUrl, serverUrl, { accessToken: "test-token" })
// when
const exitCode = await logout("test-server", { serverUrl })
// then
expect(exitCode).toBe(0)
})
it("handles non-existent server gracefully", async () => {
// given
const serverName = "non-existent-server"
// when
const exitCode = await logout(serverName, { serverUrl: "https://nonexistent.example.com" })
// then
expect(exitCode).toBe(0)
})
it("returns error when --server-url is not provided", async () => {
// given
const serverName = "test-server"
// when
const exitCode = await logout(serverName)
// then
expect(exitCode).toBe(1)
})
})

View File

@@ -0,0 +1,30 @@
import { deleteToken } from "../../features/mcp-oauth/storage"
export interface LogoutOptions {
serverUrl?: string
}
export async function logout(serverName: string, options?: LogoutOptions): Promise<number> {
try {
const serverUrl = options?.serverUrl
if (!serverUrl) {
console.error(`Error: --server-url is required for logout. Token storage uses server URLs, not names.`)
console.error(` Usage: mcp oauth logout ${serverName} --server-url https://your-server.example.com`)
return 1
}
const success = deleteToken(serverUrl, serverUrl)
if (success) {
console.log(`✓ Successfully removed tokens for ${serverName}`)
return 0
}
console.error(`Error: Failed to remove tokens for ${serverName}`)
return 1
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`Error: Failed to remove tokens for ${serverName}: ${message}`)
return 1
}
}

View File

@@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
import { status } from "./status"
describe("status command", () => {
beforeEach(() => {
// setup
})
afterEach(() => {
// cleanup
})
it("returns success code when checking status for specific server", async () => {
// given
const serverName = "test-server"
// when
const exitCode = await status(serverName)
// then
expect(typeof exitCode).toBe("number")
expect(exitCode).toBe(0)
})
it("returns success code when checking status for all servers", async () => {
// given
const serverName = undefined
// when
const exitCode = await status(serverName)
// then
expect(typeof exitCode).toBe("number")
expect(exitCode).toBe(0)
})
it("handles non-existent server gracefully", async () => {
// given
const serverName = "non-existent-server"
// when
const exitCode = await status(serverName)
// then
expect(typeof exitCode).toBe("number")
expect(exitCode).toBe(0)
})
})

View File

@@ -0,0 +1,50 @@
import { listAllTokens, listTokensByHost } from "../../features/mcp-oauth/storage"
export async function status(serverName: string | undefined): Promise<number> {
try {
if (serverName) {
const tokens = listTokensByHost(serverName)
if (Object.keys(tokens).length === 0) {
console.log(`No tokens found for ${serverName}`)
return 0
}
console.log(`OAuth Status for ${serverName}:`)
for (const [key, token] of Object.entries(tokens)) {
console.log(` ${key}:`)
console.log(` Access Token: [REDACTED]`)
if (token.refreshToken) {
console.log(` Refresh Token: [REDACTED]`)
}
if (token.expiresAt) {
const expiryDate = new Date(token.expiresAt * 1000)
const now = Date.now() / 1000
const isExpired = token.expiresAt < now
const tokenStatus = isExpired ? "EXPIRED" : "VALID"
console.log(` Expiry: ${expiryDate.toISOString()} (${tokenStatus})`)
}
}
return 0
}
const tokens = listAllTokens()
if (Object.keys(tokens).length === 0) {
console.log("No OAuth tokens stored")
return 0
}
console.log("Stored OAuth Tokens:")
for (const [key, token] of Object.entries(tokens)) {
const isExpired = token.expiresAt && token.expiresAt < Date.now() / 1000
const tokenStatus = isExpired ? "EXPIRED" : "VALID"
console.log(` ${key}: ${tokenStatus}`)
}
return 0
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(`Error: Failed to get token status: ${message}`)
return 1
}
}

View File

@@ -12,6 +12,7 @@ function createConfig(overrides: Partial<InstallConfig> = {}): InstallConfig {
hasCopilot: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
...overrides,
}
}
@@ -310,19 +311,19 @@ describe("generateModelConfig", () => {
})
describe("explore agent special cases", () => {
test("explore uses Gemini flash when Gemini available", () => {
// #given Gemini is available
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 gemini-3-flash-preview
expect(result.agents?.explore?.model).toBe("google/gemini-3-flash-preview")
// #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 + isMax20 but no Gemini", () => {
// #given Claude is available with Max 20 plan but no Gemini
test("explore uses Claude haiku when Claude available", () => {
// #given Claude is available
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
@@ -332,26 +333,37 @@ describe("generateModelConfig", () => {
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
})
test("explore uses grok-code when Claude without isMax20 and no Gemini", () => {
// #given Claude is available without Max 20 plan and no Gemini
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 grok-code
expect(result.agents?.explore?.model).toBe("opencode/grok-code")
// #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 grok-code when only OpenAI available", () => {
test("explore uses gpt-5-nano when only OpenAI available", () => {
// #given only OpenAI is available
const config = createConfig({ hasOpenAI: true })
// #when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use grok-code (fallback)
expect(result.agents?.explore?.model).toBe("opencode/grok-code")
// #then explore should use gpt-5-nano (fallback)
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
})
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")
})
})
@@ -364,7 +376,7 @@ describe("generateModelConfig", () => {
const result = generateModelConfig(config)
// #then Sisyphus should use opus (sisyphus-high)
expect(result.agents?.Sisyphus?.model).toBe("anthropic/claude-opus-4-5")
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
})
test("Sisyphus uses sisyphus-low capability when isMax20 is false", () => {
@@ -375,7 +387,7 @@ describe("generateModelConfig", () => {
const result = generateModelConfig(config)
// #then Sisyphus should use sonnet (sisyphus-low)
expect(result.agents?.Sisyphus?.model).toBe("anthropic/claude-sonnet-4-5")
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-sonnet-4-5")
})
})

View File

@@ -14,6 +14,7 @@ interface ProviderAvailability {
opencodeZen: boolean
copilot: boolean
zai: boolean
kimiForCoding: boolean
isMaxPlan: boolean
}
@@ -49,6 +50,7 @@ function toProviderAvailability(config: InstallConfig): ProviderAvailability {
opencodeZen: config.hasOpencodeZen,
copilot: config.hasCopilot,
zai: config.hasZaiCodingPlan,
kimiForCoding: config.hasKimiForCoding,
isMaxPlan: config.isMax20,
}
}
@@ -61,6 +63,7 @@ function isProviderAvailable(provider: string, avail: ProviderAvailability): boo
"github-copilot": avail.copilot,
opencode: avail.opencodeZen,
"zai-coding-plan": avail.zai,
"kimi-for-coding": avail.kimiForCoding,
}
return mapping[provider] ?? false
}
@@ -97,13 +100,15 @@ function resolveModelFromChain(
function getSisyphusFallbackChain(isMaxPlan: boolean): FallbackEntry[] {
// Sisyphus uses opus when isMaxPlan, sonnet otherwise
if (isMaxPlan) {
return AGENT_MODEL_REQUIREMENTS.Sisyphus.fallbackChain
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: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
]
}
@@ -115,7 +120,8 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
avail.native.gemini ||
avail.opencodeZen ||
avail.copilot ||
avail.zai
avail.zai ||
avail.kimiForCoding
if (!hasAnyProvider) {
return {
@@ -139,21 +145,23 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
continue
}
// Special case: explore has custom Gemini → Claude → Grok logic
// Special case: explore uses Claude haiku → GitHub Copilot gpt-5-mini → OpenCode gpt-5-nano
if (role === "explore") {
if (avail.native.gemini) {
agents[role] = { model: "google/gemini-3-flash-preview" }
} else if (avail.native.claude && avail.isMaxPlan) {
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
role === "sisyphus" ? getSisyphusFallbackChain(avail.isMaxPlan) : req.fallbackChain
const resolved = resolveModelFromChain(fallbackChain, avail)
if (resolved) {

View File

@@ -82,6 +82,7 @@ describe("createEventState", () => {
expect(state.lastOutput).toBe("")
expect(state.lastPartText).toBe("")
expect(state.currentTool).toBe(null)
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
})
@@ -126,6 +127,119 @@ describe("event handling", () => {
expect(state.mainSessionIdle).toBe(false)
})
it("hasReceivedMeaningfulWork is false initially after session.idle", async () => {
// #given - session goes idle without any assistant output (race condition scenario)
const ctx = createMockContext("my-session")
const state = createEventState()
const payload: EventPayload = {
type: "session.idle",
properties: { sessionID: "my-session" },
}
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
await processEvents(ctx, events, state)
// #then - idle but no meaningful work yet
expect(state.mainSessionIdle).toBe(true)
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
it("message.updated with assistant role sets hasReceivedMeaningfulWork", async () => {
// #given
const ctx = createMockContext("my-session")
const state = createEventState()
const payload: EventPayload = {
type: "message.updated",
properties: {
info: { sessionID: "my-session", role: "assistant" },
},
}
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
await processEvents(ctx, events, state)
// #then
expect(state.hasReceivedMeaningfulWork).toBe(true)
})
it("message.updated with user role does not set hasReceivedMeaningfulWork", async () => {
// #given - user message should not count as meaningful work
const ctx = createMockContext("my-session")
const state = createEventState()
const payload: EventPayload = {
type: "message.updated",
properties: {
info: { sessionID: "my-session", role: "user" },
},
}
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
await processEvents(ctx, events, state)
// #then - user role should not count as meaningful work
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
it("tool.execute sets hasReceivedMeaningfulWork", async () => {
// #given
const ctx = createMockContext("my-session")
const state = createEventState()
const payload: EventPayload = {
type: "tool.execute",
properties: {
sessionID: "my-session",
name: "read_file",
input: { filePath: "/src/index.ts" },
},
}
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
await processEvents(ctx, events, state)
// #then
expect(state.hasReceivedMeaningfulWork).toBe(true)
})
it("tool.execute from different session does not set hasReceivedMeaningfulWork", async () => {
// #given
const ctx = createMockContext("my-session")
const state = createEventState()
const payload: EventPayload = {
type: "tool.execute",
properties: {
sessionID: "other-session",
name: "read_file",
input: { filePath: "/src/index.ts" },
},
}
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
await processEvents(ctx, events, state)
// #then - different session's tool call shouldn't count
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
it("session.status with busy type sets mainSessionIdle to false", async () => {
// #given
const ctx = createMockContext("my-session")
@@ -136,6 +250,7 @@ describe("event handling", () => {
lastOutput: "",
lastPartText: "",
currentTool: null,
hasReceivedMeaningfulWork: false,
}
const payload: EventPayload = {

View File

@@ -63,6 +63,8 @@ export interface EventState {
lastOutput: string
lastPartText: string
currentTool: string | null
/** Set to true when the main session has produced meaningful work (text, tool call, or tool result) */
hasReceivedMeaningfulWork: boolean
}
export function createEventState(): EventState {
@@ -73,6 +75,7 @@ export function createEventState(): EventState {
lastOutput: "",
lastPartText: "",
currentTool: null,
hasReceivedMeaningfulWork: false,
}
}
@@ -113,7 +116,9 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
const isMainSession = sessionID === ctx.sessionID
const sessionTag = isMainSession
? pc.green("[MAIN]")
: pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
: sessionID
? pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
: pc.dim("[system]")
switch (payload.type) {
case "session.idle":
@@ -124,8 +129,6 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
}
case "message.part.updated": {
// Skip verbose logging for partial message updates
// Only log tool invocation state changes, not text streaming
const partProps = props as MessagePartUpdatedProps | undefined
const part = partProps?.part
if (part?.type === "tool-invocation") {
@@ -133,6 +136,11 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
console.error(
pc.dim(`${sessionTag} message.part (tool): ${toolPart.toolName} [${toolPart.state}]`)
)
} else if (part?.type === "text" && part.text) {
const preview = part.text.slice(0, 80).replace(/\n/g, "\\n")
console.error(
pc.dim(`${sessionTag} message.part (text): "${preview}${part.text.length > 80 ? "..." : ""}"`)
)
}
break
}
@@ -140,11 +148,10 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
case "message.updated": {
const msgProps = props as MessageUpdatedProps | undefined
const role = msgProps?.info?.role ?? "unknown"
const content = msgProps?.content ?? ""
const preview = content.slice(0, 100).replace(/\n/g, "\\n")
console.error(
pc.dim(`${sessionTag} message.updated (${role}): "${preview}${content.length > 100 ? "..." : ""}"`)
)
const model = msgProps?.info?.modelID
const agent = msgProps?.info?.agent
const details = [role, agent, model].filter(Boolean).join(", ")
console.error(pc.dim(`${sessionTag} message.updated (${details})`))
break
}
@@ -241,6 +248,7 @@ function handleMessagePartUpdated(
const newText = part.text.slice(state.lastPartText.length)
if (newText) {
process.stdout.write(newText)
state.hasReceivedMeaningfulWork = true
}
state.lastPartText = part.text
}
@@ -257,16 +265,7 @@ function handleMessageUpdated(
if (props?.info?.sessionID !== ctx.sessionID) return
if (props?.info?.role !== "assistant") return
const content = props.content
if (!content || content === state.lastOutput) return
if (state.lastPartText.length === 0) {
const newContent = content.slice(state.lastOutput.length)
if (newContent) {
process.stdout.write(newContent)
}
}
state.lastOutput = content
state.hasReceivedMeaningfulWork = true
}
function handleToolExecute(
@@ -296,6 +295,7 @@ function handleToolExecute(
}
}
state.hasReceivedMeaningfulWork = true
process.stdout.write(`\n${pc.cyan(">")} ${pc.bold(toolName)}${inputPreview}\n`)
}

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 = () => {
@@ -133,6 +143,14 @@ export async function run(options: RunOptions): Promise<number> {
process.exit(1)
}
// Guard against premature completion: don't check completion until the
// session has produced meaningful work (text output, tool call, or tool result).
// Without this, a session that goes busy->idle before the LLM responds
// would exit immediately because 0 todos + 0 children = "complete".
if (!eventState.hasReceivedMeaningfulWork) {
continue
}
const shouldExit = await checkCompletionConditions(ctx)
if (shouldExit) {
console.log(pc.green("\n\nAll tasks completed."))

View File

@@ -44,8 +44,13 @@ export interface SessionStatusProps {
}
export interface MessageUpdatedProps {
info?: { sessionID?: string; role?: string }
content?: string
info?: {
sessionID?: string
role?: string
modelID?: string
providerID?: string
agent?: string
}
}
export interface MessagePartUpdatedProps {

View File

@@ -9,6 +9,7 @@ export interface InstallArgs {
copilot?: BooleanArg
opencodeZen?: BooleanArg
zaiCodingPlan?: BooleanArg
kimiForCoding?: BooleanArg
skipAuth?: boolean
}
@@ -20,6 +21,7 @@ export interface InstallConfig {
hasCopilot: boolean
hasOpencodeZen: boolean
hasZaiCodingPlan: boolean
hasKimiForCoding: boolean
}
export interface ConfigMergeResult {
@@ -37,4 +39,5 @@ export interface DetectedConfig {
hasCopilot: boolean
hasOpencodeZen: boolean
hasZaiCodingPlan: boolean
hasKimiForCoding: boolean
}

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,8 +85,10 @@ export const HookNameSchema = z.enum([
"edit-error-recovery",
"delegate-task-retry",
"prometheus-md-only",
"sisyphus-junior-notepad",
"start-work",
"atlas",
"stop-continuation-guard",
])
export const BuiltinCommandNameSchema = z.enum([
@@ -112,22 +117,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({
@@ -159,7 +177,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(),
@@ -170,6 +188,7 @@ export const CategoryConfigSchema = z.object({
export const BuiltinCategoryNameSchema = z.enum([
"visual-engineering",
"ultrabrain",
"deep",
"artistry",
"quick",
"unspecified-low",
@@ -296,6 +315,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(),
@@ -315,6 +384,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>
@@ -337,5 +409,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,31 +2,31 @@
## OVERVIEW
Core feature modules + Claude Code compatibility layer. Background agents, skill MCP, builtin skills/commands, 5 loaders.
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 (1335 lines)
├── background-agent/ # Task lifecycle (1377 lines)
│ ├── manager.ts # Launch → poll → complete
── concurrency.ts # Per-provider limits
│ └── types.ts # BackgroundTask, LaunchInput
── skill-mcp-manager/ # MCP client lifecycle
│ ├── manager.ts # Lazy loading, cleanup
│ └── types.ts # SkillMcpConfig
├── builtin-skills/ # Playwright, git-master, frontend-ui-ux
│ └── skills.ts # 1203 lines
├── builtin-commands/ # ralph-loop, refactor, init-deep
── 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
├── claude-code-mcp-loader/ # .mcp.json with ${VAR} expansion
├── claude-code-plugin-loader/ # installed_plugins.json
├── 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
── 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
@@ -41,8 +41,9 @@ features/
- **Lifecycle**: `launch``poll` (2s) → `complete`
- **Stability**: 3 consecutive polls = idle
- **Concurrency**: Per-provider/model limits
- **Concurrency**: Per-provider/model limits via `ConcurrencyManager`
- **Cleanup**: 30m TTL, 3m stale timeout
- **State**: Per-session Maps, cleaned on `session.deleted`
## SKILL MCP
@@ -55,3 +56,4 @@ features/
- **Sequential delegation**: Use `delegate_task` parallel
- **Trust self-reports**: ALWAYS verify
- **Main thread blocks**: No heavy I/O in loader init
- **Direct state mutation**: Use managers for boulder/session state

View File

@@ -176,8 +176,8 @@ describe("ConcurrencyManager.acquire/release", () => {
await manager.acquire("model-a")
await manager.acquire("model-a")
// #then - both resolved without waiting
expect(true).toBe(true)
// #then - both resolved without waiting, count should be 2
expect(manager.getCount("model-a")).toBe(2)
})
test("should allow acquires up to default limit of 5", async () => {
@@ -190,8 +190,8 @@ describe("ConcurrencyManager.acquire/release", () => {
await manager.acquire("model-a")
await manager.acquire("model-a")
// #then - all 5 resolved
expect(true).toBe(true)
// #then - all 5 resolved, count should be 5
expect(manager.getCount("model-a")).toBe(5)
})
test("should queue when limit reached", async () => {
@@ -276,8 +276,8 @@ describe("ConcurrencyManager.acquire/release", () => {
manager.release("model-a")
await manager.acquire("model-a")
// #then
expect(true).toBe(true)
// #then - count should be 1 after re-acquiring
expect(manager.getCount("model-a")).toBe(1)
})
test("should handle release when no acquire", () => {
@@ -288,21 +288,21 @@ describe("ConcurrencyManager.acquire/release", () => {
// #when - release without acquire
manager.release("model-a")
// #then - should not throw
expect(true).toBe(true)
// #then - count should be 0 (no negative count)
expect(manager.getCount("model-a")).toBe(0)
})
test("should handle release when no prior acquire", () => {
// #given - default config
// #when - release without acquire
manager.release("model-a")
// #when - release without acquire
manager.release("model-a")
// #then - should not throw
expect(true).toBe(true)
})
// #then - count should be 0 (no negative count)
expect(manager.getCount("model-a")).toBe(0)
})
test("should handle multiple acquires and releases correctly", async () => {
test("should handle multiple acquires and releases correctly", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 3 }
manager = new ConcurrencyManager(config)
@@ -317,11 +317,11 @@ describe("ConcurrencyManager.acquire/release", () => {
manager.release("model-a")
manager.release("model-a")
// Should be able to acquire again
await manager.acquire("model-a")
// Should be able to acquire again
await manager.acquire("model-a")
// #then
expect(true).toBe(true)
// #then - count should be 1 after re-acquiring
expect(manager.getCount("model-a")).toBe(1)
})
test("should use model-specific limit for acquire", async () => {

View File

@@ -170,6 +170,7 @@ function createBackgroundManager(): BackgroundManager {
const client = {
session: {
prompt: async () => ({}),
abort: async () => ({}),
},
}
return new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
@@ -776,7 +777,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 +785,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 +828,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 +840,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 +857,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 +865,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)
})
})
@@ -1053,6 +1054,7 @@ describe("BackgroundManager.resume model persistence", () => {
promptCalls.push(args)
return {}
},
abort: async () => ({}),
},
}
manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
@@ -1926,3 +1928,254 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => {
})
})
describe("BackgroundManager.shutdown session abort", () => {
test("should call session.abort for all running tasks during shutdown", () => {
// #given
const abortedSessionIDs: string[] = []
const client = {
session: {
prompt: async () => ({}),
abort: async (args: { path: { id: string } }) => {
abortedSessionIDs.push(args.path.id)
return {}
},
},
}
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
const task1: BackgroundTask = {
id: "task-1",
sessionID: "session-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Running task 1",
prompt: "Test",
agent: "test-agent",
status: "running",
startedAt: new Date(),
}
const task2: BackgroundTask = {
id: "task-2",
sessionID: "session-2",
parentSessionID: "parent-2",
parentMessageID: "msg-2",
description: "Running task 2",
prompt: "Test",
agent: "test-agent",
status: "running",
startedAt: new Date(),
}
getTaskMap(manager).set(task1.id, task1)
getTaskMap(manager).set(task2.id, task2)
// #when
manager.shutdown()
// #then
expect(abortedSessionIDs).toContain("session-1")
expect(abortedSessionIDs).toContain("session-2")
expect(abortedSessionIDs).toHaveLength(2)
})
test("should not call session.abort for completed or cancelled tasks", () => {
// #given
const abortedSessionIDs: string[] = []
const client = {
session: {
prompt: async () => ({}),
abort: async (args: { path: { id: string } }) => {
abortedSessionIDs.push(args.path.id)
return {}
},
},
}
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
const completedTask: BackgroundTask = {
id: "task-completed",
sessionID: "session-completed",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Completed task",
prompt: "Test",
agent: "test-agent",
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
}
const cancelledTask: BackgroundTask = {
id: "task-cancelled",
sessionID: "session-cancelled",
parentSessionID: "parent-2",
parentMessageID: "msg-2",
description: "Cancelled task",
prompt: "Test",
agent: "test-agent",
status: "cancelled",
startedAt: new Date(),
completedAt: new Date(),
}
const pendingTask: BackgroundTask = {
id: "task-pending",
parentSessionID: "parent-3",
parentMessageID: "msg-3",
description: "Pending task",
prompt: "Test",
agent: "test-agent",
status: "pending",
queuedAt: new Date(),
}
getTaskMap(manager).set(completedTask.id, completedTask)
getTaskMap(manager).set(cancelledTask.id, cancelledTask)
getTaskMap(manager).set(pendingTask.id, pendingTask)
// #when
manager.shutdown()
// #then
expect(abortedSessionIDs).toHaveLength(0)
})
test("should call onShutdown callback during shutdown", () => {
// #given
let shutdownCalled = false
const client = {
session: {
prompt: async () => ({}),
abort: async () => ({}),
},
}
const manager = new BackgroundManager(
{ client, directory: tmpdir() } as unknown as PluginInput,
undefined,
{
onShutdown: () => {
shutdownCalled = true
},
}
)
// #when
manager.shutdown()
// #then
expect(shutdownCalled).toBe(true)
})
test("should not throw when onShutdown callback throws", () => {
// #given
const client = {
session: {
prompt: async () => ({}),
abort: async () => ({}),
},
}
const manager = new BackgroundManager(
{ client, directory: tmpdir() } as unknown as PluginInput,
undefined,
{
onShutdown: () => {
throw new Error("cleanup failed")
},
}
)
// #when / #then
expect(() => manager.shutdown()).not.toThrow()
})
})
describe("BackgroundManager.completionTimers - Memory Leak Fix", () => {
function getCompletionTimers(manager: BackgroundManager): Map<string, ReturnType<typeof setTimeout>> {
return (manager as unknown as { completionTimers: Map<string, ReturnType<typeof setTimeout>> }).completionTimers
}
function setCompletionTimer(manager: BackgroundManager, taskId: string): void {
const completionTimers = getCompletionTimers(manager)
const timer = setTimeout(() => {
completionTimers.delete(taskId)
}, 5 * 60 * 1000)
completionTimers.set(taskId, timer)
}
test("should have completionTimers Map initialized", () => {
// #given
const manager = createBackgroundManager()
// #when
const completionTimers = getCompletionTimers(manager)
// #then
expect(completionTimers).toBeDefined()
expect(completionTimers).toBeInstanceOf(Map)
expect(completionTimers.size).toBe(0)
manager.shutdown()
})
test("should clear all completion timers on shutdown", () => {
// #given
const manager = createBackgroundManager()
setCompletionTimer(manager, "task-1")
setCompletionTimer(manager, "task-2")
const completionTimers = getCompletionTimers(manager)
expect(completionTimers.size).toBe(2)
// #when
manager.shutdown()
// #then
expect(completionTimers.size).toBe(0)
})
test("should cancel timer when task is deleted via session.deleted", () => {
// #given
const manager = createBackgroundManager()
const task: BackgroundTask = {
id: "task-timer-4",
sessionID: "session-timer-4",
parentSessionID: "parent-session",
parentMessageID: "msg-1",
description: "Test task",
prompt: "test",
agent: "explore",
status: "completed",
startedAt: new Date(),
}
getTaskMap(manager).set(task.id, task)
setCompletionTimer(manager, task.id)
const completionTimers = getCompletionTimers(manager)
expect(completionTimers.size).toBe(1)
// #when
manager.handleEvent({
type: "session.deleted",
properties: {
info: { id: "session-timer-4" },
},
})
// #then
expect(completionTimers.has(task.id)).toBe(false)
manager.shutdown()
})
test("should not leak timers across multiple shutdown calls", () => {
// #given
const manager = createBackgroundManager()
setCompletionTimer(manager, "task-1")
// #when
manager.shutdown()
manager.shutdown()
// #then
const completionTimers = getCompletionTimers(manager)
expect(completionTimers.size).toBe(0)
})
})

View File

@@ -5,9 +5,10 @@ import type {
LaunchInput,
ResumeInput,
} from "./types"
import { log, getAgentToolRestrictions } from "../../shared"
import { log, getAgentToolRestrictions, promptWithModelSuggestionRetry } 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,23 @@ export class BackgroundManager {
private concurrencyManager: ConcurrencyManager
private shutdownTriggered = false
private config?: BackgroundTaskConfig
private tmuxEnabled: boolean
private onSubagentSessionCreated?: OnSubagentSessionCreated
private onShutdown?: () => void
private queuesByKey: Map<string, QueueItem[]> = new Map()
private processingKeys: Set<string> = new Set()
private completionTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
constructor(ctx: PluginInput, config?: BackgroundTaskConfig) {
constructor(
ctx: PluginInput,
config?: BackgroundTaskConfig,
options?: {
tmuxConfig?: TmuxConfig
onSubagentSessionCreated?: OnSubagentSessionCreated
onShutdown?: () => void
}
) {
this.tasks = new Map()
this.notifications = new Map()
this.pendingByParent = new Map()
@@ -81,6 +101,9 @@ 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.onShutdown = options?.onShutdown
this.registerProcessCleanup()
}
@@ -205,7 +228,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 +248,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 +301,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)
this.client.session.prompt({
// 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
promptWithModelSuggestionRetry(this.client, {
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 +557,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 }],
},
@@ -643,7 +709,11 @@ export class BackgroundManager {
this.concurrencyManager.release(task.concurrencyKey)
task.concurrencyKey = undefined
}
// Clean up pendingByParent to prevent stale entries
const existingTimer = this.completionTimers.get(task.id)
if (existingTimer) {
clearTimeout(existingTimer)
this.completionTimers.delete(task.id)
}
this.cleanupPendingByParent(task)
this.tasks.delete(task.id)
this.clearNotificationsForTask(task.id)
@@ -1008,14 +1078,15 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}
const taskId = task.id
setTimeout(() => {
// Guard: Only delete if task still exists (could have been deleted by session.deleted event)
const timer = setTimeout(() => {
this.completionTimers.delete(taskId)
if (this.tasks.has(taskId)) {
this.clearNotificationsForTask(taskId)
this.tasks.delete(taskId)
log("[background-agent] Removed completed task from memory:", taskId)
}
}, 5 * 60 * 1000)
this.completionTimers.set(taskId, timer)
}
private formatDuration(start: Date, end?: Date): string {
@@ -1284,7 +1355,25 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
log("[background-agent] Shutting down BackgroundManager")
this.stopPolling()
// Release concurrency for all running tasks first
// Abort all running sessions to prevent zombie processes (#1240)
for (const task of this.tasks.values()) {
if (task.status === "running" && task.sessionID) {
this.client.session.abort({
path: { id: task.sessionID },
}).catch(() => {})
}
}
// Notify shutdown listeners (e.g., tmux cleanup)
if (this.onShutdown) {
try {
this.onShutdown()
} catch (error) {
log("[background-agent] Error in onShutdown callback:", error)
}
}
// Release concurrency for all running tasks
for (const task of this.tasks.values()) {
if (task.concurrencyKey) {
this.concurrencyManager.release(task.concurrencyKey)
@@ -1292,7 +1381,11 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}
}
// Then clear all state (cancels any remaining waiters)
for (const timer of this.completionTimers.values()) {
clearTimeout(timer)
}
this.completionTimers.clear()
this.concurrencyManager.clear()
this.tasks.clear()
this.notifications.clear()
@@ -1313,7 +1406,10 @@ function registerProcessSignal(
const listener = () => {
handler()
if (exitAfter) {
process.exit(0)
// Set exitCode and schedule exit after delay to allow other handlers to complete async cleanup
// Use 6s delay to accommodate LSP cleanup (5s timeout + 1s SIGKILL wait)
process.exitCode = 0
setTimeout(() => process.exit(), 6000)
}
}
process.on(signal, listener)

View File

@@ -2,6 +2,7 @@ import type { CommandDefinition } from "../claude-code-command-loader"
import type { BuiltinCommandName, BuiltinCommands } from "./types"
import { INIT_DEEP_TEMPLATE } from "./templates/init-deep"
import { RALPH_LOOP_TEMPLATE, CANCEL_RALPH_TEMPLATE } from "./templates/ralph-loop"
import { STOP_CONTINUATION_TEMPLATE } from "./templates/stop-continuation"
import { REFACTOR_TEMPLATE } from "./templates/refactor"
import { START_WORK_TEMPLATE } from "./templates/start-work"
@@ -55,7 +56,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>
@@ -70,6 +71,12 @@ $ARGUMENTS
</user-request>`,
argumentHint: "[plan-name]",
},
"stop-continuation": {
description: "(builtin) Stop all continuation mechanisms (ralph loop, todo continuation, boulder) for this session",
template: `<command-instruction>
${STOP_CONTINUATION_TEMPLATE}
</command-instruction>`,
},
}
export function loadBuiltinCommands(
@@ -81,7 +88,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

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

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