Compare commits

..

212 Commits

Author SHA1 Message Date
YeonGyu-Kim
77424f86c8 Merge pull request #2816 from code-yeongyu/fix/keep-agent-with-explicit-model
fix: always keep agent with explicit model, robust port binding & writable dir fallback
2026-03-25 11:48:26 +09:00
YeonGyu-Kim
919f7e4092 fix(data-path): writable directory fallback for data/cache paths
getDataDir() and getCacheDir() now verify the directory is writable and
fall back to os.tmpdir() if not.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 11:46:07 +09:00
YeonGyu-Kim
78a3e985be fix(mcp-oauth): robust port binding for callback server
Use port 0 fallback when findAvailablePort fails, read the actual bound
port from server.port. Tests refactored to use mock server when real
socket binding is unavailable in CI.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 11:46:07 +09:00
YeonGyu-Kim
42fb2548d6 fix(agent): always keep agent when model is explicitly configured
Previously, when an explicit model was configured, the agent name was
omitted to prevent opencode's built-in agent fallback chain from
overriding the user-specified model. This removes that conditional logic
and always passes the agent name alongside the model. Tests are updated
to reflect this behavior change.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 11:46:07 +09:00
YeonGyu-Kim
bff74f4237 Merge pull request #2695 from MoerAI/fix/provider-agnostic-fallback
fix(runtime-fallback): make fallback provider selection provider-agnostic (fixes #2303)
2026-03-25 11:36:50 +09:00
YeonGyu-Kim
038b8a79ec Revert "Merge pull request #2611 from MoerAI/fix/keep-default-builder-agent"
This reverts commit 0aa8bfe839, reversing
changes made to 422eaa9ae0.
2026-03-25 11:13:05 +09:00
YeonGyu-Kim
0aa8bfe839 Merge pull request #2611 from MoerAI/fix/keep-default-builder-agent
fix(config): keep default OpenCode Build agent enabled by default (fixes #2545)
2026-03-25 11:11:34 +09:00
YeonGyu-Kim
422eaa9ae0 Merge pull request #2753 from MoerAI/fix/prometheus-model-override
fix(prometheus): respect agent model override instead of using global opencode.json model (fixes #2693)
2026-03-25 11:09:48 +09:00
YeonGyu-Kim
63ebedc9a2 Merge pull request #2606 from RaviTharuma/fix/clamp-variant-on-non-opus-fallback
fix: clamp unsupported max variant for non-Opus Claude models
2026-03-25 11:06:31 +09:00
YeonGyu-Kim
f0b5835459 fix(publish): correct repo guard to oh-my-openagent (GitHub renamed repo) 2026-03-25 09:21:38 +09:00
YeonGyu-Kim
2a495c2e8d Merge pull request #2813 from code-yeongyu/fix/tmux-test-flake-20260325
test(tmux): remove flaky live env wrapper assertion
2026-03-25 02:08:05 +09:00
YeonGyu-Kim
0edb87b1c1 test(tmux): remove flaky live env wrapper assertion
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-25 02:05:51 +09:00
YeonGyu-Kim
cca057dc0f Merge pull request #2812 from code-yeongyu/fix/non-interactive-env-win-bash-prefix
fix(non-interactive-env): force unix prefix for bash git commands
2026-03-25 01:24:18 +09:00
YeonGyu-Kim
e000a3bb0d fix(non-interactive-env): force unix prefix for bash git commands
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-25 01:23:02 +09:00
YeonGyu-Kim
c19fc4ba22 Merge pull request #2811 from code-yeongyu/fix/publish-workflow-guard-topology-20260325
fix(publish): align repo guard and test topology
2026-03-25 01:19:29 +09:00
YeonGyu-Kim
e0de06851d fix(publish): align repo guard and test topology
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-25 01:17:42 +09:00
YeonGyu-Kim
26ac413dd9 Merge pull request #2801 from MoerAI/fix/null-byte-sanitization
fix(tool-execute-before): strip null bytes from bash commands to prevent crash (fixes #2220)
2026-03-25 01:12:45 +09:00
YeonGyu-Kim
81c912cf04 Merge pull request #2800 from MoerAI/fix/background-task-fallback-chain
fix(background-task): register fallback chain for background sessions (fixes #2203)
2026-03-25 01:12:41 +09:00
YeonGyu-Kim
9c348db450 Merge pull request #2799 from MoerAI/fix/unstable-agent-config-override
fix(category-resolver): respect is_unstable_agent config override (fixes #2061)
2026-03-25 01:12:36 +09:00
YeonGyu-Kim
2993b3255d Merge pull request #2796 from guazi04/fix/circuit-breaker-false-positive-upstream
fix(circuit-breaker): treat unknown tool input as non-comparable to prevent false positives on flat events
2026-03-25 01:12:31 +09:00
YeonGyu-Kim
0b77e2def0 Merge pull request #2810 from code-yeongyu/fix/webfetch-redirect-loop
fix(webfetch): guard redirect loops in built-in flow
2026-03-25 00:40:54 +09:00
YeonGyu-Kim
bfa8fa2378 Merge pull request #2804 from code-yeongyu/fix/b2-hashline-formatter-cache-per-project
fix(hashline-edit): scope formatter cache by directory
2026-03-25 00:32:41 +09:00
YeonGyu-Kim
6ee680af99 Merge pull request #2809 from code-yeongyu/fix/2330-recursive-subagent-spawn
fix(task): preserve restricted agent tools in sync continuation
2026-03-25 00:32:14 +09:00
YeonGyu-Kim
d327334ded Merge pull request #2808 from code-yeongyu/fix-gemini-3-pro-cleanup
fix(models): remove stale Gemini 3 Pro references
2026-03-25 00:32:10 +09:00
YeonGyu-Kim
07d120a78d Merge pull request #2807 from code-yeongyu/fix/b4-manager-model-override-1774351606
fix(background-task): apply model override omission to manager live path
2026-03-25 00:31:49 +09:00
YeonGyu-Kim
8b7b1c843a Merge pull request #2806 from code-yeongyu/fix/b5-permission-merge-order
fix(plugin): restore permission merge order precedence
2026-03-25 00:31:43 +09:00
YeonGyu-Kim
a1786f469d Merge pull request #2805 from code-yeongyu/fix/b3-config-filename-precedence
fix(config): prefer canonical plugin config filenames
2026-03-25 00:31:18 +09:00
YeonGyu-Kim
da77d8addf Merge pull request #2802 from code-yeongyu/fix/b1-preemptive-compaction-epoch-guard
fix: handle repeated compaction epochs in continuation guard
2026-03-25 00:30:54 +09:00
YeonGyu-Kim
971912e065 fix(webfetch): avoid rewriting successful redirect content 2026-03-24 23:59:57 +09:00
YeonGyu-Kim
af301ab29a fix(webfetch): guard redirect loops in built-in flow 2026-03-24 23:58:53 +09:00
YeonGyu-Kim
984464470c fix(task): preserve restricted agent tools in sync continuation
Restore sync continuation to apply agent tool restrictions after permissive defaults so resumed explore and librarian sessions cannot regain nested delegation. Add regression tests for resumed restricted agents while keeping plan-family continuation behavior intact.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 23:54:29 +09:00
YeonGyu-Kim
535ecee318 fix(models): remove stale Gemini 3 Pro references
Keep repo-owned CLI, docs, and test fixtures aligned with current Gemini 3.1 naming while leaving upstream catalog behavior untouched.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 23:53:56 +09:00
YeonGyu-Kim
32035d153e fix(config): prefer canonical plugin config filenames
Ensure oh-my-opencode filenames always win over legacy oh-my-openagent files so readers match canonical writer behavior.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:38:54 +09:00
YeonGyu-Kim
a0649616bf fix(todo-continuation-enforcer): acknowledge compaction epochs during idle
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:36:22 +09:00
YeonGyu-Kim
cb12b286c8 fix(todo-continuation-enforcer): arm compaction epochs on compaction
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:36:22 +09:00
YeonGyu-Kim
8e239e134c fix(todo-continuation-enforcer): make compaction guard epoch-aware
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:36:22 +09:00
YeonGyu-Kim
733676f1a9 fix(todo-continuation-enforcer): add compaction epoch state
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:36:22 +09:00
YeonGyu-Kim
d2e566ba9d fix(preemptive-compaction): mock session history in degradation test
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:36:22 +09:00
YeonGyu-Kim
6da4d2dae0 fix(hashline-edit): scope formatter cache by directory
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:30:16 +09:00
YeonGyu-Kim
3b41191980 fix(background-agent): honor explicit model override in manager
Keep BackgroundManager launch and resume from sending both agent and model so OpenCode does not override configured subagent models. Add launch and resume regressions for the live production path.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:28:01 +09:00
YeonGyu-Kim
0b614b751c fix(permissions): preserve explicit deny over OmO defaults
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 20:24:14 +09:00
MoerAI
c56a01c15d fix(tool-execute-before): strip null bytes from bash commands to prevent crash (fixes #2220) 2026-03-24 19:17:05 +09:00
MoerAI
d2d48fc9ff fix(background-task): register fallback chain for background sessions (fixes #2203) 2026-03-24 19:11:13 +09:00
MoerAI
41a43c62fc fix(category-resolver): respect is_unstable_agent config override (fixes #2061) 2026-03-24 19:08:21 +09:00
YeonGyu-Kim
cea8769a7f Merge pull request #2798 from code-yeongyu/fix/2353-model-selection-v2
fix(plugin): persist selected model only for main session
2026-03-24 18:57:50 +09:00
YeonGyu-Kim
7fa2417c42 fix(plugin): persist selected model only for main session
Reuse the stored model only for subsequent main-session messages when the UI provides no model, while preserving first-message behavior, explicit overrides, and subagent isolation.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 18:11:27 +09:00
YeonGyu-Kim
4bba924dad Revert "Merge pull request #2797 from code-yeongyu/fix/2353-model-selection-persistence"
This reverts commit e691303919, reversing
changes made to d4aee20743.
2026-03-24 17:59:21 +09:00
YeonGyu-Kim
e691303919 Merge pull request #2797 from code-yeongyu/fix/2353-model-selection-persistence
fix(plugin): preserve selected model across messages
2026-03-24 17:54:34 +09:00
YeonGyu-Kim
d4aee20743 Merge pull request #2794 from code-yeongyu/fix/2775-thinking-block-signatures
fix(thinking-block-validator): reuse signed thinking blocks instead of synthetic placeholders
2026-03-24 17:54:31 +09:00
YeonGyu-Kim
bad70f5e24 fix(plugin): preserve selected model across messages
Reuse the current session's selected model during config-time agent rebuilds when config.model is missing, so desktop sessions do not snap back to the default model after each send.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 17:47:08 +09:00
Mou
b9fa2a3ebc fix(background-agent): prevent circuit breaker false positives on flat-format events 2026-03-24 16:35:54 +08:00
YeonGyu-Kim
0e7bd595f8 fix(session-recovery): reuse signed thinking blocks safely
Reuse signed Anthropic thinking blocks only when they can still sort before the target message's parts, otherwise skip recovery instead of reintroducing invalid loops.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 17:22:07 +09:00
YeonGyu-Kim
0732cb85f9 fix(thinking-block-validator): reuse signed thinking parts
Preserve prior signed Anthropic thinking blocks instead of creating unsigned synthetic placeholders, and skip injection when no signed block exists.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-24 17:22:07 +09:00
YeonGyu-Kim
500784a9b9 Merge pull request #2790 from code-yeongyu/fix/2666-mcp-schema-sanitization
fix(schema): strip contentEncoding from MCP tool schemas for Gemini (fixes #2200)
2026-03-24 16:24:57 +09:00
YeonGyu-Kim
5e856b4fde fix(schema): strip contentEncoding from MCP tool schemas for Gemini compatibility
The existing normalizeToolArgSchemas only applies to omo plugin tools
(via tool-registry.ts), but MCP server tool schemas bypass this
sanitization entirely. MCP schemas with contentEncoding/contentMediaType
cause Gemini 400 errors.

Add sanitizeJsonSchema() to strip unsupported keywords from MCP tool
inputSchema before serialization in formatMcpCapabilities.

Fixes #2200
Supersedes #2666
2026-03-24 16:24:44 +09:00
YeonGyu-Kim
03dc903e8e Merge pull request #2789 from code-yeongyu/fix/2671-clearSessionState
fix(anthropic-recovery): clear session state after successful summarize (fixes #2225)
2026-03-24 16:23:25 +09:00
YeonGyu-Kim
69d0b23ab6 fix(anthropic-recovery): clear session state after successful summarize and fix timing test
- Add missing clearSessionState() call after successful summarize (line 117)
  Without this, retry state persisted even after success, potentially causing
  unnecessary retries on subsequent compaction events.

- Fix timing-sensitive test: adjust attempt=0 and firstAttemptTime to give
  proper remainingTimeMs buffer for capped delay calculation.

Fixes #2225
Supersedes #2671
2026-03-24 16:23:11 +09:00
YeonGyu-Kim
ee8735cd2c Merge pull request #2788 from code-yeongyu/fix/2670-uiSelectedModel-nullification
fix(agents): preserve uiSelectedModel when agent override has no model (fixes #2351)
2026-03-24 16:22:15 +09:00
YeonGyu-Kim
d8fe61131c fix(agents): preserve uiSelectedModel when agent override has no model
Three agent builder files used falsy checks that incorrectly nullified
uiSelectedModel when override objects existed but had no model set:

- sisyphus-agent.ts: `?.model ?` → `?.model !== undefined ?`
- atlas-agent.ts: `?.model ?` → `?.model !== undefined ?`
- general-agents.ts: `!override?.model` → `override?.model === undefined`

This caused user model selection in web mode to revert to defaults.

Fixes #2351
2026-03-24 16:22:03 +09:00
YeonGyu-Kim
935995d270 Merge pull request #2668 from MoerAI/fix/session-degradation-detection
fix(session): detect post-compaction no-text degradation and trigger recovery (fixes #2232)
2026-03-24 16:21:30 +09:00
YeonGyu-Kim
23d8b88c4a Merge pull request #2669 from MoerAI/fix/atlas-worktree-verification
fix(atlas): use worktree path for git verification when available (fixes #2229)
2026-03-24 16:21:27 +09:00
YeonGyu-Kim
b4285ce565 Merge pull request #2787 from code-yeongyu/fix/review-fixes
fix(permissions): ensure omo permission overrides take precedence over opencode defaults
2026-03-24 16:20:27 +09:00
YeonGyu-Kim
f9d354b63e fix(permissions): ensure omo permission overrides take precedence over opencode defaults
The spread order in applyToolConfig was incorrect - omo's external_directory: 'allow'
was placed BEFORE the config.permission spread, allowing opencode's default 'ask' to
overwrite it. This caused write/edit tools to hang on headless opencode serve sessions
(no TUI to approve permission prompts).

Move omo's permission overrides AFTER the base config spread so they always win.

Fixes write/edit tool hangs when running opencode serve headlessly.
2026-03-24 16:19:56 +09:00
YeonGyu-Kim
370eb945ee Merge pull request #2786 from code-yeongyu/docs/rename-opencode-to-openagent
docs: rename oh-my-opencode to oh-my-openagent
2026-03-24 15:39:00 +09:00
YeonGyu-Kim
6387065e6f docs: rename oh-my-opencode to oh-my-openagent 2026-03-24 15:31:54 +09:00
YeonGyu-Kim
bebdb97c21 Merge pull request #2784 from code-yeongyu/fix/remove-openclaw-hyperlink
docs: remove OpenClaw hyperlink
2026-03-24 13:35:12 +09:00
YeonGyu-Kim
b5e2ead4e1 docs: remove OpenClaw hyperlink from Building in Public 2026-03-24 13:34:57 +09:00
YeonGyu-Kim
91922dae36 Merge pull request #2783 from code-yeongyu/fix/building-in-public-image
docs: add screenshot to Building in Public section
2026-03-24 13:34:14 +09:00
YeonGyu-Kim
cb3d8af995 docs: add screenshot to Building in Public section
Added the actual Discord screenshot showing real-time development
with Jobdori in #building-in-public channel.
2026-03-24 13:34:04 +09:00
YeonGyu-Kim
0fb3e2063a Merge pull request #2782 from code-yeongyu/feat/building-in-public-readme
docs: add Building in Public section to all READMEs
2026-03-24 13:23:46 +09:00
YeonGyu-Kim
b37b877c45 docs: add Building in Public section to all READMEs
- Added TIP box linking to #building-in-public Discord channel
- Mentions Jobdori AI assistant (built on heavily customized OpenClaw)
- Added to all 5 language variants (EN, KO, JA, ZH-CN, RU)
- Positioned above waitlist section for visibility
2026-03-24 13:23:21 +09:00
YeonGyu-Kim
f854246d7f Merge pull request #2772 from MoerAI/fix/custom-model-resolution
fix(delegate-task): trust user-configured category models without fuzzy validation (fixes #2740)
2026-03-24 12:38:22 +09:00
YeonGyu-Kim
f1eaa7bf9b fix(shell): detect csh/tcsh and use setenv syntax (#2769)
fix(non-interactive-env): detect shell type for csh/tcsh env var syntax (fixes #2089)
2026-03-24 12:30:49 +09:00
YeonGyu-Kim
ed9b4a6329 Merge pull request #2780 from code-yeongyu/fix/issues-2741-2648-2779
fix: resolve subagent model override, empty plan completion, deep task refusal (#2741, #2648, #2779)
2026-03-24 10:28:24 +09:00
YeonGyu-Kim
a00a22ac4c fix: remove copy-paste artifacts in hephaestus gpt-5-3-codex prompt
Same issue as gpt.ts and gpt-5-4.ts: duplicated CORRECT block with pipe
characters and duplicated Hard Constraints/Task Scope Clarification sections.
2026-03-24 10:14:53 +09:00
YeonGyu-Kim
8879581fc1 fix: remove copy-paste artifacts in hephaestus GPT prompts
- Remove leading pipe characters (|) from duplicated CORRECT block
- Remove duplicated ## Hard Constraints and ### Task Scope Clarification sections
- Properly place Task Scope Clarification section between CORRECT list and Hard Constraints

Addresses review comments by cubic-dev-ai[bot] on PR #2780
2026-03-24 09:57:30 +09:00
YeonGyu-Kim
230ce835e5 fix: resolve 3 bugs - subagent model override, empty plan completion, deep task refusal
- #2741: Pass inheritedModel as fallback in subagent-resolver when user hasn't
  configured an override, ensuring custom provider models take priority
- #2648: Fix getPlanProgress to treat plans with 0 checkboxes as incomplete
  instead of complete (total > 0 && completed === total)
- #2779: Relax Hephaestus single-task guard to accept multi-step sub-tasks
  from Atlas delegation, only rejecting genuinely independent tasks

Fixes #2741, fixes #2648, fixes #2779
2026-03-24 09:45:11 +09:00
YeonGyu-Kim
10e56badb3 Merge pull request #2776 from code-yeongyu/fix/background-agent-timeout-defaults
fix: stabilize background-agent stale timeout tests (Date.now race condition)
2026-03-24 03:29:35 +09:00
YeonGyu-Kim
cddf78434c Merge pull request #2770 from code-yeongyu/fix/ci-test-timeout
fix: add fetch mock to install test to prevent CI timeout
2026-03-24 03:29:23 +09:00
YeonGyu-Kim
0078b736b9 fix: stabilize stale timeout tests with fixed Date.now()
Tests 'should use default timeout when config not provided' (manager.test.ts)
and 'should use DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS when not configured'
(task-poller.test.ts) failed in CI because Date.now() drifted between
test setup (when creating timestamps like Date.now() - 46*60*1000) and
actual execution inside checkAndInterruptStaleTasks().

On slower CI machines, this drift pushed borderline values across
the threshold, causing tasks that should be stale to remain 'running'.

Fix: Mock Date.now with spyOn to return a fixed time, ensuring
consistent timeout calculations regardless of execution speed.
2026-03-23 22:17:03 +09:00
MoerAI
6d7f69625b fix: update stale timeout test fixtures for new 45/60 min defaults 2026-03-23 21:00:59 +09:00
MoerAI
fda17dd161 fix(background-agent): increase default stale timeouts and improve cancellation messages (fixes #2684) 2026-03-23 20:49:43 +09:00
MoerAI
c41d6fd912 fix(delegate-task): trust user-configured category models without fuzzy validation (fixes #2740) 2026-03-23 20:39:47 +09:00
YeonGyu-Kim
6e9128e060 fix: add fetch mock to install test to prevent CI timeout
The first test case 'non-TUI mode: should show warning but continue when
OpenCode binary not found' was missing a globalThis.fetch mock, causing it
to make a real HTTP request to npm registry via fetchNpmDistTags().
The npm fetch timeout (5s) collided with the test timeout (5s), causing
flaky CI failures.

Added the same fetch mock pattern already used by the other two test cases.
Test runtime dropped from 5000ms+ to ~2ms.
2026-03-23 20:03:45 +09:00
MoerAI
92509d8cfb fix(non-interactive-env): detect shell type for csh/tcsh env var syntax (fixes #2089) 2026-03-23 19:33:54 +09:00
YeonGyu-Kim
331f7ec52b Merge pull request #2768 from code-yeongyu/fix/issue-2117
fix: emit formatter events from hashline-edit tool (fixes #2117)
2026-03-23 18:49:10 +09:00
YeonGyu-Kim
4ba2da7ebb fix: add tests and fix typing for formatter trigger (#2768) 2026-03-23 18:46:44 +09:00
YeonGyu-Kim
f95d3b1ef5 fix: emit formatter events from hashline-edit tool (fixes #2117) 2026-03-23 18:40:27 +09:00
YeonGyu-Kim
d5d7c7dd26 Merge pull request #2767 from code-yeongyu/fix/issue-2742
fix: respect disabled_tools config in agent prompts (fixes #2742)
2026-03-23 18:39:51 +09:00
YeonGyu-Kim
6a56c0e241 Merge pull request #2766 from code-yeongyu/fix/issue-390
fix: trigger compaction before continue after session error recovery (fixes #390)
2026-03-23 18:39:50 +09:00
YeonGyu-Kim
94c234c88c Merge pull request #2765 from code-yeongyu/fix/issue-2024
fix: skip keyword injection for non-OMO agents (fixes #2024)
2026-03-23 18:39:48 +09:00
YeonGyu-Kim
2ab976c511 Merge pull request #2764 from code-yeongyu/fix/issue-2624
fix: add oh-my-openagent.jsonc config file detection (fixes #2624)
2026-03-23 18:39:46 +09:00
YeonGyu-Kim
dc66088483 Merge pull request #2763 from code-yeongyu/fix/issue-2037
fix: respect OPENCODE_DISABLE_CLAUDE_CODE env vars (fixes #2037)
2026-03-23 18:39:45 +09:00
YeonGyu-Kim
67b5f46a7c Merge pull request #2762 from code-yeongyu/fix/issue-2150
fix: clarify Prometheus file permission error message (fixes #2150)
2026-03-23 18:39:43 +09:00
YeonGyu-Kim
0e483d27ac Merge pull request #2761 from code-yeongyu/fix/issue-2729
fix: validate serverUrl port before tmux pane spawn (fixes #2729)
2026-03-23 18:39:41 +09:00
YeonGyu-Kim
f5eaa648e9 fix: respect disabled_tools config in agent prompts (fixes #2742)
- Check disabled_tools for 'question' in tool-config-handler permission logic
- Strip Question tool code examples from Prometheus prompts when disabled
- Pass disabled_tools through prometheus agent config builder pipeline
- Add tests for disabled_tools question permission handling
2026-03-23 18:13:38 +09:00
YeonGyu-Kim
4c4760a4ee fix: trigger compaction before continue after session error recovery (fixes #390) 2026-03-23 18:12:51 +09:00
YeonGyu-Kim
7f20dd6ff5 fix: add oh-my-openagent.jsonc config file detection (fixes #2624) 2026-03-23 18:11:01 +09:00
YeonGyu-Kim
de371be236 fix: skip keyword injection for non-OMO agents (fixes #2024) 2026-03-23 18:10:44 +09:00
YeonGyu-Kim
f3c2138ef4 fix: respect OPENCODE_DISABLE_CLAUDE_CODE env vars (fixes #2037) 2026-03-23 18:10:08 +09:00
YeonGyu-Kim
0810e37240 fix: validate serverUrl port before tmux pane spawn (fixes #2729) 2026-03-23 18:09:31 +09:00
YeonGyu-Kim
a64e364fa6 fix: clarify Prometheus file permission error message (fixes #2150) 2026-03-23 18:07:59 +09:00
github-actions[bot]
d886ac701f @hunghoang3011 has signed the CLA in code-yeongyu/oh-my-openagent#2758 2026-03-23 04:28:31 +00:00
MoerAI
11f1d71c93 fix(prometheus): respect agent model override instead of using global opencode.json model (fixes #2693) 2026-03-23 10:36:59 +09:00
github-actions[bot]
30dc50d880 @0xYiliu has signed the CLA in code-yeongyu/oh-my-openagent#2738 2026-03-21 23:05:07 +00:00
github-actions[bot]
b17e633464 @ndaemy has signed the CLA in code-yeongyu/oh-my-openagent#2734 2026-03-21 10:18:31 +00:00
YeonGyu-Kim
eec268ee42 fix: use find() instead of calls[0] in wakeGateway test to handle background fetch calls 2026-03-21 18:01:39 +09:00
github-actions[bot]
363661c0d6 @whackur has signed the CLA in code-yeongyu/oh-my-openagent#2733 2026-03-21 05:27:27 +00:00
github-actions[bot]
261bbdf4dc @nguyentamdat has signed the CLA in code-yeongyu/oh-my-openagent#2718 2026-03-20 07:34:31 +00:00
YeonGyu-Kim
8aec4c5cb3 feat(hooks/todo-continuation-enforcer): enhance continuation message with skeptical verification guidance 2026-03-20 16:13:02 +09:00
YeonGyu-Kim
16cbc847ac fix(cli/run): set OPENCODE_CLIENT to 'run' to exclude question tool from registry 2026-03-20 16:12:58 +09:00
YeonGyu-Kim
436ce71dc8 docs(skills/github-triage): fix Phase 1 JSON parsing and large repo handling 2026-03-20 16:12:54 +09:00
MoerAI
0e610a72bc fix(runtime-fallback): make fallback provider selection provider-agnostic (fixes #2303) 2026-03-20 09:53:24 +09:00
github-actions[bot]
d2a49428b9 @tonymfer has signed the CLA in code-yeongyu/oh-my-openagent#2701 2026-03-19 17:14:04 +00:00
github-actions[bot]
c3b23bf603 @trafgals has signed the CLA in code-yeongyu/oh-my-openagent#2690 2026-03-19 04:22:43 +00:00
YeonGyu-Kim
50094de73e docs: fix remaining AGENTS hook composition text
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-19 12:02:52 +09:00
YeonGyu-Kim
3aa2748c04 docs: sync hook counts after continuation hook removal
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-19 12:02:52 +09:00
YeonGyu-Kim
ccaf759b6b fix(hooks): remove gpt permission continuation hook
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-19 12:02:52 +09:00
YeonGyu-Kim
521a1f76a9 fix(atlas): stop only after 10 consecutive prompt failures
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-19 12:02:52 +09:00
github-actions[bot]
490f0f2090 @walioo has signed the CLA in code-yeongyu/oh-my-openagent#2688 2026-03-19 02:35:04 +00:00
YeonGyu-Kim
caf595e727 fix(build-binaries): prevent test imports from triggering binary builds
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-19 10:47:33 +09:00
YeonGyu-Kim
1f64a45113 Merge pull request #2620 from code-yeongyu/feat/openclaw-bidirectional
feat: port OpenClaw bidirectional integration from omx
2026-03-19 10:47:07 +09:00
YeonGyu-Kim
9b2dc2189c fix(ralph-loop): detect promise tags in tool_result parts for ulw verification
Oracle's <promise>VERIFIED</promise> arrives as a tool_result part from the
task() tool call, not as a text part. Both detectCompletionInSessionMessages
and collectAssistantText only scanned type=text parts, missing the
verification signal entirely. This caused ulw loops to fail verification
even when Oracle successfully emitted VERIFIED.

Include tool_result parts in promise detection alongside text parts.
Exclude tool_use parts to avoid false positives from instructional text.
2026-03-18 19:09:59 +09:00
MoerAI
071fab1618 fix: match existing codebase session.messages() parameter shape 2026-03-18 19:08:05 +09:00
YeonGyu-Kim
f6c24e42af fix(ralph-loop): detect promise tags in tool_result parts for ulw verification
Oracle's <promise>VERIFIED</promise> arrives as a tool_result part from the
task() tool call, not as a text part. Both detectCompletionInSessionMessages
and collectAssistantText only scanned type=text parts, missing the
verification signal entirely. This caused ulw loops to fail verification
even when Oracle successfully emitted VERIFIED.

Include tool_result parts in promise detection alongside text parts.
Exclude tool_use parts to avoid false positives from instructional text.
2026-03-18 19:03:30 +09:00
YeonGyu-Kim
22fd976eb9 feat(categories): change quick category default model from claude-haiku-4-5 to gpt-5.4-mini
GPT-5.4-mini provides stronger reasoning at comparable speed and cost.
Haiku remains as the next fallback priority in the chain.

Changes:
- DEFAULT_CATEGORIES quick model: anthropic/claude-haiku-4-5 → openai/gpt-5.4-mini
- Fallback chain: gpt-5.4-mini → haiku → gemini-3-flash → minimax-m2.5 → gpt-5-nano
- OpenAI-only catalog: quick uses gpt-5.4-mini directly
- Think-mode: add gpt-5-4-mini and gpt-5-4-nano high variants
- Update all documentation references
2026-03-18 19:03:30 +09:00
YeonGyu-Kim
826284f3d9 Merge pull request #2676 from code-yeongyu/fix/atlas-task-session-review-followup
fix(atlas): address review findings for task session reuse
2026-03-18 18:50:45 +09:00
YeonGyu-Kim
3c7e6a3940 fix(atlas): address review findings for task session reuse 2026-03-18 18:44:42 +09:00
YeonGyu-Kim
33ef4db502 Merge pull request #2640 from HaD0Yun/had0yun/atlas-task-session-reuse
feat(atlas): persist preferred task session reuse
2026-03-18 18:37:16 +09:00
YeonGyu-Kim
458ec06b0e fix: extract question text from questions array per opencode tool schema 2026-03-18 18:27:09 +09:00
YeonGyu-Kim
6b66f69433 feat(gpt-permission-continuation): add context-aware continuation prompts
- Add buildContextualContinuationPrompt to include assistant message context
- Move extractPermissionPhrase to detector module for better separation
- Block continuation injection in subagent sessions
- Update handler to use contextual prompts with last response context
- Add tests for subagent session blocking and contextual prompts
- Update todo coordination test to verify new prompt format

🤖 Generated with assistance of OhMyOpenCode
2026-03-18 17:52:32 +09:00
YeonGyu-Kim
ce8957e1e1 fix(ralph-loop): harden oracle verification flow
Capture oracle verification sessions more reliably and accept parent-session VERIFIED evidence so ULW loops do not retry after successful review.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-18 17:45:59 +09:00
MoerAI
a3db64b931 fix: address cubic review — SDK compatibility and race condition fixes 2026-03-18 17:42:17 +09:00
HaD0Yun
8859da5fef fix(atlas): harden task session reuse 2026-03-18 17:31:27 +09:00
YeonGyu-Kim
23c0ff60f2 feat(background-agent): increase default max tool calls to 4000
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-18 16:36:55 +09:00
MoerAI
4723319eef fix(atlas): use worktree path for git verification when available (fixes #2229) 2026-03-18 16:23:37 +09:00
MoerAI
b8f3186d65 fix(session): detect post-compaction no-text degradation and trigger recovery (fixes #2232) 2026-03-18 16:13:23 +09:00
YeonGyu-Kim
01e18f8773 chore: remove console.* debug logging from non-CLI source files 2026-03-18 15:29:50 +09:00
YeonGyu-Kim
1669c83782 revert(todo-continuation): remove [TODO-DIAG] console.error debug logging 2026-03-18 15:10:51 +09:00
YeonGyu-Kim
09cfd0b408 diag(todo-continuation): add comprehensive debug logging for session idle handling
Add [TODO-DIAG] console.error statements throughout the todo continuation
enforcer to help diagnose why continuation prompts aren't being injected.

Changes:
- Add session.idle event handler diagnostic in handler.ts
- Add detailed blocking reason logging in idle-event.ts for all gate checks
- Update JSON schema to reflect circuit breaker config changes

🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2026-03-18 14:45:14 +09:00
YeonGyu-Kim
d48ea025f0 refactor(circuit-breaker): replace sliding window with consecutive call detection
Switch background task loop detection from percentage-based sliding window
(80% of 20-call window) to consecutive same-tool counting. Triggers when
same tool signature is called 20+ times in a row; a different tool resets
the counter.
2026-03-18 14:32:27 +09:00
YeonGyu-Kim
c5c7ba4eed perf: pre-compile regex patterns and optimize hot-path string operations
- error-classifier: pre-compile default retry pattern regex
- think-mode/detector: combine multilingual patterns into single regex
- parser: skip redundant toLowerCase on pre-lowered keywords
- edit-operations: use fast arraysEqual instead of JSON comparison
- hash-computation: optimize streaming line extraction with index tracking
2026-03-18 14:19:23 +09:00
YeonGyu-Kim
90aa3a306c perf(hooks,tools): optimize string operations and reduce redundant iterations
- output-renderer, hashline-edit-diff: replace str += with array join (H2)
- auto-slash-command: single-pass Map grouping instead of 6x filter (M1)
- comment-checker: hoist Zod schema to module scope (M2)
- session-last-agent: reverse iterate sorted array instead of sort+reverse (L2)
2026-03-18 14:19:12 +09:00
YeonGyu-Kim
c2f7d059d2 perf(shared): optimize hot-path utilities across plugin
- task-list: replace O(n³) blocker resolution with Map lookup (C4)
- logger: buffer log entries and flush periodically to reduce sync I/O (C5)
- plugin-interface: create chatParamsHandler once at init (H3)
- pattern-matcher: cache compiled RegExp for wildcard matchers (H6)
- file-reference-resolver: use replaceAll instead of split/join (M9)
- connected-providers-cache: add in-memory cache for read operations (L4)
2026-03-18 14:19:00 +09:00
YeonGyu-Kim
7a96a167e6 perf(claude-code-hooks): defer config loading until after disabled check
Move loadClaudeHooksConfig and loadPluginExtendedConfig after isHookDisabled check
in both tool-execute-before and tool-execute-after handlers to skip 5 file reads
per tool call when hooks are disabled (C1)
2026-03-18 14:18:49 +09:00
YeonGyu-Kim
2da19fe608 perf(background-agent): use Set for countedToolPartIDs, cache circuit breaker settings, optimize loop detector
- Replace countedToolPartIDs string[] with Set<string> for O(1) has/add vs O(n) includes/spread (C2)
- Cache resolveCircuitBreakerSettings at manager level to avoid repeated object creation (C3)
- Optimize recordToolCall to avoid full array copy with slice (L1)
2026-03-18 14:18:38 +09:00
YeonGyu-Kim
952bd5338d fix(background-agent): treat non-active session statuses as terminal to prevent parent session hang
Previously, pollRunningTasks() and checkAndInterruptStaleTasks() treated
any non-"idle" session status as "still running", which caused tasks with
terminal statuses like "interrupted" to be skipped indefinitely — both
for completion detection AND stale timeout. This made the parent session
hang forever waiting for an ALL COMPLETE notification that never came.

Extract isActiveSessionStatus() and isTerminalSessionStatus() that
classify session statuses explicitly. Only known active statuses
("busy", "retry", "running") protect tasks from completion/stale checks.
Known terminal statuses ("interrupted") trigger immediate completion.
Unknown statuses fall through to the standard idle/gone path with output
validation as a conservative default.

Introduced by: a0c93816 (2026-02-14), dc370f7f (2026-03-08)
2026-03-18 14:06:23 +09:00
YeonGyu-Kim
57757a345d refactor: improve test isolation and DI for cache/port-utils/resolve-file-uri
- connected-providers-cache: extract factory pattern (createConnectedProvidersCacheStore) for testable cache dir injection
- port-utils.test: environment-independent tests with real socket probing and contiguous port detection
- resolve-file-uri.test: mock homedir instead of touching real home directory
- github-triage: update SKILL.md
2026-03-18 13:17:01 +09:00
YeonGyu-Kim
3caae14192 fix(ralph-loop): abort stale Oracle sessions before ulw verification restart
When Oracle verification fails in ulw-loop mode, the previous Oracle
session was never aborted before restarting. Each retry created a new
descendant session, causing unbounded session accumulation and 500
errors from server overload.

Now abort the old verification session before:
- restarting the loop after failed verification
- re-entering verification phase on subsequent DONE detection
2026-03-18 12:49:27 +09:00
YeonGyu-Kim
55ac653eaa feat(hooks): add todo-description-override hook to enforce atomic todo format
Override TodoWrite description via tool.definition hook to require
WHERE/WHY/HOW/RESULT in each todo title and enforce 1-3 tool call
granularity.
2026-03-18 11:49:13 +09:00
YeonGyu-Kim
1d5652dfa9 Merge pull request #2655 from tad-hq/infinite-circuit-target-fix
fix(circuit-breaker): make repetitive detection target-aware and add enabled escape hatch
2026-03-18 11:46:06 +09:00
YeonGyu-Kim
76c460536d docs(start-work): update worktree and task breakdown guidance
- Change worktree behavior: default to current directory, worktree only with --worktree flag
- Add mandatory TASK BREAKDOWN section with granular sub-task requirements
- Add WORKTREE COMPLETION section for merging worktree branches back

🤖 Generated with assistance of OhMyOpenCode
2026-03-18 11:16:43 +09:00
github-actions[bot]
b067d4a284 @ogormans-deptstack has signed the CLA in code-yeongyu/oh-my-openagent#2656 2026-03-17 20:42:53 +00:00
github-actions[bot]
94838ec039 @tad-hq has signed the CLA in code-yeongyu/oh-my-openagent#2655 2026-03-17 20:07:20 +00:00
tad-hq
224ecea8c7 chore: regenerate JSON schema with circuitBreaker.enabled field 2026-03-17 13:43:56 -06:00
tad-hq
5d5755f29d fix(circuit-breaker): wire target-aware detection into background manager 2026-03-17 13:40:46 -06:00
tad-hq
1fdce01fd2 fix(circuit-breaker): target-aware loop detection via tool signatures 2026-03-17 13:36:09 -06:00
tad-hq
c8213c970e fix(circuit-breaker): add enabled config flag as escape hatch 2026-03-17 13:29:06 -06:00
YeonGyu-Kim
576ff453e5 Merge pull request #2651 from code-yeongyu/fix/openagent-version-in-publish
fix(release): set version when publishing oh-my-openagent
2026-03-18 02:15:36 +09:00
YeonGyu-Kim
9b8aca45f9 fix(release): set version when publishing oh-my-openagent
The publish step was updating name and optionalDependencies but not
version, causing npm to try publishing the base package.json version
(3.11.0) instead of the release version (3.12.0).

Error was: 'You cannot publish over the previously published versions: 3.11.0'
2026-03-18 02:15:15 +09:00
YeonGyu-Kim
f1f20f5a79 Merge pull request #2650 from code-yeongyu/fix/openagent-platform-publish
fix(release): add oh-my-openagent dual-publish to platform and main workflows
2026-03-18 01:55:31 +09:00
YeonGyu-Kim
de40caf76d fix(release): add oh-my-openagent dual-publish to platform and main workflows
- publish-platform.yml: Build job now checks BOTH oh-my-opencode and
  oh-my-openagent before skipping. Build only skips when both are published.
  Added 'Publish oh-my-openagent-{platform}' step that renames package.json
  and publishes under the openagent name.

- publish.yml: Added 'Publish oh-my-openagent' step after opencode publish.
  Rewrites package name and optionalDependencies to oh-my-openagent variants,
  then publishes. Restores package.json after.

Previously, oh-my-openagent platform packages were never published because
the build skip check only looked at oh-my-opencode (which was already published),
causing the entire build to be skipped.
2026-03-18 01:45:02 +09:00
Ravi Tharuma
71b1f7e807 fix(anthropic-effort): clamp variant against mutable request message 2026-03-17 11:57:56 +01:00
HaD0Yun
8adf6a2c47 fix(atlas): tighten session reuse metadata parsing 2026-03-17 18:14:17 +09:00
github-actions[bot]
d80833896c @HaD0Yun has signed the CLA in code-yeongyu/oh-my-openagent#2640 2026-03-17 08:27:56 +00:00
HaD0Yun
5c6194372e feat(atlas): persist preferred task session reuse 2026-03-17 17:25:46 +09:00
YeonGyu-Kim
399796cbe4 fix(openclaw): add comment clarifying proc.exited race condition avoidance
cubic identified potential race condition where Bun's proc.exitCode
may be null immediately after stdout closes. Added clarifying
comment that await proc.exited ensures exitCode is set before
checking.

fixes: cubic review on PR #2620
2026-03-17 17:14:52 +09:00
YeonGyu-Kim
77c3ed1a1f chore: remove omx state files and add .omx/ to gitignore 2026-03-17 17:00:29 +09:00
YeonGyu-Kim
82e25c845b fix: address cubic re-review — remove non-existent session.stop event, fix env var fallback test 2026-03-17 17:00:18 +09:00
YeonGyu-Kim
d50c38f037 refactor(tests): rename benchmarks/ to tests/hashline/, remove FriendliAI dependency
- Move benchmarks/ → tests/hashline/
- Replace @friendliai/ai-provider with @ai-sdk/openai-compatible
- Remove all 'benchmark' naming (package name, scripts, env vars, session IDs)
- Fix import paths for new directory depth (../src → ../../src)
- Fix pre-existing syntax error in headless.ts (unclosed case block)
- Inject HASHLINE_EDIT_DESCRIPTION into test system prompt
- Scripts renamed: bench:* → test:*
2026-03-17 16:47:13 +09:00
YeonGyu-Kim
f2d5f4ca92 improve(hashline-edit): rewrite tool description with examples and fix lines schema
- Add XML-structured description (<must>, <operations>, <examples>, <auto>)
- Add 5 concrete examples including BAD pattern showing duplication
- Add explicit anti-duplication warning for range replace
- Move snapshot rule to top-level <must> section
- Clarify batch semantics (multiple ops, not one big replace)
- Fix lines schema: add string[] to union (was string|null, now string[]|string|null)
- Matches runtime RawHashlineEdit type and description text
2026-03-17 16:47:13 +09:00
YeonGyu-Kim
b788586caf relax task timeouts: stale timeout 3min→20min, session wait 30s→1min 2026-03-17 16:47:13 +09:00
YeonGyu-Kim
90351e442e update look_at tool description to discourage visual precision use cases 2026-03-17 16:47:13 +09:00
YeonGyu-Kim
4ad88b2576 feat(task-toast): show model name before category in toast notification
Display resolved model ID (e.g., gpt-5.3-codex: deep) instead of
agent/category format when modelInfo is available. Falls back to
old format when no model info exists.
2026-03-17 16:47:13 +09:00
YeonGyu-Kim
2ce69710e3 docs: sync agent-model-matching guide with actual fallback chains
- Metis: add missing GPT-5.4 high as 2nd fallback
- Hephaestus: add GPT-5.4 (Copilot) fallback, was incorrectly listed as Codex-only
- Oracle: add opencode-go/glm-5 as last fallback
- Momus: add opencode-go/glm-5 fallback, note xhigh variant
- Atlas: add GPT-5.4 medium as 3rd fallback
- Sisyphus: add Kimi K2.5 (moonshot providers) in chain
- Sisyphus-Junior: add missing agent to Utility Runners section
- GPT Family table: merge duplicate GPT-5.4 rows
- Categories: add missing opencode-go intermediate fallbacks for
  visual-engineering, ultrabrain, quick, unspecified-low/high, writing
2026-03-17 16:47:13 +09:00
YeonGyu-Kim
0b4d092cf6 Merge pull request #2639 from code-yeongyu/feature/2635-smart-circuit-breaker
feat(background-agent): add smart circuit breaker for repeated tool calls
2026-03-17 16:43:08 +09:00
YeonGyu-Kim
53285617d3 Merge pull request #2636 from code-yeongyu/fix/pre-publish-blockers
fix: resolve 12 pre-publish blockers (security, correctness, migration)
2026-03-17 16:36:04 +09:00
YeonGyu-Kim
ae3befbfbe fix(background-agent): apply smart circuit breaker to manager events
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-17 16:31:55 +09:00
YeonGyu-Kim
dc1a05ac3e feat(background-agent): add loop detector helpers
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-17 16:31:55 +09:00
YeonGyu-Kim
e271b4a1b0 feat(config): add background task circuit breaker settings
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-17 16:31:55 +09:00
YeonGyu-Kim
fee938d63a fix(cli): cherry-pick glm-4.7-free → gpt-5-nano fallback fix from dev 2026-03-17 16:30:12 +09:00
YeonGyu-Kim
4d74d888e4 Merge pull request #2637 from code-yeongyu/fix/ulw-verification-session-tracking
fix(ulw-loop): add fallback for Oracle verification session tracking
2026-03-17 16:25:28 +09:00
YeonGyu-Kim
4bc7b1d27c fix(ulw-loop): add fallback for Oracle verification session tracking
The verification_session_id was never reliably set because the
prompt-based attempt_id matching in tool-execute-after depends on
metadata.prompt surviving the delegate-task execution chain. When
this fails silently, the loop never detects Oracle's VERIFIED
emission.

Add a fallback: when exact attempt_id matching fails but oracle
agent + verification_pending state match, still set the session ID.
Add diagnostic logging to trace verification flow failures.
Add integration test covering the full verification chain.
2026-03-17 16:21:40 +09:00
YeonGyu-Kim
78dac0642e Merge pull request #2590 from MoerAI/fix/subagent-circuit-breaker
fix(background-agent): add circuit breaker to prevent subagent infinite loops (fixes #2571)
2026-03-17 16:09:29 +09:00
YeonGyu-Kim
92bc72a90b fix(bun-install): use workspaceDir option instead of hardcoded cache-dir 2026-03-17 16:05:51 +09:00
YeonGyu-Kim
a7301ba8a9 fix(delegate-task): guard skipped sentinel in subagent-resolver 2026-03-17 15:57:23 +09:00
YeonGyu-Kim
e9887dd82f fix(doctor): align auto-update and doctor config paths 2026-03-17 15:56:02 +09:00
YeonGyu-Kim
c0082d8a09 Merge pull request #2634 from code-yeongyu/fix/run-in-background-required
fix(delegate-task): remove auto-default for run_in_background, require explicit parameter
2026-03-17 15:55:17 +09:00
YeonGyu-Kim
fbc3b4e230 Merge pull request #2612 from MoerAI/fix/dead-fallback-model
fix(cli): replace dead glm-4.7-free with gpt-5-nano as ultimate fallback (fixes #2101)
2026-03-17 15:53:29 +09:00
YeonGyu-Kim
1f7fdb43ba Merge pull request #2539 from cpkt9762/fix/category-variant-no-requirement
fix(delegate-task): build categoryModel with variant for categories without fallback chain
2026-03-17 15:53:11 +09:00
YeonGyu-Kim
566031f4fa fix(delegate-task): remove auto-default for run_in_background, require explicit parameter
Remove the auto-defaulting logic from PR #2420 that silently set
run_in_background=false when category/subagent_type/session_id was present.

The tool description falsely claimed 'Default: false' which misled agents
into omitting the parameter. Now the description says REQUIRED and the
validation always throws when the parameter is missing, with a clear
error message guiding the agent to retry with the correct value.

Reverts the behavioral change from #2420 while keeping the issue's
root cause (misleading description) fixed.
2026-03-17 15:49:47 +09:00
YeonGyu-Kim
0cf386ec52 fix(skill-tool): invalidate cached skill description on execute 2026-03-17 15:49:26 +09:00
YeonGyu-Kim
d493f9ec3a fix(cli-run): move resolveRunModel inside try block 2026-03-17 15:49:26 +09:00
YeonGyu-Kim
2c7ded2433 fix(background-agent): defer task cleanup while siblings running 2026-03-17 15:17:34 +09:00
YeonGyu-Kim
82c7807a4f fix(event): clear retry dedupe key on non-retry status 2026-03-17 15:17:34 +09:00
YeonGyu-Kim
df7e1ae16d fix(todo-continuation): remove activity-based stagnation bypass 2026-03-17 15:17:34 +09:00
YeonGyu-Kim
0471078006 fix(tmux): escape serverUrl in pane shell commands 2026-03-17 15:16:54 +09:00
YeonGyu-Kim
1070b9170f docs: remove temporary injury notice from README 2026-03-17 10:41:56 +09:00
acamq
bb312711cf Merge pull request #2618 from RaviTharuma/fix/extract-status-code-nested-errors
fix(runtime-fallback): extract status code from nested AI SDK errors
2026-03-16 16:28:31 -06:00
github-actions[bot]
c31facf41e @gxlife has signed the CLA in code-yeongyu/oh-my-openagent#2625 2026-03-16 15:17:21 +00:00
YeonGyu-Kim
c644930753 Fix OpenClaw review issues 2026-03-16 22:28:54 +09:00
YeonGyu-Kim
b79df5e018 feat: port OpenClaw bidirectional integration from omx
Ports the complete OpenClaw integration system from oh-my-codex:

Outbound (opencode→OpenClaw):
- wakeOpenClaw() fire-and-forget gateway notifications
- HTTP and command gateway dispatchers
- Template variable interpolation
- Config from oh-my-opencode.jsonc (no env gate needed)

Inbound (OpenClaw→opencode):
- Reply listener daemon (Discord/Telegram polling)
- Session registry for message↔tmux pane correlation
- Tmux pane detection, content capture, and text injection
- Input sanitization and rate limiting
- Pane verification before injection

Files:
- src/openclaw/ (types, config, dispatcher, index, reply-listener, session-registry, tmux, daemon)
- src/config/schema/openclaw.ts (Zod v4 schema)
- src/hooks/openclaw.ts (session hook)
- Tests: 12 pass (config + dispatcher)
2026-03-16 21:55:10 +09:00
Ravi Tharuma
de66f1f397 fix(runtime-fallback): prefer numeric status codes over non-numeric in extraction chain
The nullish-coalescing chain could stop at a non-numeric value (e.g.
status: "error"), preventing deeper nested numeric statusCode values
from being reached. Switch to Array.find() with a type guard to always
select the first numeric value.

Adds 11 tests for extractStatusCode covering: top-level, nested
(data/error/cause), non-numeric skip, fallback to regex, and
precedence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 13:51:23 +01:00
YeonGyu-Kim
427fa6d7a2 Merge pull request #2619 from code-yeongyu/revert/openclaw-one-way
revert: remove one-way OpenClaw integration
2026-03-16 21:09:30 +09:00
YeonGyu-Kim
239da8b02a Revert "Merge pull request #2607 from code-yeongyu/feat/openclaw-integration"
This reverts commit 8213534e87, reversing
changes made to 84fb1113f1.
2026-03-16 21:09:08 +09:00
YeonGyu-Kim
17244e2c84 Revert "Merge pull request #2609 from code-yeongyu/fix/rename-omx-to-omo-env"
This reverts commit 4759dfb654, reversing
changes made to 8213534e87.
2026-03-16 21:09:08 +09:00
Ravi Tharuma
24a0f7b032 fix(runtime-fallback): extract status code from nested AI SDK errors
AI SDK wraps HTTP status codes inside error.error.statusCode (e.g., AI_APICallError). The current extractStatusCode only checks the top level, missing these nested codes.

This caused runtime-fallback to skip retryable errors like 400, 500, 504 because it couldn't find the status code.

Fixes #2617
2026-03-16 13:04:14 +01:00
MoerAI
fc48df1d53 fix(cli): replace dead glm-4.7-free with gpt-5-nano as ultimate fallback
The opencode/glm-4.7-free model was removed from the OpenCode platform,
causing the ULTIMATE_FALLBACK in the CLI installer to point to a dead
model. Users installing OMO without any major provider configured would
get a non-functional model assignment.

Replaced with opencode/gpt-5-nano which is confirmed available per
user reports and existing fallback chains in model-requirements.ts.

Fixes #2101
2026-03-16 19:21:10 +09:00
MoerAI
6455b851b8 fix(config): keep default OpenCode Build agent enabled by default
The default_builder_enabled config defaults to false, which removes
the default OpenCode Build agent on OMO install. This forces users
into the full OMO orchestration for every task, including simple ones
where the lightweight Build agent would be more appropriate.

Changed the default to true so the Build agent remains available
alongside Sisyphus. Users who prefer the previous behavior can set
default_builder_enabled: false in their config.

Fixes #2545
2026-03-16 19:18:46 +09:00
Ravi Tharuma
9346bc8379 fix: clamp variant "max" to "high" for non-Opus Claude models on fallback
When an agent configured with variant: "max" falls back from Opus to
Sonnet (or Haiku), the "max" variant was passed through unchanged.
OpenCode sends this as level: "max" to the Anthropic API, which rejects
it with: level "max" not supported, valid levels: low, medium, high

The anthropic-effort hook previously only handled Opus (inject effort=max)
and skipped all other Claude models. Now it actively clamps "max" → "high"
for non-Opus Claude models and mutates message.variant so OpenCode
doesn't pass the unsupported level to the API.
2026-03-16 07:49:55 +01:00
MoerAI
3055454ecc fix(background-agent): add circuit breaker to prevent subagent infinite loops
Adds a configurable maxToolCalls limit (default: 200) that automatically
cancels background tasks when they exceed the threshold. This prevents
runaway subagent loops from burning unlimited tokens, as reported in #2571
where a Gemini subagent ran 809 consecutive tool calls over 3.5 hours
costing ~$350.

The circuit breaker triggers in the existing tool call tracking path
(message.part.updated/delta events) and cancels the task with a clear
error message explaining what happened. The limit is configurable via
background_task.maxToolCalls in oh-my-opencode.jsonc.

Fixes #2571
2026-03-16 11:07:33 +09:00
cpkt9762
11e9276498 fix(delegate-task): build categoryModel with variant for categories without fallback chain
When a category has no CATEGORY_MODEL_REQUIREMENTS entry (e.g.
user-defined categories like solana-re), the !requirement branch
set actualModel but never built categoryModel with variant from
the user config. The bottom fallback then created categoryModel
via parseModelString alone, silently dropping the variant.

Mirror the requirement branch logic: read variant from
userCategories and resolved.config, and build categoryModel
with it.

Fixes #2538
2026-03-13 04:15:17 +08:00
291 changed files with 12559 additions and 3230 deletions

BIN
.github/assets/building-in-public.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -59,20 +59,39 @@ jobs:
- 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}")
# Convert platform name for output (replace - with _)
PLATFORM_KEY="${{ matrix.platform }}"
PLATFORM_KEY="${PLATFORM_KEY//-/_}"
if [ "$STATUS" = "200" ]; then
# Check oh-my-opencode
OC_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-opencode-${{ matrix.platform }}/${VERSION}")
# Check oh-my-openagent
OA_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-openagent-${{ matrix.platform }}/${VERSION}")
echo "oh-my-opencode-${{ matrix.platform }}@${VERSION}: ${OC_STATUS}"
echo "oh-my-openagent-${{ matrix.platform }}@${VERSION}: ${OA_STATUS}"
if [ "$OC_STATUS" = "200" ]; then
echo "skip_opencode=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-opencode-${{ matrix.platform }}@${VERSION} already published"
else
echo "skip_opencode=false" >> $GITHUB_OUTPUT
echo "→ oh-my-opencode-${{ matrix.platform }}@${VERSION} needs publishing"
fi
if [ "$OA_STATUS" = "200" ]; then
echo "skip_openagent=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-openagent-${{ matrix.platform }}@${VERSION} already published"
else
echo "skip_openagent=false" >> $GITHUB_OUTPUT
echo "→ oh-my-openagent-${{ matrix.platform }}@${VERSION} needs publishing"
fi
# Skip build only if BOTH are already published
if [ "$OC_STATUS" = "200" ] && [ "$OA_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 in package.json
@@ -207,23 +226,38 @@ jobs:
matrix:
platform: [darwin-arm64, darwin-x64, darwin-x64-baseline, linux-x64, linux-x64-baseline, linux-arm64, linux-x64-musl, linux-x64-musl-baseline, linux-arm64-musl, windows-x64, windows-x64-baseline]
steps:
- name: Check if oh-my-opencode already published
- 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"
OC_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-opencode-${{ matrix.platform }}/${VERSION}")
OA_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-openagent-${{ matrix.platform }}/${VERSION}")
if [ "$OC_STATUS" = "200" ]; then
echo "skip_opencode=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-opencode-${{ matrix.platform }}@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
echo "→ ${PKG_NAME}@${VERSION} will be published"
echo "skip_opencode=false" >> $GITHUB_OUTPUT
fi
if [ "$OA_STATUS" = "200" ]; then
echo "skip_openagent=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-openagent-${{ matrix.platform }}@${VERSION} already published"
else
echo "skip_openagent=false" >> $GITHUB_OUTPUT
fi
# Need artifact if either package needs publishing
if [ "$OC_STATUS" = "200" ] && [ "$OA_STATUS" = "200" ]; then
echo "skip_all=true" >> $GITHUB_OUTPUT
else
echo "skip_all=false" >> $GITHUB_OUTPUT
fi
- name: Download artifact
id: download
if: steps.check.outputs.skip != 'true'
if: steps.check.outputs.skip_all != 'true'
continue-on-error: true
uses: actions/download-artifact@v4
with:
@@ -231,7 +265,7 @@ jobs:
path: .
- name: Extract artifact
if: steps.check.outputs.skip != 'true' && steps.download.outcome == 'success'
if: steps.check.outputs.skip_all != 'true' && steps.download.outcome == 'success'
run: |
PLATFORM="${{ matrix.platform }}"
mkdir -p packages/${PLATFORM}
@@ -247,13 +281,13 @@ jobs:
ls -la packages/${PLATFORM}/bin/
- uses: actions/setup-node@v4
if: steps.check.outputs.skip != 'true' && steps.download.outcome == 'success'
if: steps.check.outputs.skip_all != 'true' && steps.download.outcome == 'success'
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Publish ${{ matrix.platform }}
if: steps.check.outputs.skip != 'true' && steps.download.outcome == 'success'
- name: Publish oh-my-opencode-${{ matrix.platform }}
if: steps.check.outputs.skip_opencode != 'true' && steps.download.outcome == 'success'
run: |
cd packages/${{ matrix.platform }}
@@ -267,3 +301,25 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
timeout-minutes: 15
- name: Publish oh-my-openagent-${{ matrix.platform }}
if: steps.check.outputs.skip_openagent != 'true' && steps.download.outcome == 'success'
run: |
cd packages/${{ matrix.platform }}
# Rename package for oh-my-openagent
jq --arg name "oh-my-openagent-${{ matrix.platform }}" \
--arg desc "Platform-specific binary for oh-my-openagent (${{ matrix.platform }})" \
'.name = $name | .description = $desc | .bin = {"oh-my-openagent": (.bin | to_entries | .[0].value)}' \
package.json > tmp.json && mv tmp.json package.json
TAG_ARG=""
if [ -n "${{ inputs.dist_tag }}" ]; then
TAG_ARG="--tag ${{ inputs.dist_tag }}"
fi
npm publish --access public --provenance $TAG_ARG
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
timeout-minutes: 15

View File

@@ -57,6 +57,7 @@ jobs:
bun test src/cli/doctor/format-default.test.ts
bun test src/tools/call-omo-agent/sync-executor.test.ts
bun test src/tools/call-omo-agent/session-creator.test.ts
bun test src/tools/session-manager
bun test src/features/opencode-skill-loader/loader.test.ts
bun test src/hooks/anthropic-context-window-limit-recovery/recovery-hook.test.ts
bun test src/hooks/anthropic-context-window-limit-recovery/executor.test.ts
@@ -66,9 +67,8 @@ jobs:
# Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files
# that were already run in isolation above.
# Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts, session-manager (all)
# Excluded from src/hooks/anthropic-context-window-limit-recovery: recovery-hook.test.ts, executor.test.ts
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
bun test bin script src/config src/mcp src/index.test.ts \
src/agents src/shared \
src/cli/run src/cli/config-manager src/cli/mcp-oauth \
@@ -77,7 +77,7 @@ jobs:
src/cli/doctor/runner.test.ts src/cli/doctor/checks \
src/tools/ast-grep src/tools/background-task src/tools/delegate-task \
src/tools/glob src/tools/grep src/tools/interactive-bash \
src/tools/look-at src/tools/lsp src/tools/session-manager \
src/tools/look-at src/tools/lsp \
src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \
src/tools/call-omo-agent/background-agent-executor.test.ts \
src/tools/call-omo-agent/background-executor.test.ts \
@@ -216,6 +216,48 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
- name: Check if oh-my-openagent already published
id: check-openagent
run: |
VERSION="${{ steps.version.outputs.version }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-openagent/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "✓ oh-my-openagent@${VERSION} already published"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Publish oh-my-openagent
if: steps.check-openagent.outputs.skip != 'true'
run: |
VERSION="${{ steps.version.outputs.version }}"
# Update package name, version, and optionalDependencies for oh-my-openagent
jq --arg v "$VERSION" '
.name = "oh-my-openagent" |
.version = $v |
.optionalDependencies = (
.optionalDependencies | to_entries |
map(.key = (.key | sub("^oh-my-opencode-"; "oh-my-openagent-")) | .value = $v) |
from_entries
)
' package.json > tmp.json && mv tmp.json package.json
TAG_ARG=""
if [ -n "${{ steps.version.outputs.dist_tag }}" ]; then
TAG_ARG="--tag ${{ steps.version.outputs.dist_tag }}"
fi
npm publish --access public --provenance $TAG_ARG || echo "::warning::oh-my-openagent publish failed"
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
- name: Restore package.json
if: steps.check-openagent.outputs.skip != 'true'
run: |
git checkout -- package.json
trigger-platform:
runs-on: ubuntu-latest
needs: publish-main

1
.gitignore vendored
View File

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

View File

@@ -26,7 +26,7 @@ For each commit, you MUST:
<version-context>
<published-version>
!`npm view oh-my-openagent version 2>/dev/null || echo "not published"`
!`npm view oh-my-opencode version 2>/dev/null || echo "not published"`
</published-version>
<local-version>
!`node -p "require('./package.json').version" 2>/dev/null || echo "unknown"`
@@ -38,13 +38,13 @@ For each commit, you MUST:
<git-context>
<commits-since-release>
!`npm view oh-my-openagent version 2>/dev/null | xargs -I{} git log "v{}"..HEAD --oneline 2>/dev/null || echo "no commits since release"`
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git log "v{}"..HEAD --oneline 2>/dev/null || echo "no commits since release"`
</commits-since-release>
<diff-stat>
!`npm view oh-my-openagent version 2>/dev/null | xargs -I{} git diff "v{}"..HEAD --stat 2>/dev/null || echo "no diff available"`
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git diff "v{}"..HEAD --stat 2>/dev/null || echo "no diff available"`
</diff-stat>
<files-changed-summary>
!`npm view oh-my-openagent version 2>/dev/null | xargs -I{} git diff "v{}"..HEAD --stat 2>/dev/null | tail -1 || echo ""`
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git diff "v{}"..HEAD --stat 2>/dev/null | tail -1 || echo ""`
</files-changed-summary>
</git-context>

View File

@@ -1,5 +1,5 @@
---
description: Easter egg command - about oh-my-openagent
description: Easter egg command - about oh-my-opencode
---
<command-instruction>
@@ -13,9 +13,9 @@ Print the following message to the user EXACTLY as written (in a friendly, celeb
**You found the easter egg!** 🥚✨
## What is Oh My OpenAgent?
## What is Oh My OpenCode?
**Oh My OpenAgent** is a powerful OpenCode plugin that transforms your AI agent into a full development team:
**Oh My OpenCode** is a powerful OpenCode plugin that transforms your AI agent into a full development team:
- 🤖 **Multi-Agent Orchestration**: Oracle (GPT-5.2), Librarian (Claude), Explore (Grok), Frontend Engineer (Gemini), and more
- 🔧 **LSP Tools**: Full IDE capabilities for your agents - hover, goto definition, find references, rename, code actions
@@ -28,7 +28,7 @@ Print the following message to the user EXACTLY as written (in a friendly, celeb
Created with ❤️ by **[code-yeongyu](https://github.com/code-yeongyu)**
🔗 **GitHub**: https://github.com/code-yeongyu/oh-my-openagent
🔗 **GitHub**: https://github.com/code-yeongyu/oh-my-opencode
---

View File

@@ -1,10 +1,10 @@
---
description: Publish oh-my-openagent to npm via GitHub Actions workflow
description: Publish oh-my-opencode to npm via GitHub Actions workflow
argument-hint: <patch|minor|major>
---
<command-instruction>
You are the release manager for oh-my-openagent. Execute the FULL publish workflow from start to finish.
You are the release manager for oh-my-opencode. Execute the FULL publish workflow from start to finish.
## CRITICAL: ARGUMENT REQUIREMENT
@@ -277,7 +277,7 @@ gh release view "v${NEW_VERSION}" --json url --jq '.url'
Poll npm registry until the new version appears:
```bash
npm view oh-my-openagent version
npm view oh-my-opencode version
```
Compare with expected version. If not matching after 2 minutes, warn user about npm propagation delay.
@@ -314,7 +314,7 @@ After publish-platform workflow completes, verify all 7 platform packages are pu
```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-openagent-${PLATFORM}" version
npm view "oh-my-opencode-${PLATFORM}" version
done
```
@@ -323,13 +323,13 @@ All 7 packages should show the same version as the main package (`${NEW_VERSION}
**Expected packages:**
| Package | Description |
|---------|-------------|
| `oh-my-openagent-darwin-arm64` | macOS Apple Silicon |
| `oh-my-openagent-darwin-x64` | macOS Intel |
| `oh-my-openagent-linux-x64` | Linux x64 (glibc) |
| `oh-my-openagent-linux-arm64` | Linux ARM64 (glibc) |
| `oh-my-openagent-linux-x64-musl` | Linux x64 (musl/Alpine) |
| `oh-my-openagent-linux-arm64-musl` | Linux ARM64 (musl/Alpine) |
| `oh-my-openagent-windows-x64` | Windows x64 |
| `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.
@@ -339,8 +339,8 @@ If any platform package version doesn't match, warn the user and suggest checkin
Report success to user with:
- New version number
- GitHub release URL: https://github.com/code-yeongyu/oh-my-openagent/releases/tag/v{version}
- npm package URL: https://www.npmjs.com/package/oh-my-openagent
- 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
---
@@ -362,7 +362,7 @@ Respond to user in English.
<current-context>
<published-version>
!`npm view oh-my-openagent version 2>/dev/null || echo "not published"`
!`npm view oh-my-opencode version 2>/dev/null || echo "not published"`
</published-version>
<local-version>
!`node -p "require('./package.json').version" 2>/dev/null || echo "unknown"`
@@ -371,6 +371,6 @@ Respond to user in English.
!`git status --porcelain`
</git-status>
<recent-commits>
!`npm view oh-my-openagent version 2>/dev/null | xargs -I{} git log "v{}"..HEAD --oneline 2>/dev/null | head -15 || echo "no commits"`
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git log "v{}"..HEAD --oneline 2>/dev/null | head -15 || echo "no commits"`
</recent-commits>
</current-context>

View File

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

View File

@@ -37,7 +37,7 @@ Then capture raw data needed by agent prompts:
```bash
# Extract versions (already in /get-unpublished-changes output)
PUBLISHED=$(npm view oh-my-openagent version 2>/dev/null || echo "not published")
PUBLISHED=$(npm view oh-my-opencode version 2>/dev/null || echo "not published")
LOCAL=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
# Raw data for agents (diffs, file lists)
@@ -85,7 +85,7 @@ task(
<review_type>PER-CHANGE DEEP ANALYSIS</review_type>
<change_group>{GROUP_NAME}</change_group>
<project>oh-my-openagent (npm package)</project>
<project>oh-my-opencode (npm package)</project>
<published_version>{PUBLISHED}</published_version>
<target_version>{LOCAL}</target_version>
@@ -160,7 +160,7 @@ task(
prompt="""
Run /review-work on the unpublished changes between v{PUBLISHED} and HEAD.
GOAL: Review all changes heading into npm publish of oh-my-openagent. These changes span {COMMIT_COUNT} commits across {FILE_COUNT} files.
GOAL: Review all changes heading into npm publish of oh-my-opencode. These changes span {COMMIT_COUNT} commits across {FILE_COUNT} files.
CONSTRAINTS:
- This is a plugin published to npm — public API stability matters
@@ -169,7 +169,7 @@ CONSTRAINTS:
- Factory pattern (createXXX) for tools, hooks, agents
- kebab-case files, barrel exports, no catch-all files
BACKGROUND: Pre-publish review of oh-my-openagent, an OpenCode plugin with 1268 TypeScript files, 160k LOC. Changes since v{PUBLISHED} are about to be published.
BACKGROUND: Pre-publish review of oh-my-opencode, an OpenCode plugin with 1268 TypeScript files, 160k LOC. Changes since v{PUBLISHED} are about to be published.
The diff base is: git diff v{PUBLISHED}..HEAD
@@ -190,7 +190,7 @@ task(
prompt="""
<review_type>RELEASE SYNTHESIS — OVERALL ASSESSMENT</review_type>
<project>oh-my-openagent (npm package)</project>
<project>oh-my-opencode (npm package)</project>
<published_version>{PUBLISHED}</published_version>
<local_version>{LOCAL}</local_version>
@@ -325,7 +325,7 @@ Do NOT deliver the final report until ALL agents have completed.
Compile the final report:
```markdown
# Pre-Publish Review — oh-my-openagent
# Pre-Publish Review — oh-my-opencode
## Release: v{PUBLISHED} -> v{LOCAL}
**Commits:** {COMMIT_COUNT} | **Files Changed:** {FILE_COUNT} | **Agents:** {AGENT_COUNT}

View File

@@ -2,7 +2,7 @@
## Overview
Add a `max_background_agents` config option to oh-my-openagent that limits total simultaneous background agents across all models/providers. Currently, concurrency is only limited per-model/provider key (default 5 per key). This new option adds a **global ceiling** on total running background agents.
Add a `max_background_agents` config option to oh-my-opencode that limits total simultaneous background agents across all models/providers. Currently, concurrency is only limited per-model/provider key (default 5 per key). This new option adds a **global ceiling** on total running background agents.
## Step-by-Step Plan
@@ -80,7 +80,7 @@ Check `src/config/schema/background-task.ts` and `src/features/background-agent/
| File | Reason |
|------|--------|
| `src/config/schema/oh-my-openagent-config.ts` | No change needed - `BackgroundTaskConfigSchema` is already composed into root schema via `background_task` field |
| `src/config/schema/oh-my-opencode-config.ts` | No change needed - `BackgroundTaskConfigSchema` is already composed into root schema via `background_task` field |
| `src/create-managers.ts` | No change needed - `pluginConfig.background_task` already passed to `BackgroundManager` constructor |
| `src/features/background-agent/manager.ts` | No change needed - already passes config to `ConcurrencyManager` |
| `src/plugin-config.ts` | No change needed - `background_task` is a simple object field, uses default override merge |

View File

@@ -63,7 +63,7 @@ All existing tests must continue to pass unchanged.
Verify the config flows correctly through the system:
1. **Schema → Type**: `BackgroundTaskConfig` type auto-includes `maxBackgroundAgents` via `z.infer`
2. **Config file → Schema**: `loadConfigFromPath()` in `plugin-config.ts` uses `OhMyOpenAgentConfigSchema.safeParse()` which includes `BackgroundTaskConfigSchema`
2. **Config file → Schema**: `loadConfigFromPath()` in `plugin-config.ts` uses `OhMyOpenCodeConfigSchema.safeParse()` which includes `BackgroundTaskConfigSchema`
3. **Config → Manager**: `create-managers.ts` passes `pluginConfig.background_task` to `BackgroundManager` constructor
4. **Manager → ConcurrencyManager**: `BackgroundManager` constructor passes config to `new ConcurrencyManager(config)`
5. **ConcurrencyManager → Enforcement**: `acquire()` reads `config.maxBackgroundAgents` via `getGlobalLimit()`

View File

@@ -36,7 +36,7 @@ import { createWebsearchConfig } from "./websearch"
import { context7 } from "./context7"
import { grep_app } from "./grep-app"
import { arxiv } from "./arxiv"
import type { OhMyOpenAgentConfig } from "../config/schema"
import type { OhMyOpenCodeConfig } from "../config/schema"
export { McpNameSchema, type McpName } from "./types"
@@ -48,7 +48,7 @@ type RemoteMcpConfig = {
oauth?: false
}
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenAgentConfig) {
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenCodeConfig) {
const mcps: Record<string, RemoteMcpConfig> = {}
if (!disabledMcps.includes("websearch")) {

View File

@@ -41,7 +41,7 @@ Pattern followed: `grep-app.ts` (static export, no auth, no config factory neede
import { context7 } from "./context7"
import { grep_app } from "./grep-app"
+import { arxiv } from "./arxiv"
import type { OhMyOpenAgentConfig } from "../config/schema"
import type { OhMyOpenCodeConfig } from "../config/schema"
-export { McpNameSchema, type McpName } from "./types"
+export { McpNameSchema, type McpName } from "./types"
@@ -54,7 +54,7 @@ Pattern followed: `grep-app.ts` (static export, no auth, no config factory neede
oauth?: false
}
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenAgentConfig) {
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenCodeConfig) {
const mcps: Record<string, RemoteMcpConfig> = {}
if (!disabledMcps.includes("websearch")) {

View File

@@ -51,7 +51,7 @@ Since the regex lives in the Go binary and this repo wraps it, the fix is two-pr
- Relax `(?i)^[\s#/*-]*note:\s*\w` to only match AI-style memo patterns like `Note: this was changed...`, `Note: implementation details...`
- Add `--exclude-pattern` CLI flag for user-configurable exclusions
**B. This repo (oh-my-openagent)** - the PR scope:
**B. This repo (oh-my-opencode)** - the PR scope:
1. Add `exclude_patterns` config field to `CommentCheckerConfigSchema`
2. Pass `--exclude-pattern` flags to the CLI binary
3. Add integration tests with mock binaries for false positive scenarios

View File

@@ -20,7 +20,7 @@ Additionally, the binary flags ALL non-filtered comments (not just agent memos),
## Architecture Understanding
```
TypeScript (oh-my-openagent) Go Binary (go-claude-code-comment-checker)
TypeScript (oh-my-opencode) Go Binary (go-claude-code-comment-checker)
───────────────────────────── ──────────────────────────────────────────
hook.ts main.go
├─ tool.execute.before ├─ Read JSON from stdin
@@ -33,7 +33,7 @@ hook.ts main.go
└─ append to output
```
Key files in oh-my-openagent:
Key files in oh-my-opencode:
- `src/hooks/comment-checker/hook.ts` - Hook factory, registers before/after handlers
- `src/hooks/comment-checker/cli-runner.ts` - Orchestrates CLI invocation, semaphore
- `src/hooks/comment-checker/cli.ts` - Binary resolution, process spawning, timeout handling

View File

@@ -1,27 +1,27 @@
# oh-my-openagent — O P E N C O D E Plugin
# oh-my-opencode — O P E N C O D E Plugin
**Generated:** 2026-03-06 | **Commit:** 7fe44024 | **Branch:** dev
## OVERVIEW
OpenCode plugin (npm: `oh-my-openagent`) that extends Claude Code (OpenCode fork) with multi-agent orchestration, 46 lifecycle hooks, 26 tools, skill/command/MCP systems, and Claude Code compatibility. 1268 TypeScript files, 160k LOC.
OpenCode plugin (npm: `oh-my-opencode`) that extends Claude Code (OpenCode fork) with multi-agent orchestration, 48 lifecycle hooks, 26 tools, skill/command/MCP systems, and Claude Code compatibility. 1268 TypeScript files, 160k LOC.
## STRUCTURE
```
oh-my-openagent/
oh-my-opencode/
├── src/
│ ├── index.ts # Plugin entry: loadConfig → createManagers → createTools → createHooks → createPluginInterface
│ ├── plugin-config.ts # JSONC multi-level config: user → project → defaults (Zod v4)
│ ├── agents/ # 11 agents (Sisyphus, Hephaestus, Oracle, Librarian, Explore, Atlas, Prometheus, Metis, Momus, Multimodal-Looker, Sisyphus-Junior)
│ ├── hooks/ # 46 hooks across 45 directories + 11 standalone files
│ ├── hooks/ # 48 lifecycle hooks across dedicated modules and standalone files
│ ├── tools/ # 26 tools across 15 directories
│ ├── features/ # 19 feature modules (background-agent, skill-loader, tmux, MCP-OAuth, etc.)
│ ├── shared/ # 95+ utility files in 13 categories
│ ├── config/ # Zod v4 schema system (24 files)
│ ├── cli/ # CLI: install, run, doctor, mcp-oauth (Commander.js)
│ ├── mcp/ # 3 built-in remote MCPs (websearch, context7, grep_app)
│ ├── plugin/ # 8 OpenCode hook handlers + 46 hook composition
│ ├── plugin/ # 8 OpenCode hook handlers + 48 hook composition
│ └── plugin-handlers/ # 6-phase config loading pipeline
├── packages/ # Monorepo: cli-runner, 12 platform binaries
└── local-ignore/ # Dev-only test fixtures
@@ -30,11 +30,11 @@ oh-my-openagent/
## INITIALIZATION FLOW
```
OhMyOpenAgentPlugin(ctx)
OhMyOpenCodePlugin(ctx)
├─→ loadPluginConfig() # JSONC parse → project/user merge → Zod validate → migrate
├─→ createManagers() # TmuxSessionManager, BackgroundManager, SkillMcpManager, ConfigHandler
├─→ createTools() # SkillContext + AvailableCategories + ToolRegistry (26 tools)
├─→ createHooks() # 3-tier: Core(37) + Continuation(7) + Skill(2) = 46 hooks
├─→ createHooks() # 3-tier: Core(39) + Continuation(7) + Skill(2) = 48 hooks
└─→ createPluginInterface() # 8 OpenCode hook handlers → PluginInterface
```
@@ -65,7 +65,7 @@ OhMyOpenAgentPlugin(ctx)
| Add new command | `src/features/builtin-commands/` | Template in templates/ |
| Add new CLI command | `src/cli/cli-program.ts` | Commander.js subcommand |
| Add new doctor check | `src/cli/doctor/checks/` | Register in checks/index.ts |
| Modify config schema | `src/config/schema/` + update root schema | Zod v4, add to OhMyOpenAgentConfigSchema |
| Modify config schema | `src/config/schema/` + update root schema | Zod v4, add to OhMyOpenCodeConfigSchema |
| Add new category | `src/tools/delegate-task/constants.ts` | DEFAULT_CATEGORIES + CATEGORY_MODEL_REQUIREMENTS |
## MULTI-LEVEL CONFIG
@@ -97,7 +97,7 @@ Fields: agents (14 overridable, 21 fields each), categories (8 built-in + custom
- **Test pattern**: Bun test (`bun:test`), co-located `*.test.ts`, given/when/then style (nested describe with `#given`/`#when`/`#then` prefixes)
- **CI test split**: mock-heavy tests run in isolation (separate `bun test` processes), rest in batch
- **Factory pattern**: `createXXX()` for all tools, hooks, agents
- **Hook tiers**: Session (23) → Tool-Guard (10) → Transform (4) → Continuation (7) → Skill (2)
- **Hook tiers**: Session (23) → Tool-Guard (12) → Transform (4) → Continuation (7) → Skill (2)
- **Agent modes**: `primary` (respects UI model) vs `subagent` (own fallback chain) vs `all`
- **Model resolution**: 4-step: override → category-default → provider-fallback → system-default
- **Config format**: JSONC with comments, Zod v4 validation, snake_case keys
@@ -128,9 +128,9 @@ bun test # Bun test suite
bun run build # Build plugin (ESM + declarations + schema)
bun run build:all # Build + platform binaries
bun run typecheck # tsc --noEmit
bunx oh-my-openagent install # Interactive setup
bunx oh-my-openagent doctor # Health diagnostics
bunx oh-my-openagent run # Non-interactive session
bunx oh-my-opencode install # Interactive setup
bunx oh-my-opencode doctor # Health diagnostics
bunx oh-my-opencode run # Non-interactive session
```
## CI/CD

2
CLA.md
View File

@@ -1,6 +1,6 @@
# Contributor License Agreement
Thank you for your interest in contributing to oh-my-openagent ("Project"), owned by YeonGyu Kim ("Owner").
Thank you for your interest in contributing to oh-my-opencode ("Project"), owned by YeonGyu Kim ("Owner").
By signing this Contributor License Agreement ("Agreement"), you agree to the following terms:

View File

@@ -1,6 +1,6 @@
# Contributing to Oh My OpenAgent
# Contributing to Oh My OpenCode
First off, thanks for taking the time to contribute! This document provides guidelines and instructions for contributing to oh-my-openagent.
First off, thanks for taking the time to contribute! This document provides guidelines and instructions for contributing to oh-my-opencode.
## Table of Contents
@@ -87,19 +87,19 @@ After making changes, you can test your local build in OpenCode:
```json
{
"plugin": ["file:///absolute/path/to/oh-my-openagent/dist/index.js"]
"plugin": ["file:///absolute/path/to/oh-my-opencode/dist/index.js"]
}
```
For example, if your project is at `/Users/yourname/projects/oh-my-openagent`:
For example, if your project is at `/Users/yourname/projects/oh-my-opencode`:
```json
{
"plugin": ["file:///Users/yourname/projects/oh-my-openagent/dist/index.js"]
"plugin": ["file:///Users/yourname/projects/oh-my-opencode/dist/index.js"]
}
```
> **Note**: Remove `"oh-my-openagent"` from the plugin array if it exists, to avoid conflicts with the npm version.
> **Note**: Remove `"oh-my-opencode"` from the plugin array if it exists, to avoid conflicts with the npm version.
3. **Restart OpenCode** to load the changes.
@@ -108,9 +108,9 @@ After making changes, you can test your local build in OpenCode:
## Project Structure
```
oh-my-openagent/
oh-my-opencode/
├── src/
│ ├── index.ts # Plugin entry (OhMyOpenAgentPlugin)
│ ├── index.ts # Plugin entry (OhMyOpenCodePlugin)
│ ├── plugin-config.ts # JSONC multi-level config (Zod v4)
│ ├── agents/ # 11 agents (Sisyphus, Hephaestus, Oracle, Librarian, Explore, Atlas, Prometheus, Metis, Momus, Multimodal-Looker, Sisyphus-Junior)
│ ├── hooks/ # Lifecycle hooks for orchestration, recovery, UX, and context management
@@ -272,4 +272,4 @@ export function createMyHook(input: PluginInput) {
---
Thank you for contributing to Oh My OpenAgent! Your efforts help make AI-assisted coding better for everyone.
Thank you for contributing to Oh My OpenCode! Your efforts help make AI-assisted coding better for everyone.

View File

@@ -72,7 +72,7 @@ Use ultrawork (ulw) to spawn UltraBrain agents in parallel. Each UB agent gets a
## ADDITIONAL BLOCKERS FROM GPT-5.4 REVIEW
### G1: Package Identity Split-Brain
**Problem:** Installer writes oh-my-openagent but doctor, auto-update, version lookup, publish workflow still reference oh-my-openagent. Half-migrated state.
**Problem:** Installer writes oh-my-openagent but doctor, auto-update, version lookup, publish workflow still reference oh-my-opencode. Half-migrated state.
**Fix:** Audit ALL references to package name. Either complete the migration consistently or revert to single name for this release.
**Files:** Installer, doctor, auto-update, version lookup, publish workflow -- grep for both package names

View File

@@ -2,7 +2,7 @@
Portions of this software are licensed as follows:
- All third party components incorporated into the oh-my-openagent Software are licensed under the original license
- All third party components incorporated into the oh-my-opencode Software are licensed under the original license
provided by the owner of the applicable component.
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
License" as defined below.

View File

@@ -4,6 +4,17 @@
> コアメンテナーのQが負傷したため、今週は Issue/PR への返信とリリースが遅れる可能性があります。
> ご理解とご支援に感謝します。
> [!TIP]
> **Building in Public**
>
> メンテナーが Jobdori を使い、oh-my-opencode をリアルタイムで開発・メンテナンスしています。Jobdori は OpenClaw をベースに大幅カスタマイズされた AI アシスタントです。
> すべての機能開発、修正、Issue トリアージを Discord でライブでご覧いただけます。
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ #building-in-public で確認する**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)
@@ -12,18 +23,18 @@
> [!TIP]
> 私たちと一緒に!
>
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | [Discordコミュニティ](https://discord.gg/PUwSMR9XNk)に参加して、コントリビューターや他の `oh-my-openagent` ユーザーと交流しましょう。 |
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | [Discordコミュニティ](https://discord.gg/PUwSMR9XNk)に参加して、コントリビューターや他の `oh-my-opencode` ユーザーと交流しましょう。 |
> | :-----| :----- |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-openagent` のニュースやアップデートは私のXアカウントで投稿されていましたが、 <br /> 誤って凍結されてしまったため、現在は [@justsisyphus](https://x.com/justsisyphus) が代わりにアップデートを投稿しています。 |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode` のニュースやアップデートは私のXアカウントで投稿されていましたが、 <br /> 誤って凍結されてしまったため、現在は [@justsisyphus](https://x.com/justsisyphus) が代わりにアップデートを投稿しています。 |
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | さらに多くのプロジェクトを見たい場合は、GitHubで [@code-yeongyu](https://github.com/code-yeongyu) をフォローしてください。 |
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
<div align="center">
[![Oh My OpenAgent](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Oh My OpenCode](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
</div>
@@ -34,7 +45,7 @@
<div align="center">
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-openagent?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/releases)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-openagent?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-openagent)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-opencode?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-opencode)
[![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-openagent?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/graphs/contributors)
[![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-openagent?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/network/members)
[![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/stargazers)
@@ -54,25 +65,25 @@
> 「Claude Codeが人間なら3ヶ月かかることを7日でやるとしたら、Sisyphusはそれを1時間でやってのけます。タスクが終わるまでひたすら働き続けます。まさに規律あるエージェントです。」 <br/>- B, Quant Researcher
> 「Oh My OpenAgentを使って、たった1日で8000個の eslint 警告を叩き潰しました。」 <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> 「Oh My Opencodeを使って、たった1日で8000個の eslint 警告を叩き潰しました。」 <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> 「Ohmyopencodeとralph loopを使って、45k行のtauriアプリを一晩でSaaSウェブアプリに変換しました。インタビューモードから始めて、私のプロンプトに対して質問や推奨事項を尋ねました。勝手に作業していくのを見るのは楽しかったし、今朝起きたらウェブサイトがほぼ動いているのを見て驚愕しました」 - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
> 「oh-my-openagentを使ってください。もう二度と元には戻れません。」 <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> 「oh-my-opencodeを使ってください。もう二度と元には戻れません。」 <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> 「何がどうすごいのかまだ上手く言語化できないんですが、開発体験が完全に異次元に到達してしまいました。」 - [苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
> 「週末にマインクラフト/ソウルライクな化け物を作ろうと、open code、oh my openagent、supermemoryで実験中です。昼食後の散歩に行っている間に、しゃがむアニメーションを追加するように指示しておきました。[動画]」 - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> 「週末にマインクラフト/ソウルライクな化け物を作ろうと、open code、oh my opencode、supermemoryで実験中です。昼食後の散歩に行っている間に、しゃがむアニメーションを追加するように指示しておきました。[動画]」 - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> 「これをコアに取り込んで彼を採用すべきだ。マジで。これ、本当に、本当に、本当に良い。」 <br/>- Henning Kilset
> 「彼を説得できるなら @yeon_gyu_kim を雇ってください。彼がopencodeに革命を起こしました。」 <br/>- [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
> 「Oh My OpenAgentはマジでヤバい」 - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
> 「Oh My OpenCodeはマジでヤバい」 - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
---
# Oh My OpenAgent
# Oh My OpenCode
最初はこれを「Claude Codeにステロイドを打ったもの」と呼んでいました。それは過小評価でした。
@@ -90,7 +101,7 @@ OmOをインストールして、`ultrawork`とタイプしてください。狂
以下のプロンプトをコピーして、あなたのLLMエージェントClaude Code、AmpCode、Cursorなどに貼り付けてください
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -260,19 +271,19 @@ project/
> **背景のストーリーを知りたいですか?** なぜSisyphusは岩を転がすのか、なぜHephaestusは「正当なる職人」なのか、そして[オーケストレーションガイド](docs/guide/orchestration.md)をお読みください。
>
> oh-my-openagentは初めてですか?どのモデルを使うべきかについては、**[インストールガイド](docs/guide/installation.md#step-5-understand-your-model-setup)** で推奨モデルを確認してください。
> oh-my-opencodeは初めてですか?どのモデルを使うべきかについては、**[インストールガイド](docs/guide/installation.md#step-5-understand-your-model-setup)** で推奨モデルを確認してください。
## アンインストール (Uninstallation)
oh-my-openagentを削除するには:
oh-my-opencodeを削除するには:
1. **OpenCodeの設定からプラグインを削除する**
`~/.config/opencode/opencode.json`(または `opencode.jsonc`)を編集し、`plugin` 配列から `"oh-my-openagent"` を削除します:
`~/.config/opencode/opencode.json`(または `opencode.jsonc`)を編集し、`plugin` 配列から `"oh-my-opencode"` を削除します:
```bash
# jq を使用する場合
jq '.plugin = [.plugin[] | select(. != "oh-my-openagent")]' \
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
```

View File

@@ -4,21 +4,32 @@
> 핵심 메인테이너 Q가 부상을 입어, 이번 주에는 이슈/PR 응답 및 릴리스가 지연될 수 있습니다.
> 양해와 응원에 감사드립니다.
> [!TIP]
> **Building in Public**
>
> 메인테이너가 Jobdori를 통해 oh-my-opencode를 실시간으로 개발하고 있습니다. Jobdori는 OpenClaw를 기반으로 대폭 커스터마이징된 AI 어시스턴트입니다.
> 모든 기능 개발, 버그 수정, 이슈 트리아지를 Discord에서 실시간으로 확인하세요.
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ #building-in-public에서 확인하기**](https://discord.gg/PUwSMR9XNk)
> [!TIP]
> 저희와 함께 하세요!
>
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | [Discord 커뮤니티](https://discord.gg/PUwSMR9XNk)에 가입하여 기여자 및 다른 `oh-my-openagent` 사용자들과 소통하세요. |
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | [Discord 커뮤니티](https://discord.gg/PUwSMR9XNk)에 가입하여 기여자 및 다른 `oh-my-opencode` 사용자들과 소통하세요. |
> | :-----| :----- |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-openagent`에 대한 소식과 업데이트는 제 X 계정에 올라왔었지만, <br /> 실수로 정지된 이후에는 [@justsisyphus](https://x.com/justsisyphus)가 대신 업데이트를 게시하고 있습니다. |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode`에 대한 소식과 업데이트는 제 X 계정에 올라왔었지만, <br /> 실수로 정지된 이후에는 [@justsisyphus](https://x.com/justsisyphus)가 대신 업데이트를 게시하고 있습니다. |
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | 더 많은 프로젝트를 보려면 GitHub에서 [@code-yeongyu](https://github.com/code-yeongyu)를 팔로우하세요. |
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
<div align="center">
[![Oh My OpenAgent](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Oh My OpenCode](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
</div>
@@ -29,7 +40,7 @@
<div align="center">
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-openagent?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/releases)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-openagent?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-openagent)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-opencode?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-opencode)
[![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-openagent?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/graphs/contributors)
[![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-openagent?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/network/members)
[![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/stargazers)
@@ -49,25 +60,25 @@
> "Claude Code가 인간이 3개월 걸릴 일을 7일 만에 한다면, Sisyphus는 1시간 만에 해냅니다. 작업이 끝날 때까지 그냥 계속 알아서 작동합니다. 이건 정말 규율이 잡힌 에이전트예요." <br/>- B, Quant Researcher
> "Oh My OpenAgent로 하루 만에 eslint 경고 8000개를 해결했습니다." <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> "Oh My Opencode로 하루 만에 eslint 경고 8000개를 해결했습니다." <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> "Ohmyopencode와 ralph loop를 써서 45k 라인짜리 tauri 앱을 하룻밤 만에 SaaS 웹앱으로 변환했어요. 인터뷰 모드로 시작해서, 제가 쓴 프롬프트에 대해 질문하고 추천을 부탁했죠. 일하는 걸 지켜보는 것도 재밌었고, 아침에 일어났더니 웹사이트가 대부분 돌아가고 있는 걸 보고 경악했습니다!" - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
> "oh-my-openagent 쓰세요, 다시는 예전으로 못 돌아갑니다." <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "oh-my-opencode 쓰세요, 다시는 예전으로 못 돌아갑니다." <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "뭐가 이렇게 대단한 건지 아직 정확하게 말로 표현하긴 어려운데, 개발 경험 자체가 완전히 다른 차원에 도달해버렸어요." - [苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
> "주말에 마인크래프트/소울라이크 같은 괴물 같은 걸 만들어보려고 open code, oh my openagent, supermemory로 실험 중입니다. 점심 먹고 산책 다녀오는 동안 앉기 애니메이션을 추가하라고 시켜뒀어요. [영상]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> "주말에 마인크래프트/소울라이크 같은 괴물 같은 걸 만들어보려고 open code, oh my opencode, supermemory로 실험 중입니다. 점심 먹고 산책 다녀오는 동안 앉기 애니메이션을 추가하라고 시켜뒀어요. [영상]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> "이걸 코어에 당겨오고 저 사람 스카우트해야 돼요. 진심으로. 이거 진짜, 진짜, 진짜 좋습니다." <br/>- Henning Kilset
> "설득할 수만 있다면 @yeon_gyu_kim 채용하세요, 이 사람이 opencode를 혁명적으로 바꿨습니다." <br/>- [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
> "Oh My OpenAgent는 진짜 미쳤다" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
> "Oh My OpenCode는 진짜 미쳤다" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
---
# Oh My OpenAgent
# Oh My OpenCode
Claude Code, Codex, 온갖 OSS 모델들 사이에서 헤매고 있나요. 워크플로우 설정하랴, 에이전트 디버깅하랴 피곤할 겁니다.
@@ -84,7 +95,7 @@ OmO 설치하고. `ultrawork` 치세요. 끝.
다음 프롬프트를 복사해서 여러분의 LLM 에이전트(Claude Code, AmpCode, Cursor 등)에 붙여넣으세요:
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -254,19 +265,19 @@ project/
> **비하인드 스토리가 궁금하신가요?** 왜 Sisyphus가 돌을 굴리는지, 왜 Hephaestus가 "진정한 장인"인지, 그리고 [오케스트레이션 가이드](docs/guide/orchestration.md)를 읽어보세요.
>
> oh-my-openagent가 처음이신가요? 어떤 모델을 써야 할지 **[설치 가이드](docs/guide/installation.md#step-5-understand-your-model-setup)** 에서 추천 조합을 확인하세요.
> oh-my-opencode가 처음이신가요? 어떤 모델을 써야 할지 **[설치 가이드](docs/guide/installation.md#step-5-understand-your-model-setup)** 에서 추천 조합을 확인하세요.
## 제거 (Uninstallation)
oh-my-openagent를 지우려면:
oh-my-opencode를 지우려면:
1. **OpenCode 설정에서 플러그인 제거**
`~/.config/opencode/opencode.json` (또는 `opencode.jsonc`)를 열고 `plugin` 배열에서 `"oh-my-openagent"`를 지우세요.
`~/.config/opencode/opencode.json` (또는 `opencode.jsonc`)를 열고 `plugin` 배열에서 `"oh-my-opencode"`를 지우세요.
```bash
# jq 사용 시
jq '.plugin = [.plugin[] | select(. != "oh-my-openagent")]' \
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
```

View File

@@ -1,3 +1,13 @@
> [!TIP]
> **Building in Public**
>
> The maintainer builds and maintains oh-my-opencode in real-time with Jobdori, an AI assistant built on a heavily customized fork of OpenClaw.
> Every feature, every fix, every issue triage — live in our Discord.
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ Watch it happen in #building-in-public**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)
@@ -6,18 +16,18 @@
> [!TIP]
> Be with us!
>
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | Join our [Discord community](https://discord.gg/PUwSMR9XNk) to connect with contributors and fellow `oh-my-openagent` users. |
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | Join our [Discord community](https://discord.gg/PUwSMR9XNk) to connect with contributors and fellow `oh-my-opencode` users. |
> | :-----| :----- |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | News and updates for `oh-my-openagent` used to be posted on my X account. <br /> Since it was suspended mistakenly, [@justsisyphus](https://x.com/justsisyphus) now posts updates on my behalf. |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | News and updates for `oh-my-opencode` used to be posted on my X account. <br /> Since it was suspended mistakenly, [@justsisyphus](https://x.com/justsisyphus) now posts updates on my behalf. |
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | Follow [@code-yeongyu](https://github.com/code-yeongyu) on GitHub for more projects. |
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
<div align="center">
[![Oh My OpenAgent](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Oh My OpenCode](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
</div>
@@ -31,7 +41,7 @@
<div align="center">
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-openagent?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/releases)
[![npm downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fohmyopenagent.com%2Fapi%2Fnpm-downloads&style=flat-square)](https://www.npmjs.com/package/oh-my-openagent)
[![npm downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fohmyopenagent.com%2Fapi%2Fnpm-downloads&style=flat-square)](https://www.npmjs.com/package/oh-my-opencode)
[![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-openagent?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/graphs/contributors)
[![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-openagent?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/network/members)
[![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/stargazers)
@@ -51,27 +61,27 @@
> "If Claude Code does in 7 days what a human does in 3 months, Sisyphus does it in 1 hour. It just works until the task is done. It is a discipline agent." <br/>- B, Quant Researcher
> "Knocked out 8000 eslint warnings with Oh My OpenAgent, just in a day" <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> "Knocked out 8000 eslint warnings with Oh My Opencode, just in a day" <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> "I converted a 45k line tauri app into a SaaS web app overnight using Ohmyopencode and ralph loop. Started with interview me prompt, asked it for ratings and recommendations on the questions. It was amazing to watch it work and to wake up this morning to a mostly working website!" - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
> "use oh-my-openagent, you will never go back" <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "use oh-my-opencode, you will never go back" <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> "I haven't really been able to articulate exactly what makes it so great yet, but the development experience has reached a completely different dimension." - [
苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
> "Experimenting with open code, oh my openagent and supermemory this weekend to build some minecraft/souls-like abomination."
> "Experimenting with open code, oh my opencode and supermemory this weekend to build some minecraft/souls-like abomination."
> "Asking it to add crouch animations while I go take my post-lunch walk. [Video]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> "You guys should pull this into core and recruit him. Seriously. It's really, really, really good." <br/>- Henning Kilset
> "Hire @yeon_gyu_kim if you can convince him, this dude has revolutionized opencode." <br/>- [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
> "Oh My OpenAgent Is Actually Insane" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
> "Oh My OpenCode Is Actually Insane" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
---
# Oh My OpenAgent
# Oh My OpenCode
You're juggling Claude Code, Codex, random OSS models. Configuring workflows. Debugging agents.
@@ -87,7 +97,7 @@ Install OmO. Type `ultrawork`. Done.
Copy and paste this prompt to your LLM agent (Claude Code, AmpCode, Cursor, etc.):
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -158,10 +168,6 @@ Even only with following subscriptions, ultrawork will work well (this project i
**Prometheus** (`claude-opus-4-6` / **`kimi-k2.5`** / **`glm-5`** ) is your strategic planner. Interview mode: it questions, identifies scope, and builds a detailed plan before a single line of code is touched.
**Atlas** (`claude-sonnet-4-6`) is the executor. He takes the plan from Prometheus and drives it to completion, managing the todo list and coordinating subagents.
**Sisyphus-Junior** is the dedicated executor for category-based tasks.
Every agent is tuned to its model's specific strengths. No manual model-juggling. [Learn more →](docs/guide/overview.md)
> Anthropic [blocked OpenCode because of us.](https://x.com/thdxr/status/2010149530486911014) That's why Hephaestus is called "The Legitimate Craftsman." The irony is intentional.
@@ -259,19 +265,19 @@ Add your own: `.opencode/skills/*/SKILL.md` or `~/.config/opencode/skills/*/SKIL
---
> **New to oh-my-openagent?** Read the **[Overview](docs/guide/overview.md)** to understand what you have, or check the **[Orchestration Guide](docs/guide/orchestration.md)** for how agents collaborate.
> **New to oh-my-opencode?** Read the **[Overview](docs/guide/overview.md)** to understand what you have, or check the **[Orchestration Guide](docs/guide/orchestration.md)** for how agents collaborate.
## Uninstallation
To remove oh-my-openagent:
To remove oh-my-opencode:
1. **Remove the plugin from your OpenCode config**
Edit `~/.config/opencode/opencode.json` (or `opencode.jsonc`) and remove `"oh-my-openagent"` from the `plugin` array:
Edit `~/.config/opencode/opencode.json` (or `opencode.jsonc`) and remove `"oh-my-opencode"` from the `plugin` array:
```bash
# Using jq
jq '.plugin = [.plugin[] | select(. != "oh-my-openagent")]' \
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
```
@@ -300,8 +306,7 @@ Features you'll think should've always existed. Once you use them, you can't go
See full [Features Documentation](docs/reference/features.md).
**Quick Overview:**
- **Primary Agents**: Sisyphus (the main agent), Hephaestus (deep worker), Prometheus (planner), Atlas (executor), Sisyphus-Junior (category executor)
- **Specialist Subagents**: Oracle (architecture/debugging), Librarian (docs/code search), Explore (fast codebase grep), Multimodal Looker (vision)
- **Agents**: Sisyphus (the main agent), Prometheus (planner), Oracle (architecture/debugging), Librarian (docs/code search), Explore (fast codebase grep), Multimodal Looker
- **Background Agents**: Run multiple agents in parallel like a real dev team
- **LSP & AST Tools**: Refactoring, rename, diagnostics, AST-aware code search
- **Hash-anchored Edit Tool**: `LINE#ID` references validate content before applying every change. Surgical edits, zero stale-line errors
@@ -309,7 +314,7 @@ See full [Features Documentation](docs/reference/features.md).
- **Claude Code Compatibility**: Full hook system, commands, skills, agents, MCPs
- **Built-in MCPs**: websearch (Exa), context7 (docs), grep_app (GitHub search)
- **Session Tools**: List, read, search, and analyze session history
- **Productivity Features**: Ralph Loop, Todo Enforcer, GPT permission-tail continuation, Comment Checker, Think Mode, and more
- **Productivity Features**: Ralph Loop, Todo Enforcer, Comment Checker, Think Mode, and more
- **Model Setup**: Agent-model matching is built into the [Installation Guide](docs/guide/installation.md#step-5-understand-your-model-setup)
## Configuration
@@ -326,7 +331,7 @@ See [Configuration Documentation](docs/reference/configuration.md).
- **Sisyphus Agent**: Main orchestrator with Prometheus (Planner) and Metis (Plan Consultant)
- **Background Tasks**: Configure concurrency limits per provider/model
- **Categories**: Domain-specific task delegation (`visual`, `business-logic`, custom)
- **Hooks**: 25+ built-in hooks, including `gpt-permission-continuation`, all configurable via `disabled_hooks`
- **Hooks**: 25+ built-in hooks, all configurable via `disabled_hooks`
- **MCPs**: Built-in websearch (Exa), context7 (docs), grep_app (GitHub search)
- **LSP**: Full LSP support with refactoring tools
- **Experimental**: Aggressive truncation, auto-resume, and more

View File

@@ -4,6 +4,17 @@
> Ключевой мейнтейнер Q получил травму, поэтому на этой неделе ответы по issue/PR и релизы могут задерживаться.
> Спасибо за терпение и поддержку.
> [!TIP]
> **Building in Public**
>
> Мейнтейнер разрабатывает и поддерживает oh-my-opencode в режиме реального времени с помощью Jobdori — ИИ-ассистента на базе глубоко кастомизированной версии OpenClaw.
> Каждая фича, каждый фикс, каждый триаж issue — в прямом эфире в нашем Discord.
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ Смотрите в #building-in-public**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)
@@ -12,16 +23,16 @@
> [!TIP] Будьте с нами!
>
> | [](https://discord.gg/PUwSMR9XNk) | Вступайте в наш [Discord](https://discord.gg/PUwSMR9XNk), чтобы общаться с контрибьюторами и пользователями `oh-my-openagent`. |
> | [](https://discord.gg/PUwSMR9XNk) | Вступайте в наш [Discord](https://discord.gg/PUwSMR9XNk), чтобы общаться с контрибьюторами и пользователями `oh-my-opencode`. |
> | ----------------------------------- | ------------------------------------------------------------ |
> | [](https://x.com/justsisyphus) | Новости и обновления `oh-my-openagent` раньше публиковались на моём аккаунте X. <br /> После ошибочной блокировки, [@justsisyphus](https://x.com/justsisyphus) публикует обновления вместо меня. |
> | [](https://x.com/justsisyphus) | Новости и обновления `oh-my-opencode` раньше публиковались на моём аккаунте X. <br /> После ошибочной блокировки, [@justsisyphus](https://x.com/justsisyphus) публикует обновления вместо меня. |
> | [](https://github.com/code-yeongyu) | Подпишитесь на [@code-yeongyu](https://github.com/code-yeongyu) на GitHub, чтобы следить за другими проектами. |
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> --> <div align="center">
[![Oh My OpenAgent](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Oh My OpenCode](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
</div>
@@ -31,7 +42,7 @@
<div align="center">
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-openagent?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/releases) [![npm downloads](https://img.shields.io/npm/dt/oh-my-openagent?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-openagent) [![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-openagent?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/graphs/contributors) [![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-openagent?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/network/members) [![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/stargazers) [![GitHub Issues](https://img.shields.io/github/issues/code-yeongyu/oh-my-openagent?color=ff80eb&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/issues) [![License](https://img.shields.io/badge/license-SUL--1.0-white?labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/blob/master/LICENSE.md) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-openagent)
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-openagent?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/releases) [![npm downloads](https://img.shields.io/npm/dt/oh-my-opencode?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-opencode) [![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-openagent?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/graphs/contributors) [![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-openagent?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/network/members) [![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/stargazers) [![GitHub Issues](https://img.shields.io/github/issues/code-yeongyu/oh-my-openagent?color=ff80eb&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/issues) [![License](https://img.shields.io/badge/license-SUL--1.0-white?labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/blob/master/LICENSE.md) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/code-yeongyu/oh-my-openagent)
English | 한국어 | 日本語 | 简体中文 | Русский
@@ -43,25 +54,25 @@ English | 한국어 | 日本語 | 简体中文 | Русский
> «Если Claude Code делает за 7 дней то, на что у человека уходит 3 месяца, Sisyphus справляется за 1 час. Он просто работает, пока задача не выполнена. Это дисциплинированный агент.» <br/>— B, исследователь в области квантовых финансов
> «За один день устранил 8000 предупреждений eslint с помощью Oh My OpenAgent.» <br/>— [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> «За один день устранил 8000 предупреждений eslint с помощью Oh My Opencode.» <br/>— [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> «За ночь конвертировал приложение на tauri в 45k строк в веб-SaaS с помощью Ohmyopencode и ralph loop. Начал с промпта «проинтервьюируй меня», попросил оценки и рекомендации по вопросам. Было удивительно наблюдать за работой и утром проснуться с почти рабочим сайтом!» — [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
> «Используйте oh-my-openagent — вы не захотите возвращаться назад.» <br/>— [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> «Используйте oh-my-opencode — вы не захотите возвращаться назад.» <br/>— [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> «Пока не могу точно объяснить, почему это так круто, но опыт разработки вышел на совершенно другой уровень.» — [苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
> «Экспериментирую с open code, oh my openagent и supermemory этим выходным, чтобы собрать нечто среднее между Minecraft и souls-like.» «Попросил добавить анимации приседания, пока хожу на обеденную прогулку. [Видео]» — [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> «Экспериментирую с open code, oh my opencode и supermemory этим выходным, чтобы собрать нечто среднее между Minecraft и souls-like.» «Попросил добавить анимации приседания, пока хожу на обеденную прогулку. [Видео]» — [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> «Ребята, вам нужно включить это в ядро и нанять его. Серьёзно. Это очень, очень, очень хорошо.» <br/>— Henning Kilset
> «Наймите @yeon_gyu_kim, если сможете его уговорить, этот парень революционизировал opencode.» <br/>— [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
> «Oh My OpenAgent — это что-то с чем-то» — [YouTube — Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
> «Oh My OpenCode — это что-то с чем-то» — [YouTube — Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
------
# Oh My OpenAgent
# Oh My OpenCode
Вы жонглируете Claude Code, Codex, случайными OSS-моделями. Настраиваете рабочие процессы. Дебажите агентов.
@@ -76,7 +87,7 @@ English | 한국어 | 日本語 | 简体中文 | Русский
Скопируйте и вставьте этот промпт в ваш LLM-агент (Claude Code, AmpCode, Cursor и т.д.):
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -242,19 +253,19 @@ project/
------
> **Впервые в oh-my-openagent?** Прочитайте **Обзор**, чтобы понять, что у вас есть, или ознакомьтесь с **руководством по оркестрации**, чтобы узнать, как агенты взаимодействуют.
> **Впервые в oh-my-opencode?** Прочитайте **Обзор**, чтобы понять, что у вас есть, или ознакомьтесь с **руководством по оркестрации**, чтобы узнать, как агенты взаимодействуют.
## Удаление
Чтобы удалить oh-my-openagent:
Чтобы удалить oh-my-opencode:
1. **Удалите плагин из конфига OpenCode**
Отредактируйте `~/.config/opencode/opencode.json` (или `opencode.jsonc`) и уберите `"oh-my-openagent"` из массива `plugin`:
Отредактируйте `~/.config/opencode/opencode.json` (или `opencode.jsonc`) и уберите `"oh-my-opencode"` из массива `plugin`:
```bash
# С помощью jq
jq '.plugin = [.plugin[] | select(. != "oh-my-openagent")]' \
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
```

View File

@@ -4,6 +4,17 @@
> 核心维护者 Q 因受伤,本周 issue/PR 回复和发布可能会延迟。
> 感谢你的耐心与支持。
> [!TIP]
> **Building in Public**
>
> 维护者正在使用 Jobdori 实时开发和维护 oh-my-opencode。Jobdori 是基于 OpenClaw 深度定制的 AI 助手。
> 每个功能开发、每次修复、每次 Issue 分类,都在 Discord 上实时进行。
>
> [![Building in Public](./.github/assets/building-in-public.png)](https://discord.gg/PUwSMR9XNk)
>
> [**→ 在 #building-in-public 频道中查看**](https://discord.gg/PUwSMR9XNk)
> [!NOTE]
>
> [![Sisyphus Labs - Sisyphus is the agent that codes like your team.](./.github/assets/sisyphuslabs.png?v=2)](https://sisyphuslabs.ai)
@@ -12,18 +23,18 @@
> [!TIP]
> 加入我们!
>
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | 加入我们的 [Discord 社区](https://discord.gg/PUwSMR9XNk),与贡献者及其他 `oh-my-openagent` 用户交流。 |
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | 加入我们的 [Discord 社区](https://discord.gg/PUwSMR9XNk),与贡献者及其他 `oh-my-opencode` 用户交流。 |
> | :-----| :----- |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | 关于 `oh-my-openagent` 的新闻和更新过去发布在我的 X 账号上。<br /> 因为账号被意外停用,现在由 [@justsisyphus](https://x.com/justsisyphus) 代为发布更新。 |
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | 关于 `oh-my-opencode` 的新闻和更新过去发布在我的 X 账号上。<br /> 因为账号被意外停用,现在由 [@justsisyphus](https://x.com/justsisyphus) 代为发布更新。 |
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | 在 GitHub 上关注 [@code-yeongyu](https://github.com/code-yeongyu) 获取更多项目信息。 |
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
<div align="center">
[![Oh My OpenAgent](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Oh My OpenCode](./.github/assets/hero.jpg)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-openagent)
[![Preview](./.github/assets/omo.png)](https://github.com/code-yeongyu/oh-my-openagent#oh-my-opencode)
</div>
@@ -34,7 +45,7 @@
<div align="center">
[![GitHub Release](https://img.shields.io/github/v/release/code-yeongyu/oh-my-openagent?color=369eff&labelColor=black&logo=github&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/releases)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-openagent?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-openagent)
[![npm downloads](https://img.shields.io/npm/dt/oh-my-opencode?color=ff6b35&labelColor=black&style=flat-square)](https://www.npmjs.com/package/oh-my-opencode)
[![GitHub Contributors](https://img.shields.io/github/contributors/code-yeongyu/oh-my-openagent?color=c4f042&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/graphs/contributors)
[![GitHub Forks](https://img.shields.io/github/forks/code-yeongyu/oh-my-openagent?color=8ae8ff&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/network/members)
[![GitHub Stars](https://img.shields.io/github/stars/code-yeongyu/oh-my-openagent?color=ffcb47&labelColor=black&style=flat-square)](https://github.com/code-yeongyu/oh-my-openagent/stargazers)
@@ -54,25 +65,25 @@
> “如果人类需要 3 个月完成的事情 Claude Code 需要 7 天,那么 Sisyphus 只需要 1 小时。它会一直工作直到任务完成。它是一个极度自律的智能体。” <br/>- B, 量化研究员
> “用 Oh My OpenAgent 一天之内解决了 8000 个 eslint 警告。” <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> “用 Oh My Opencode 一天之内解决了 8000 个 eslint 警告。” <br/>- [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
> “我用 Ohmyopencode 和 ralph loop 花了一晚上的时间,把一个 45k 行代码的 tauri 应用转换成了 SaaS Web 应用。从面试模式开始,让它对我提供的提示词进行提问和提出建议。看着它工作很有趣,今早醒来看到网站基本已经跑起来了,太震撼了!” - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
> “用 oh-my-openagent 吧,你绝对回不去了。” <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> “用 oh-my-opencode 吧,你绝对回不去了。” <br/>- [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
> “我很难准确描述它到底哪里牛逼,但开发体验已经达到完全不同的维度了。” - [苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
> “这周末我用 open code、oh my openagent 和 supermemory 瞎折腾一个像我的世界/魂系一样的怪物游戏。吃完午饭去散步前,我让它把下蹲动画加进去。[视频]” - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> “这周末我用 open code、oh my opencode 和 supermemory 瞎折腾一个像我的世界/魂系一样的怪物游戏。吃完午饭去散步前,我让它把下蹲动画加进去。[视频]” - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
> “你们真该把这个合并到核心代码里,然后把他招安了。说真的,这东西实在太牛了。” <br/>- Henning Kilset
> “如果你们能说服 @yeon_gyu_kim赶紧招募他。这个人彻底改变了 opencode。” <br/>- [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
> “Oh My OpenAgent 简直疯了。” - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
> “Oh My OpenCode 简直疯了。” - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
---
# Oh My OpenAgent
# Oh My OpenCode
我们最初把这叫做“给 Claude Code 打类固醇”。那是低估了它。
@@ -91,7 +102,7 @@
复制并粘贴以下提示词到你的 LLM Agent (Claude Code, AmpCode, Cursor 等):
```
Install and configure oh-my-openagent by following the instructions here:
Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
```
@@ -259,19 +270,19 @@ Agent 会自动顺藤摸瓜加载对应的 Context免去了你所有的手动
---
> **第一次用 oh-my-openagent** 阅读 **[概述](docs/guide/overview.md)** 了解你拥有哪些功能,或查看 **[编排指南](docs/guide/orchestration.md)** 了解 Agent 如何协作。
> **第一次用 oh-my-opencode** 阅读 **[概述](docs/guide/overview.md)** 了解你拥有哪些功能,或查看 **[编排指南](docs/guide/orchestration.md)** 了解 Agent 如何协作。
## 如何卸载 (Uninstallation)
要移除 oh-my-openagent:
要移除 oh-my-opencode:
1. **从你的 OpenCode 配置文件中去掉插件**
编辑 `~/.config/opencode/opencode.json` (或 `opencode.jsonc`) ,并把 `"oh-my-openagent"``plugin` 数组中删掉:
编辑 `~/.config/opencode/opencode.json` (或 `opencode.jsonc`) ,并把 `"oh-my-opencode"``plugin` 数组中删掉:
```bash
# 如果你有 jq 的话
jq '.plugin = [.plugin[] | select(. != "oh-my-openagent")]' \
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
~/.config/opencode/opencode.json > /tmp/oc.json && \
mv /tmp/oc.json ~/.config/opencode/opencode.json
```

View File

@@ -3699,6 +3699,30 @@
"syncPollTimeoutMs": {
"type": "number",
"minimum": 60000
},
"maxToolCalls": {
"type": "integer",
"minimum": 10,
"maximum": 9007199254740991
},
"circuitBreaker": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"maxToolCalls": {
"type": "integer",
"minimum": 10,
"maximum": 9007199254740991
},
"consecutiveThreshold": {
"type": "integer",
"minimum": 5,
"maximum": 9007199254740991
}
},
"additionalProperties": false
}
},
"additionalProperties": false
@@ -3712,6 +3736,147 @@
},
"additionalProperties": false
},
"openclaw": {
"type": "object",
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"gateways": {
"default": {},
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"default": "http",
"type": "string",
"enum": [
"http",
"command"
]
},
"url": {
"type": "string"
},
"method": {
"default": "POST",
"type": "string"
},
"headers": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"command": {
"type": "string"
},
"timeout": {
"type": "number"
}
},
"required": [
"type",
"method"
],
"additionalProperties": false
}
},
"hooks": {
"default": {},
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "object",
"properties": {
"enabled": {
"default": true,
"type": "boolean"
},
"gateway": {
"type": "string"
},
"instruction": {
"type": "string"
}
},
"required": [
"enabled",
"gateway",
"instruction"
],
"additionalProperties": false
}
},
"replyListener": {
"type": "object",
"properties": {
"discordBotToken": {
"type": "string"
},
"discordChannelId": {
"type": "string"
},
"discordMention": {
"type": "string"
},
"authorizedDiscordUserIds": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
},
"telegramBotToken": {
"type": "string"
},
"telegramChatId": {
"type": "string"
},
"pollIntervalMs": {
"default": 3000,
"type": "number"
},
"rateLimitPerMinute": {
"default": 10,
"type": "number"
},
"maxMessageLength": {
"default": 500,
"type": "number"
},
"includePrefix": {
"default": true,
"type": "boolean"
}
},
"required": [
"authorizedDiscordUserIds",
"pollIntervalMs",
"rateLimitPerMinute",
"maxMessageLength",
"includePrefix"
],
"additionalProperties": false
}
},
"required": [
"enabled",
"gateways",
"hooks"
],
"additionalProperties": false
},
"babysitting": {
"type": "object",
"properties": {

View File

@@ -1,18 +0,0 @@
{
"name": "hashline-edit-benchmark",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Hashline edit tool benchmark using Vercel AI SDK with FriendliAI provider",
"scripts": {
"bench:basic": "bun run test-edit-ops.ts",
"bench:edge": "bun run test-edge-cases.ts",
"bench:multi": "bun run test-multi-model.ts",
"bench:all": "bun run bench:basic && bun run bench:edge"
},
"dependencies": {
"@friendliai/ai-provider": "^1.0.9",
"ai": "^6.0.94",
"zod": "^4.1.0"
}
}

View File

@@ -64,8 +64,8 @@ These agents have Claude-optimized prompts — long, detailed, mechanics-driven.
| Agent | Role | Fallback Chain | Notes |
| ------------ | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Sisyphus** | Main orchestrator | Claude Opus → opencode-go/kimi-k2.5 → K2P5 → GPT-5.4 → GLM-5 → Big Pickle | Claude-family first. GPT-5.4 has dedicated prompt support. Kimi/GLM as intermediate fallbacks. |
| **Metis** | Plan gap analyzer | Claude Opus → opencode-go/glm-5 → K2P5 | Claude preferred. Uses opencode-go for reliable GLM-5 access. |
| **Sisyphus** | Main orchestrator | Claude Opus → opencode-go/kimi-k2.5 → K2P5 → Kimi K2.5 → GPT-5.4 → GLM-5 → Big Pickle | Claude-family first. GPT-5.4 has dedicated prompt support. Kimi available through multiple providers. |
| **Metis** | Plan gap analyzer | Claude Opus → GPT-5.4 → opencode-go/glm-5 → K2P5 | Claude preferred. GPT-5.4 as secondary before GLM-5 fallback. |
### Dual-Prompt Agents → Claude preferred, GPT supported
@@ -74,7 +74,7 @@ These agents ship separate prompts for Claude and GPT families. They auto-detect
| Agent | Role | Fallback Chain | Notes |
| -------------- | ----------------- | -------------------------------------- | -------------------------------------------------------------------- |
| **Prometheus** | Strategic planner | Claude Opus → GPT-5.4 → opencode-go/glm-5 → Gemini 3.1 Pro | Interview-mode planning. GPT prompt is compact and principle-driven. |
| **Atlas** | Todo orchestrator | Claude Sonnet → opencode-go/kimi-k2.5 | Claude first, opencode-go as the current fallback path. |
| **Atlas** | Todo orchestrator | Claude Sonnet → opencode-go/kimi-k2.5 → GPT-5.4 | Claude first, opencode-go as intermediate, GPT-5.4 as last resort. |
### Deep Specialists → GPT
@@ -82,9 +82,9 @@ These agents are built for GPT's principle-driven style. Their prompts assume au
| Agent | Role | Fallback Chain | Notes |
| -------------- | ----------------------- | -------------------------------------- | ------------------------------------------------ |
| **Hephaestus** | Autonomous deep worker | GPT-5.3 Codex only | No fallback. Requires GPT access. The craftsman. |
| **Oracle** | Architecture consultant | GPT-5.4 → Gemini 3.1 Pro → Claude Opus | Read-only high-IQ consultation. |
| **Momus** | Ruthless reviewer | GPT-5.4 → Claude Opus → Gemini 3.1 Pro | Verification and plan review. |
| **Hephaestus** | Autonomous deep worker | GPT-5.3 Codex → GPT-5.4 (Copilot) | Requires GPT access. GPT-5.4 via Copilot as fallback. The craftsman. |
| **Oracle** | Architecture consultant | GPT-5.4 → Gemini 3.1 Pro → Claude Opus → opencode-go/glm-5 | Read-only high-IQ consultation. |
| **Momus** | Ruthless reviewer | GPT-5.4 → Claude Opus → Gemini 3.1 Pro → opencode-go/glm-5 | Verification and plan review. GPT-5.4 uses xhigh variant. |
### Utility Runners → Speed over Intelligence
@@ -95,6 +95,7 @@ These agents do grep, search, and retrieval. They intentionally use the fastest,
| **Explore** | Fast codebase grep | Grok Code Fast → opencode-go/minimax-m2.5 → MiniMax Free → Haiku → GPT-5-Nano | Speed is everything. Fire 10 in parallel. |
| **Librarian** | Docs/code search | opencode-go/minimax-m2.5 → MiniMax Free → Haiku → GPT-5-Nano | Doc retrieval doesn't need deep reasoning. |
| **Multimodal Looker** | Vision/screenshots | GPT-5.4 → opencode-go/kimi-k2.5 → GLM-4.6v → GPT-5-Nano | Uses the first available multimodal-capable fallback. |
| **Sisyphus-Junior** | Category executor | Claude Sonnet → opencode-go/kimi-k2.5 → GPT-5.4 → Big Pickle | Handles delegated category tasks. Sonnet-tier default. |
---
@@ -119,8 +120,8 @@ Principle-driven, explicit reasoning, deep technical capability. Best for agents
| Model | Strengths |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| **GPT-5.3 Codex** | Deep coding powerhouse. Autonomous exploration. Required for Hephaestus. |
| **GPT-5.4** | High intelligence, strategic reasoning. Default for Oracle. |
| **GPT-5.4** | Strong principle-driven reasoning. Default for Momus and a key fallback for Prometheus / Atlas. |
| **GPT-5.4** | High intelligence, strategic reasoning. Default for Oracle, Momus, and a key fallback for Prometheus / Atlas. Uses xhigh variant for Momus. |
| **GPT-5.4 Mini** | Fast + strong reasoning. Good for lightweight autonomous tasks. Default for quick category. |
| **GPT-5-Nano** | Ultra-cheap, fast. Good for simple utility tasks. |
### Other Models
@@ -166,14 +167,14 @@ When agents delegate work, they don't pick a model name — they pick a **catego
| Category | When Used | Fallback Chain |
| -------------------- | -------------------------- | -------------------------------------------- |
| `visual-engineering` | Frontend, UI, CSS, design | Gemini 3.1 Pro → GLM 5 → Claude Opus |
| `ultrabrain` | Maximum reasoning needed | GPT-5.4 → Gemini 3.1 Pro → Claude Opus |
| `visual-engineering` | Frontend, UI, CSS, design | Gemini 3.1 Pro → GLM 5 → Claude Opus → opencode-go/glm-5 → K2P5 |
| `ultrabrain` | Maximum reasoning needed | GPT-5.4 → Gemini 3.1 Pro → Claude Opus → opencode-go/glm-5 |
| `deep` | Deep coding, complex logic | GPT-5.3 Codex → Claude Opus → Gemini 3.1 Pro |
| `artistry` | Creative, novel approaches | Gemini 3.1 Pro → Claude Opus → GPT-5.4 |
| `quick` | Simple, fast tasks | Claude Haiku → Gemini Flash → GPT-5-Nano |
| `unspecified-high` | General complex work | Claude Opus → GPT-5.4 (high) → GLM 5 → K2P5 |
| `unspecified-low` | General standard work | Claude Sonnet → GPT-5.3 Codex → Gemini Flash |
| `writing` | Text, docs, prose | Gemini Flash → Claude Sonnet |
| `quick` | Simple, fast tasks | GPT-5.4 Mini → Claude Haiku → Gemini Flash → opencode-go/minimax-m2.5 → GPT-5-Nano |
| `unspecified-high` | General complex work | Claude Opus → GPT-5.4 → GLM 5 → K2P5 → opencode-go/glm-5 → Kimi K2.5 |
| `unspecified-low` | General standard work | Claude Sonnet → GPT-5.3 Codex → opencode-go/kimi-k2.5 → Gemini Flash |
| `writing` | Text, docs, prose | Gemini Flash → opencode-go/kimi-k2.5 → Claude Sonnet |
See the [Orchestration System Guide](./orchestration.md) for how agents dispatch tasks to categories.
@@ -185,7 +186,7 @@ See the [Orchestration System Guide](./orchestration.md) for how agents dispatch
```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-openagent.schema.json",
"agents": {
// Main orchestrator: Claude Opus or Kimi K2.5 work best

View File

@@ -156,7 +156,7 @@ Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/
##### oh-my-openagent Agent Model Override
The `opencode-antigravity-auth` plugin uses different model names than the built-in Google auth. Override the agent models in `oh-my-opencode.json` (or `.opencode/oh-my-opencode.json`):
The `opencode-antigravity-auth` plugin uses different model names than the built-in Google auth. Override the agent models in `oh-my-openagent.json` (or `.opencode/oh-my-openagent.json`):
```json
{
@@ -176,7 +176,7 @@ The `opencode-antigravity-auth` plugin uses different model names than the built
**Available models (Gemini CLI quota)**:
- `google/gemini-2.5-flash`, `google/gemini-2.5-pro`, `google/gemini-3-flash-preview`, `google/gemini-3-pro-preview`
- `google/gemini-2.5-flash`, `google/gemini-2.5-pro`, `google/gemini-3-flash-preview`, `google/gemini-3.1-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.
@@ -287,13 +287,14 @@ Not all models behave the same way. Understanding which models are "similar" hel
| ----------------- | -------------------------------- | ------------------------------------------------- |
| **GPT-5.3-codex** | openai, github-copilot, opencode | Deep coding powerhouse. Required for Hephaestus. |
| **GPT-5.4** | openai, github-copilot, opencode | High intelligence. Default for Oracle. |
| **GPT-5.4 Mini** | openai, github-copilot, opencode | Fast + strong reasoning. Default for quick category. |
| **GPT-5-Nano** | opencode | Ultra-cheap, fast. Good for simple utility tasks. |
**Different-Behavior Models**:
| Model | Provider(s) | Notes |
| --------------------- | -------------------------------- | ----------------------------------------------------------- |
| **Gemini 3 Pro** | google, github-copilot, opencode | Excels at visual/frontend tasks. Different reasoning style. |
| **Gemini 3.1 Pro** | google, github-copilot, opencode | Excels at visual/frontend tasks. Different reasoning style. |
| **Gemini 3 Flash** | google, github-copilot, opencode | Fast, good for doc search and light tasks. |
| **MiniMax M2.5** | venice | Fast and smart. Good for utility tasks. |
| **MiniMax M2.5 Free** | opencode | Free-tier MiniMax. Fast for search/retrieval. |
@@ -316,7 +317,7 @@ Based on your subscriptions, here's how the agents were configured:
| Agent | Role | Default Chain | What It Does |
| ------------ | ---------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Sisyphus** | Main ultraworker | Opus (max) → Kimi K2.5 → GLM 5 → Big Pickle | Primary coding agent. Orchestrates everything. **Never use GPT — no GPT prompt exists.** |
| **Metis** | Plan review | Opus (max) → Kimi K2.5 → GPT-5.4 → Gemini 3 Pro | Reviews Prometheus plans for gaps. |
| **Metis** | Plan review | Opus (max) → Kimi K2.5 → GPT-5.4 → Gemini 3.1 Pro | Reviews Prometheus plans for gaps. |
**Dual-Prompt Agents** (auto-switch between Claude and GPT prompts):
@@ -326,7 +327,7 @@ Priority: **Claude > GPT > Claude-like models**
| Agent | Role | Default Chain | GPT Prompt? |
| -------------- | ----------------- | ---------------------------------------------------------- | ---------------------------------------------------------------- |
| **Prometheus** | Strategic planner | Opus (max) → **GPT-5.4 (high)** → Kimi K2.5 → Gemini 3 Pro | Yes — XML-tagged, principle-driven (~300 lines vs ~1,100 Claude) |
| **Prometheus** | Strategic planner | Opus (max) → **GPT-5.4 (high)** → Kimi K2.5 → Gemini 3.1 Pro | Yes — XML-tagged, principle-driven (~300 lines vs ~1,100 Claude) |
| **Atlas** | Todo orchestrator | **Kimi K2.5** → Sonnet → GPT-5.4 | Yes — GPT-optimized todo management |
**GPT-Native Agents** (built for GPT, don't override to Claude):
@@ -334,8 +335,8 @@ Priority: **Claude > GPT > Claude-like models**
| Agent | Role | Default Chain | Notes |
| -------------- | ---------------------- | -------------------------------------- | ------------------------------------------------------ |
| **Hephaestus** | Deep autonomous worker | GPT-5.3-codex (medium) only | "Codex on steroids." No fallback. Requires GPT access. |
| **Oracle** | Architecture/debugging | GPT-5.4 (high) → Gemini 3 Pro → Opus | High-IQ strategic backup. GPT preferred. |
| **Momus** | High-accuracy reviewer | GPT-5.4 (medium) → Opus → Gemini 3 Pro | Verification agent. GPT preferred. |
| **Oracle** | Architecture/debugging | GPT-5.4 (high) → Gemini 3.1 Pro → Opus | High-IQ strategic backup. GPT preferred. |
| **Momus** | High-accuracy reviewer | GPT-5.4 (medium) → Opus → Gemini 3.1 Pro | Verification agent. GPT preferred. |
**Utility Agents** (speed over intelligence):
@@ -344,8 +345,8 @@ These agents do search, grep, and retrieval. They intentionally use fast, cheap
| Agent | Role | Default Chain | Design Rationale |
| --------------------- | ------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Explore** | Fast codebase grep | MiniMax M2.5 Free → Grok Code Fast → MiniMax M2.5 → Haiku → GPT-5-Nano | Speed is everything. Grok is blazing fast for grep. |
| **Librarian** | Docs/code search | MiniMax M2.5 → MiniMax Free → Haiku → Nano | Fast, cheap models for search. |
| **Multimodal Looker** | Vision/screenshots | GPT-5.4 → Kimi K2.5 → GLM-4.6v → GPT-5-Nano | Strong vision capabilities. |
| **Librarian** | Docs/code search | MiniMax M2.5 Free → Gemini Flash → Big Pickle | Entirely free-tier. Doc retrieval doesn't need deep reasoning. |
| **Multimodal Looker** | Vision/screenshots | Kimi K2.5 → Kimi Free → Gemini Flash → GPT-5.4 → GLM-4.6v | Kimi excels at multimodal understanding. |
#### Why Different Models Need Different Prompts
@@ -364,7 +365,7 @@ This is why Prometheus and Atlas ship separate prompts per model family — they
#### Custom Model Configuration
If the user wants to override which model an agent uses, you can customize in `oh-my-opencode.json`:
If the user wants to override which model an agent uses, you can customize in `oh-my-openagent.json`:
```jsonc
{

View File

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

View File

@@ -41,7 +41,7 @@ We used to call this "Claude Code on steroids." That was wrong.
This isn't about making Claude Code better. It's about breaking free from the idea that one model, one provider, one way of working is enough. Anthropic wants you locked in. OpenAI wants you locked in. Everyone wants you locked in.
Oh My OpenAgent doesn't play that game. It orchestrates across models, picking the right brain for the right job. Claude for orchestration. GPT for deep reasoning. Gemini for frontend. Haiku for quick tasks. All working together, automatically.
Oh My OpenAgent doesn't play that game. It orchestrates across models, picking the right brain for the right job. Claude for orchestration. GPT for deep reasoning. Gemini for frontend. GPT-5.4 Mini for quick tasks. All working together, automatically.
---
@@ -60,11 +60,10 @@ User Request
├─→ [Prometheus] — Strategic planning (interview mode)
├─→ [Atlas] — Todo orchestration and execution
├─→ [Specialist Subagents]
├─→ [Oracle] — Architecture consultation
├─→ [Librarian] — Documentation/code search
└─→ [Explore] — Fast codebase grep
└─→ [Sisyphus-Junior] — Category-based executor
├─→ [Oracle] — Architecture consultation
├─→ [Librarian] — Documentation/code search
├─→ [Explore] — Fast codebase grep
└─→ [Category-based agents] — Specialized by task type
```
When Sisyphus delegates to a subagent, it doesn't pick a model name. It picks a **category**`visual-engineering`, `ultrabrain`, `quick`, `deep`. The category automatically maps to the right model. You touch nothing.
@@ -100,9 +99,9 @@ Use Hephaestus when you need deep architectural reasoning, complex debugging acr
**Why this beats vanilla Codex CLI:**
- **Multi-model orchestration.** Pure Codex is single-model. OmO routes different tasks to different models automatically. GPT for deep reasoning. Gemini for frontend. Haiku for speed. The right brain for the right job.
- **Multi-model orchestration.** Pure Codex is single-model. OmO routes different tasks to different models automatically. GPT for deep reasoning. Gemini for frontend. GPT-5.4 Mini for speed. The right brain for the right job.
- **Background agents.** Fire 5+ agents in parallel. Something Codex simply cannot do. While one agent writes code, another researches patterns, another checks documentation. Like a real dev team.
- **Category system.** Tasks are routed by intent, not model name. `visual-engineering` gets Gemini. `ultrabrain` gets GPT-5.4. `quick` gets Haiku. No manual juggling.
- **Category system.** Tasks are routed by intent, not model name. `visual-engineering` gets Gemini. `ultrabrain` gets GPT-5.4. `quick` gets GPT-5.4 Mini. No manual juggling.
- **Accumulated wisdom.** Subagents learn from previous results. Conventions discovered in task 1 are passed to task 5. Mistakes made early aren't repeated. The system gets smarter as it works.
### Prometheus: The Strategic Planner
@@ -117,20 +116,17 @@ Atlas executes Prometheus plans. Distributes tasks to specialized subagents. Acc
Run `/start-work` to activate Atlas on your latest plan.
### Sisyphus-Junior: The Specialist
### Oracle: The Consultant
When Sisyphus delegates a task via a specific **Category** (like `visual-engineering` or `deep`), **Sisyphus-Junior** is the agent that performs it. It is optimized for focused execution within a specific domain and cannot re-delegate, preventing infinite loops.
Read-only high-IQ consultant for architecture decisions and complex debugging. Consult Oracle when facing unfamiliar patterns, security concerns, or multi-system tradeoffs.
### Specialist Subagents
### Supporting Cast
These agents are primarily designed to be called by other agents or for specific queries, rather than managing a full workflow.
- **Oracle** — Read-only high-IQ consultant for architecture decisions and complex debugging.
- **Librarian** — Documentation and OSS code search. Stays current on library APIs and best practices.
- **Explore** — Fast codebase grep. Uses speed-focused models for pattern discovery.
- **Multimodal Looker** — Vision and screenshot analysis.
- **Metis** — Gap analyzer. Catches what Prometheus missed before plans are finalized.
- **Momus** — Ruthless reviewer. Validates plans against clarity, verification, and context criteria.
- **Explore** — Fast codebase grep. Uses speed-focused models for pattern discovery.
- **Librarian** — Documentation and OSS code search. Stays current on library APIs and best practices.
- **Multimodal Looker** — Vision and screenshot analysis.
---
@@ -172,7 +168,7 @@ You can override specific agents or categories in your config:
```jsonc
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-openagent.schema.json",
"agents": {
// Main orchestrator: Claude Opus or Kimi K2.5 work best
@@ -199,8 +195,8 @@ You can override specific agents or categories in your config:
// General high-effort work
"unspecified-high": { "model": "anthropic/claude-opus-4-6", "variant": "max" },
// Quick tasks: use the cheapest models
"quick": { "model": "anthropic/claude-haiku-4-5" },
// Quick tasks: use GPT-5.4-mini (fast and cheap)
"quick": { "model": "openai/gpt-5.4-mini" },
// Deep reasoning: GPT-5.4
"ultrabrain": { "model": "openai/gpt-5.4", "variant": "xhigh" },
@@ -224,7 +220,7 @@ You can override specific agents or categories in your config:
**Different-behavior models**:
- Gemini 3 Pro — excels at visual/frontend tasks
- Gemini 3.1 Pro — excels at visual/frontend tasks
- MiniMax M2.5 — fast and smart for utility tasks
- Grok Code Fast 1 — optimized for code grep/search

View File

@@ -27,7 +27,7 @@ npx oh-my-openagent
## install
Interactive installation tool for initial Oh-My-OpenCode setup. Provides a TUI based on `@clack/prompts`.
Interactive installation tool for initial Oh-My-OpenAgent setup. Provides a TUI based on `@clack/prompts`.
### Usage
@@ -39,7 +39,7 @@ bunx oh-my-openagent install
1. **Provider Selection**: Choose your AI provider (Claude, ChatGPT, or Gemini)
2. **API Key Input**: Enter the API key for your selected provider
3. **Configuration File Creation**: Generates `opencode.json` or `oh-my-opencode.json` files
3. **Configuration File Creation**: Generates `opencode.json` or `oh-my-openagent.json` files
4. **Plugin Registration**: Automatically registers the oh-my-openagent plugin in OpenCode settings
### Options
@@ -53,7 +53,7 @@ bunx oh-my-openagent install
## doctor
Diagnoses your environment to ensure Oh-My-OpenCode is functioning correctly. Performs 17+ health checks.
Diagnoses your environment to ensure Oh-My-OpenAgent is functioning correctly. Performs 17+ health checks.
### Usage
@@ -86,7 +86,7 @@ bunx oh-my-openagent doctor
oh-my-openagent doctor
┌──────────────────────────────────────────────────┐
│ Oh-My-OpenCode Doctor │
│ Oh-My-OpenAgent Doctor │
└──────────────────────────────────────────────────┘
Installation
@@ -94,7 +94,7 @@ Installation
✓ Plugin registered in opencode.json
Configuration
✓ oh-my-opencode.json is valid
✓ oh-my-openagent.json is valid
⚠ categories.visual-engineering: using default model
Authentication
@@ -178,8 +178,8 @@ Tokens are stored in `~/.config/opencode/mcp-oauth.json` with `0600` permissions
The CLI searches for configuration files in the following locations (in priority order):
1. **Project Level**: `.opencode/oh-my-opencode.json`
2. **User Level**: `~/.config/opencode/oh-my-opencode.json`
1. **Project Level**: `.opencode/oh-my-openagent.json`
2. **User Level**: `~/.config/opencode/oh-my-openagent.json`
### JSONC Support

View File

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

View File

@@ -1,29 +1,34 @@
# Oh-My-OpenCode Features Reference
# Oh-My-OpenAgent Features Reference
## Agents
Oh-My-OpenCode provides 11 specialized AI agents. Each has distinct expertise, optimized models, and tool permissions.
Oh-My-OpenAgent provides 11 specialized AI agents. Each has distinct expertise, optimized models, and tool permissions.
### Primary Agents
### Core Agents
| Agent | Model | Purpose |
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sisyphus** | `claude-opus-4-6` | The default orchestrator. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: `kimi-k2.5``glm-5`. |
| **Hephaestus** | `gpt-5.3-codex` | The Legitimate Craftsman. Autonomous deep worker. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end. Fallback: `gpt-5.4` on GitHub Copilot. Requires a GPT-capable provider. |
| **Prometheus** | `claude-opus-4-6` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: `gpt-5.4``gemini-3.1-pro`. |
| **Atlas** | `claude-sonnet-4-6`| Executor. Takes the plan from Prometheus and drives it to completion, managing the todo list and coordinating subagents. Fallback: `gpt-5.4` (medium). |
| **Sisyphus-Junior** | _(category-dependent)_ | Category-spawned executor. Model is selected automatically based on the task category. Used when the main agent delegates work via the `task` tool. |
| **Sisyphus** | `claude-opus-4-6` | The default orchestrator. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: `glm-5``big-pickle`. |
| **Hephaestus** | `gpt-5.3-codex` | The Legitimate Craftsman. Autonomous deep worker inspired by AmpCode's deep mode. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end without premature stopping. Named after the Greek god of forge and craftsmanship. Fallback: `gpt-5.4` on GitHub Copilot. Requires a GPT-capable provider. |
| **Oracle** | `gpt-5.4` | Architecture decisions, code review, debugging. Read-only consultation with stellar logical reasoning and deep analysis. Inspired by AmpCode. Fallback: `gemini-3.1-pro``claude-opus-4-6`. |
| **Librarian** | `gemini-3-flash` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: `minimax-m2.5-free``big-pickle`. |
| **Explore** | `grok-code-fast-1` | Fast codebase exploration and contextual grep. Fallback: `minimax-m2.5-free``claude-haiku-4-5``gpt-5-nano`. |
| **Multimodal-Looker** | `gpt-5.3-codex` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Fallback: `k2p5``gemini-3-flash``glm-4.6v``gpt-5-nano`. |
### Specialist Subagents
### Planning Agents
| Agent | Model | Purpose |
| --------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Oracle** | `gpt-5.4` | Architecture decisions, code review, debugging. Read-only consultation. Fallback: `gemini-3.1-pro``claude-opus-4-6`. |
| **Librarian** | `minimax-m2.5` | Multi-repo analysis, documentation lookup, OSS implementation examples. Fallback: `minimax-m2.5-free``claude-haiku-4-5``gpt-5-nano`. |
| **Explore** | `grok-code-fast-1` | Fast codebase exploration and contextual grep. Fallback: `minimax-m2.5``minimax-m2.5-free``claude-haiku-4-5`. |
| **Multimodal-Looker** | `gpt-5.4` | Visual content specialist. Analyzes PDFs, images, diagrams. Fallback: `kimi-k2.5``glm-4.6v``gpt-5-nano`. |
| **Metis** | `claude-opus-4-6` | Plan consultant — pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: `gpt-5.4``gemini-3.1-pro`. |
| **Momus** | `gpt-5.4` | Plan reviewer — validates plans against clarity, verifiability, and completeness standards. Fallback: `claude-opus-4-6``gemini-3.1-pro`. |
| Agent | Model | Purpose |
| -------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Prometheus** | `claude-opus-4-6` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: `gpt-5.4``gemini-3.1-pro`. |
| **Metis** | `claude-opus-4-6` | Plan consultant — pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: `gpt-5.4``gemini-3.1-pro`. |
| **Momus** | `gpt-5.4` | Plan reviewer — validates plans against clarity, verifiability, and completeness standards. Fallback: `claude-opus-4-6``gemini-3.1-pro`. |
### Orchestration Agents
| Agent | Model | Purpose |
| ------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Atlas** | `claude-sonnet-4-6` | Todo-list orchestrator. Executes planned tasks systematically, managing todo items and coordinating work. Fallback: `gpt-5.4` (medium). |
| **Sisyphus-Junior** | _(category-dependent)_ | Category-spawned executor. Model is selected automatically based on the task category (visual-engineering, quick, deep, etc.). Used when the main agent delegates work via the `task` tool. |
### Invoking Agents
@@ -85,7 +90,7 @@ When running inside tmux:
- Each pane shows agent output live
- Auto-cleanup when agents complete
Customize agent models, prompts, and permissions in `oh-my-opencode.json`.
Customize agent models, prompts, and permissions in `oh-my-openagent.json`.
## Category System
@@ -106,7 +111,7 @@ By combining these two concepts, you can generate optimal agents through `task`.
| `ultrabrain` | `openai/gpt-5.4` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
| `deep` | `openai/gpt-5.3-codex` (medium) | Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding. |
| `artistry` | `google/gemini-3.1-pro` (high) | Highly creative/artistic tasks, novel ideas |
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
| `quick` | `openai/gpt-5.4-mini` | Trivial tasks - single file changes, typo fixes, simple modifications |
| `unspecified-low` | `anthropic/claude-sonnet-4-6` | Tasks that don't fit other categories, low effort required |
| `unspecified-high` | `anthropic/claude-opus-4-6` (max) | Tasks that don't fit other categories, high effort required |
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
@@ -124,7 +129,7 @@ task({
### Custom Categories
You can define custom categories in `oh-my-opencode.json`.
You can define custom categories in `oh-my-openagent.json`.
#### Category Configuration Schema
@@ -232,7 +237,7 @@ Skills provide specialized workflows with embedded MCP servers and detailed inst
### Browser Automation Options
Oh-My-OpenCode provides two browser automation providers, configurable via `browser_automation_engine.provider`.
Oh-My-OpenAgent provides two browser automation providers, configurable via `browser_automation_engine.provider`.
#### Option 1: Playwright MCP (Default)
@@ -675,7 +680,6 @@ Hooks intercept and modify behavior at key points in the agent lifecycle across
| **ralph-loop** | Event + Message | Manages self-referential loop continuation. |
| **start-work** | Message | Handles /start-work command execution. |
| **auto-slash-command** | Message | Automatically executes slash commands from prompts. |
| **gpt-permission-continuation** | Event | Auto-continues GPT sessions when the final assistant reply ends with a permission-seeking tail such as `If you want, ...`. |
| **stop-continuation-guard** | Event + Message | Guards the stop-continuation mechanism. |
| **category-skill-reminder** | Event + PostToolUse | Reminds agents about available category skills for delegation. |
| **anthropic-effort** | Params | Adjusts Anthropic API effort level based on context. |
@@ -730,7 +734,6 @@ Hooks intercept and modify behavior at key points in the agent lifecycle across
| Hook | Event | Description |
| ------------------------------ | ----- | ---------------------------------------------------------- |
| **gpt-permission-continuation** | Event | Continues GPT replies that end in a permission-seeking tail. |
| **todo-continuation-enforcer** | Event | Enforces todo completion — yanks idle agents back to work. |
| **compaction-todo-preserver** | Event | Preserves todo state during session compaction. |
| **unstable-agent-babysitter** | Event | Handles unstable agent behavior with recovery strategies. |
@@ -782,12 +785,10 @@ Disable specific hooks in config:
```json
{
"disabled_hooks": ["comment-checker", "gpt-permission-continuation"]
"disabled_hooks": ["comment-checker"]
}
```
Use `gpt-permission-continuation` when you want GPT sessions to stop at permission-seeking endings instead of auto-resuming.
## MCPs
### Built-in MCPs

View File

@@ -1,12 +1,12 @@
{
"name": "oh-my-openagent",
"name": "oh-my-opencode",
"version": "3.11.0",
"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",
"type": "module",
"bin": {
"oh-my-openagent": "bin/oh-my-opencode.js"
"oh-my-opencode": "bin/oh-my-opencode.js"
},
"files": [
"dist",
@@ -76,17 +76,17 @@
"typescript": "^5.7.3"
},
"optionalDependencies": {
"oh-my-openagent-darwin-arm64": "3.11.0",
"oh-my-openagent-darwin-x64": "3.11.0",
"oh-my-openagent-darwin-x64-baseline": "3.11.0",
"oh-my-openagent-linux-arm64": "3.11.0",
"oh-my-openagent-linux-arm64-musl": "3.11.0",
"oh-my-openagent-linux-x64": "3.11.0",
"oh-my-openagent-linux-x64-baseline": "3.11.0",
"oh-my-openagent-linux-x64-musl": "3.11.0",
"oh-my-openagent-linux-x64-musl-baseline": "3.11.0",
"oh-my-openagent-windows-x64": "3.11.0",
"oh-my-openagent-windows-x64-baseline": "3.11.0"
"oh-my-opencode-darwin-arm64": "3.11.0",
"oh-my-opencode-darwin-x64": "3.11.0",
"oh-my-opencode-darwin-x64-baseline": "3.11.0",
"oh-my-opencode-linux-arm64": "3.11.0",
"oh-my-opencode-linux-arm64-musl": "3.11.0",
"oh-my-opencode-linux-x64": "3.11.0",
"oh-my-opencode-linux-x64-baseline": "3.11.0",
"oh-my-opencode-linux-x64-musl": "3.11.0",
"oh-my-opencode-linux-x64-musl-baseline": "3.11.0",
"oh-my-opencode-windows-x64": "3.11.0",
"oh-my-opencode-windows-x64-baseline": "3.11.0"
},
"overrides": {
"@opencode-ai/sdk": "^1.2.24"

View File

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

View File

@@ -2207,6 +2207,102 @@
"created_at": "2026-03-16T04:55:10Z",
"repoId": 1108837393,
"pullRequestNo": 2604
},
{
"name": "gxlife",
"id": 110413359,
"comment_id": 4068427047,
"created_at": "2026-03-16T15:17:01Z",
"repoId": 1108837393,
"pullRequestNo": 2625
},
{
"name": "HaD0Yun",
"id": 102889891,
"comment_id": 4073195308,
"created_at": "2026-03-17T08:27:45Z",
"repoId": 1108837393,
"pullRequestNo": 2640
},
{
"name": "tad-hq",
"id": 213478119,
"comment_id": 4077697128,
"created_at": "2026-03-17T20:07:09Z",
"repoId": 1108837393,
"pullRequestNo": 2655
},
{
"name": "ogormans-deptstack",
"id": 208788555,
"comment_id": 4077893096,
"created_at": "2026-03-17T20:42:42Z",
"repoId": 1108837393,
"pullRequestNo": 2656
},
{
"name": "walioo",
"id": 25835823,
"comment_id": 4087098221,
"created_at": "2026-03-19T02:13:02Z",
"repoId": 1108837393,
"pullRequestNo": 2688
},
{
"name": "trafgals",
"id": 6454757,
"comment_id": 4087725932,
"created_at": "2026-03-19T04:22:32Z",
"repoId": 1108837393,
"pullRequestNo": 2690
},
{
"name": "tonymfer",
"id": 66512584,
"comment_id": 4091847232,
"created_at": "2026-03-19T17:13:49Z",
"repoId": 1108837393,
"pullRequestNo": 2701
},
{
"name": "nguyentamdat",
"id": 16253213,
"comment_id": 4096267323,
"created_at": "2026-03-20T07:34:22Z",
"repoId": 1108837393,
"pullRequestNo": 2718
},
{
"name": "whackur",
"id": 26926041,
"comment_id": 4102330445,
"created_at": "2026-03-21T05:27:17Z",
"repoId": 1108837393,
"pullRequestNo": 2733
},
{
"name": "ndaemy",
"id": 18691542,
"comment_id": 4103008804,
"created_at": "2026-03-21T10:18:22Z",
"repoId": 1108837393,
"pullRequestNo": 2734
},
{
"name": "0xYiliu",
"id": 3838688,
"comment_id": 4104738337,
"created_at": "2026-03-21T22:59:33Z",
"repoId": 1108837393,
"pullRequestNo": 2738
},
{
"name": "hunghoang3011",
"id": 65234777,
"comment_id": 4107900881,
"created_at": "2026-03-23T04:28:20Z",
"repoId": 1108837393,
"pullRequestNo": 2758
}
]
}

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ export function collectPendingBuiltinAgents(input: {
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
let resolution = applyModelResolution({
uiSelectedModel: (isPrimaryAgent && !override?.model) ? uiSelectedModel : undefined,
uiSelectedModel: (isPrimaryAgent && override?.model === undefined) ? uiSelectedModel : undefined,
userModel: override?.model,
requirement,
availableModels,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,60 +5,60 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"hephaestus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"librarian": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"metis": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"momus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"multimodal-looker": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"oracle": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"prometheus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"sisyphus-junior": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
"categories": {
"artistry": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"deep": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"quick": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"ultrabrain": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"unspecified-high": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"unspecified-low": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"visual-engineering": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"writing": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
}
@@ -83,7 +83,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
"variant": "max",
},
"multimodal-looker": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"oracle": {
"model": "anthropic/claude-opus-4-6",
@@ -145,7 +145,7 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
"variant": "max",
},
"multimodal-looker": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"oracle": {
"model": "anthropic/claude-opus-4-6",
@@ -248,8 +248,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.3-codex",
"variant": "low",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -334,8 +333,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
"variant": "medium",
},
"quick": {
"model": "openai/gpt-5.3-codex",
"variant": "low",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -366,20 +364,20 @@ exports[`generateModelConfig single native provider uses Gemini models when only
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
"metis": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"momus": {
"model": "google/gemini-3.1-pro-preview",
"variant": "high",
},
"multimodal-looker": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"oracle": {
"model": "google/gemini-3.1-pro-preview",
@@ -389,7 +387,7 @@ exports[`generateModelConfig single native provider uses Gemini models when only
"model": "google/gemini-3.1-pro-preview",
},
"sisyphus-junior": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
"categories": {
@@ -426,20 +424,20 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
},
"metis": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"momus": {
"model": "google/gemini-3.1-pro-preview",
"variant": "high",
},
"multimodal-looker": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"oracle": {
"model": "google/gemini-3.1-pro-preview",
@@ -449,7 +447,7 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
"model": "google/gemini-3.1-pro-preview",
},
"sisyphus-junior": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
"categories": {
@@ -465,7 +463,7 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
"variant": "high",
},
"unspecified-high": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"unspecified-low": {
"model": "google/gemini-3-flash-preview",
@@ -533,7 +531,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
"variant": "medium",
},
"quick": {
"model": "anthropic/claude-haiku-4-5",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -608,7 +606,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
"variant": "medium",
},
"quick": {
"model": "anthropic/claude-haiku-4-5",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -684,7 +682,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
"variant": "medium",
},
"quick": {
"model": "opencode/claude-haiku-4-5",
"model": "opencode/gpt-5.4-mini",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -759,7 +757,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
"variant": "medium",
},
"quick": {
"model": "opencode/claude-haiku-4-5",
"model": "opencode/gpt-5.4-mini",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -830,7 +828,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
"variant": "high",
},
"quick": {
"model": "github-copilot/claude-haiku-4.5",
"model": "github-copilot/gpt-5.4-mini",
},
"ultrabrain": {
"model": "github-copilot/gemini-3.1-pro-preview",
@@ -900,7 +898,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
"variant": "high",
},
"quick": {
"model": "github-copilot/claude-haiku-4.5",
"model": "github-copilot/gpt-5.4-mini",
},
"ultrabrain": {
"model": "github-copilot/gemini-3.1-pro-preview",
@@ -929,7 +927,7 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
@@ -938,45 +936,45 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
"model": "zai-coding-plan/glm-4.7",
},
"metis": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"momus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"multimodal-looker": {
"model": "zai-coding-plan/glm-4.6v",
},
"oracle": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"prometheus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"sisyphus": {
"model": "zai-coding-plan/glm-5",
},
"sisyphus-junior": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
"categories": {
"quick": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"ultrabrain": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"unspecified-high": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"unspecified-low": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"visual-engineering": {
"model": "zai-coding-plan/glm-5",
},
"writing": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
}
@@ -987,7 +985,7 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json",
"agents": {
"atlas": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"explore": {
"model": "opencode/gpt-5-nano",
@@ -996,45 +994,45 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
"model": "zai-coding-plan/glm-4.7",
},
"metis": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"momus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"multimodal-looker": {
"model": "zai-coding-plan/glm-4.6v",
},
"oracle": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"prometheus": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"sisyphus": {
"model": "zai-coding-plan/glm-5",
},
"sisyphus-junior": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
"categories": {
"quick": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"ultrabrain": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"unspecified-high": {
"model": "zai-coding-plan/glm-5",
},
"unspecified-low": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"visual-engineering": {
"model": "zai-coding-plan/glm-5",
},
"writing": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
},
}
@@ -1092,7 +1090,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
"variant": "medium",
},
"quick": {
"model": "anthropic/claude-haiku-4-5",
"model": "opencode/gpt-5.4-mini",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -1167,7 +1165,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
"variant": "medium",
},
"quick": {
"model": "github-copilot/claude-haiku-4.5",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -1273,7 +1271,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
"variant": "max",
},
"multimodal-looker": {
"model": "opencode/glm-4.7-free",
"model": "opencode/gpt-5-nano",
},
"oracle": {
"model": "google/gemini-3.1-pro-preview",
@@ -1375,7 +1373,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
"variant": "medium",
},
"quick": {
"model": "github-copilot/claude-haiku-4.5",
"model": "github-copilot/gpt-5.4-mini",
},
"ultrabrain": {
"model": "opencode/gpt-5.4",
@@ -1453,7 +1451,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
"variant": "medium",
},
"quick": {
"model": "anthropic/claude-haiku-4-5",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",
@@ -1531,7 +1529,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
"variant": "medium",
},
"quick": {
"model": "anthropic/claude-haiku-4-5",
"model": "openai/gpt-5.4-mini",
},
"ultrabrain": {
"model": "openai/gpt-5.4",

View File

@@ -42,7 +42,7 @@ Examples:
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
Claude Native anthropic/ models (Opus, Sonnet, Haiku)
OpenAI Native openai/ models (GPT-5.4 for Oracle)
Gemini Native google/ models (Gemini 3 Pro, Flash)
Gemini Native google/ models (Gemini 3.1 Pro, Flash)
Copilot github-copilot/ models (fallback)
OpenCode Zen opencode/ models (opencode/claude-opus-4-6, etc.)
Z.ai zai-coding-plan/glm-5 (visual-engineering fallback)

View File

@@ -10,7 +10,7 @@
| File | Purpose |
|------|---------|
| `add-plugin-to-opencode-config.ts` | Register `oh-my-openagent` in `.opencode/opencode.json` plugin array |
| `add-plugin-to-opencode-config.ts` | Register `oh-my-opencode` in `.opencode/opencode.json` plugin array |
| `add-provider-config.ts` | Add provider API key to OpenCode config (user-level) |
| `antigravity-provider-configuration.ts` | Handle Antigravity provider setup (special case) |
| `auth-plugins.ts` | Detect auth plugin requirements per provider (oauth vs key) |
@@ -26,7 +26,7 @@
| `opencode-binary.ts` | Detect OpenCode binary location, verify it's installed |
| `opencode-config-format.ts` | OpenCode config format constants and type guards |
| `parse-opencode-config-file.ts` | Parse opencode.json/opencode.jsonc with fallback |
| `plugin-name-with-version.ts` | Resolve `oh-my-openagent@X.Y.Z` for installation |
| `plugin-name-with-version.ts` | Resolve `oh-my-opencode@X.Y.Z` for installation |
| `write-omo-config.ts` | Write generated config to `.opencode/oh-my-opencode.jsonc` |
## USAGE PATTERN

View File

@@ -1,5 +1,6 @@
import { readFileSync, writeFileSync } from "node:fs"
import type { ConfigMergeResult } from "../types"
import { PLUGIN_NAME, LEGACY_PLUGIN_NAME } from "../../shared"
import { getConfigDir } from "./config-context"
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
@@ -7,8 +8,6 @@ import { detectConfigFormat } from "./opencode-config-format"
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
import { getPluginNameWithVersion } from "./plugin-name-with-version"
const PACKAGE_NAME = "oh-my-opencode"
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
try {
ensureConfigDirectoryExists()
@@ -21,7 +20,7 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
}
const { format, path } = detectConfigFormat()
const pluginEntry = await getPluginNameWithVersion(currentVersion, PACKAGE_NAME)
const pluginEntry = await getPluginNameWithVersion(currentVersion, PLUGIN_NAME)
try {
if (format === "none") {
@@ -41,13 +40,24 @@ export async function addPluginToOpenCodeConfig(currentVersion: string): Promise
const config = parseResult.config
const plugins = config.plugin ?? []
const existingIndex = plugins.findIndex((plugin) => plugin === PACKAGE_NAME || plugin.startsWith(`${PACKAGE_NAME}@`))
if (existingIndex !== -1) {
if (plugins[existingIndex] === pluginEntry) {
// Check for existing plugin (either current or legacy name)
const currentNameIndex = plugins.findIndex(
(plugin) => plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`)
)
const legacyNameIndex = plugins.findIndex(
(plugin) => plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`)
)
// If either name exists, update to new name
if (currentNameIndex !== -1) {
if (plugins[currentNameIndex] === pluginEntry) {
return { success: true, configPath: path }
}
plugins[existingIndex] = pluginEntry
plugins[currentNameIndex] = pluginEntry
} else if (legacyNameIndex !== -1) {
// Upgrade legacy name to new name
plugins[legacyNameIndex] = pluginEntry
} else {
plugins.push(pluginEntry)
}

View File

@@ -11,6 +11,8 @@ type BunInstallOutputMode = "inherit" | "pipe"
interface RunBunInstallOptions {
outputMode?: BunInstallOutputMode
/** Workspace directory to install to. Defaults to cache dir if not provided. */
workspaceDir?: string
}
interface BunInstallOutput {
@@ -65,7 +67,7 @@ function logCapturedOutputOnFailure(outputMode: BunInstallOutputMode, output: Bu
export async function runBunInstallWithDetails(options?: RunBunInstallOptions): Promise<BunInstallResult> {
const outputMode = options?.outputMode ?? "pipe"
const cacheDir = getOpenCodeCacheDir()
const cacheDir = options?.workspaceDir ?? getOpenCodeCacheDir()
const packageJsonPath = `${cacheDir}/package.json`
if (!existsSync(packageJsonPath)) {

View File

@@ -1,5 +1,5 @@
import { existsSync, readFileSync } from "node:fs"
import { parseJsonc } from "../../shared"
import { parseJsonc, LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "../../shared"
import type { DetectedConfig } from "../types"
import { getOmoConfigPath } from "./config-context"
import { detectConfigFormat } from "./opencode-config-format"
@@ -55,8 +55,12 @@ function detectProvidersFromOmoConfig(): {
}
}
function isOurPlugin(plugin: string): boolean {
return plugin === PLUGIN_NAME || plugin.startsWith(`${PLUGIN_NAME}@`) ||
plugin === LEGACY_PLUGIN_NAME || plugin.startsWith(`${LEGACY_PLUGIN_NAME}@`)
}
export function detectCurrentConfig(): DetectedConfig {
const PACKAGE_NAME = "oh-my-opencode"
const result: DetectedConfig = {
isInstalled: false,
hasClaude: true,
@@ -82,7 +86,7 @@ export function detectCurrentConfig(): DetectedConfig {
const openCodeConfig = parseResult.config
const plugins = openCodeConfig.plugin ?? []
result.isInstalled = plugins.some((plugin) => plugin.startsWith(PACKAGE_NAME))
result.isInstalled = plugins.some(isOurPlugin)
if (!result.isInstalled) {
return result

View File

@@ -52,6 +52,30 @@ describe("detectCurrentConfig - single package detection", () => {
expect(result.isInstalled).toBe(true)
})
it("detects oh-my-openagent as installed (legacy name)", () => {
// given
const config = { plugin: ["oh-my-openagent"] }
writeFileSync(testConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
// when
const result = detectCurrentConfig()
// then
expect(result.isInstalled).toBe(true)
})
it("detects oh-my-openagent with version pin as installed (legacy name)", () => {
// given
const config = { plugin: ["oh-my-openagent@3.11.0"] }
writeFileSync(testConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
// when
const result = detectCurrentConfig()
// then
expect(result.isInstalled).toBe(true)
})
it("returns false when plugin not present", () => {
// given
const config = { plugin: ["some-other-plugin"] }
@@ -64,6 +88,18 @@ describe("detectCurrentConfig - single package detection", () => {
expect(result.isInstalled).toBe(false)
})
it("returns false when plugin not present (even with similar name)", () => {
// given - not exactly oh-my-openagent
const config = { plugin: ["oh-my-openagent-extra"] }
writeFileSync(testConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
// when
const result = detectCurrentConfig()
// then
expect(result.isInstalled).toBe(false)
})
it("detects OpenCode Go from the existing omo config", () => {
// given
writeFileSync(testConfigPath, JSON.stringify({ plugin: ["oh-my-opencode"] }, null, 2) + "\n", "utf-8")
@@ -130,6 +166,38 @@ describe("addPluginToOpenCodeConfig - single package writes", () => {
expect(savedConfig.plugin).not.toContain("oh-my-opencode@3.10.0")
})
it("recognizes oh-my-openagent as already installed (legacy name)", async () => {
// given
const config = { plugin: ["oh-my-openagent"] }
writeFileSync(testConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
// when
const result = await addPluginToOpenCodeConfig("3.11.0")
// then
expect(result.success).toBe(true)
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
// Should upgrade to new name
expect(savedConfig.plugin).toContain("oh-my-opencode")
expect(savedConfig.plugin).not.toContain("oh-my-openagent")
})
it("replaces version-pinned oh-my-openagent@X.Y.Z with new name", async () => {
// given
const config = { plugin: ["oh-my-openagent@3.10.0"] }
writeFileSync(testConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8")
// when
const result = await addPluginToOpenCodeConfig("3.11.0")
// then
expect(result.success).toBe(true)
const savedConfig = JSON.parse(readFileSync(testConfigPath, "utf-8"))
// Legacy should be replaced with new name
expect(savedConfig.plugin).toContain("oh-my-opencode")
expect(savedConfig.plugin).not.toContain("oh-my-openagent")
})
it("adds new plugin when none exists", async () => {
// given
const config = {}

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { existsSync, readFileSync } from "node:fs"
import { PACKAGE_NAME } from "../constants"
import { getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
import { LEGACY_PLUGIN_NAME, PLUGIN_NAME, getOpenCodeConfigPaths, parseJsonc } from "../../../shared"
export interface PluginInfo {
registered: boolean
@@ -24,18 +23,33 @@ function detectConfigPath(): string | null {
}
function parsePluginVersion(entry: string): string | null {
if (!entry.startsWith(`${PACKAGE_NAME}@`)) return null
const value = entry.slice(PACKAGE_NAME.length + 1)
if (!value || value === "latest") return null
return value
// Check for current package name
if (entry.startsWith(`${PLUGIN_NAME}@`)) {
const value = entry.slice(PLUGIN_NAME.length + 1)
if (!value || value === "latest") return null
return value
}
// Check for legacy package name
if (entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) {
const value = entry.slice(LEGACY_PLUGIN_NAME.length + 1)
if (!value || value === "latest") return null
return value
}
return null
}
function findPluginEntry(entries: string[]): { entry: string; isLocalDev: boolean } | null {
for (const entry of entries) {
if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`)) {
// Check for current package name
if (entry === PLUGIN_NAME || entry.startsWith(`${PLUGIN_NAME}@`)) {
return { entry, isLocalDev: false }
}
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
// Check for legacy package name
if (entry === LEGACY_PLUGIN_NAME || entry.startsWith(`${LEGACY_PLUGIN_NAME}@`)) {
return { entry, isLocalDev: false }
}
// Check for file:// paths that include either name
if (entry.startsWith("file://") && (entry.includes(PLUGIN_NAME) || entry.includes(LEGACY_PLUGIN_NAME))) {
return { entry, isLocalDev: true }
}
}
@@ -76,7 +90,7 @@ export function getPluginInfo(): PluginInfo {
registered: true,
configPath,
entry: pluginEntry.entry,
isPinned: pinnedVersion !== null && /^\d+\.\d+\.\d+/.test(pinnedVersion),
isPinned: pinnedVersion !== null && /^\d+\.\d+\.\d+/.test(pinnedVersion ?? ""),
pinnedVersion,
isLocalDev: pluginEntry.isLocalDev,
}

View File

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

View File

@@ -19,7 +19,7 @@ export type { GeneratedOmoConfig } from "./model-fallback-types"
const ZAI_MODEL = "zai-coding-plan/glm-4.7"
const ULTIMATE_FALLBACK = "opencode/glm-4.7-free"
const ULTIMATE_FALLBACK = "opencode/gpt-5-nano"
const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json"

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
## OVERVIEW
37 files. Powers the `oh-my-openagent run <message>` command. Connects to OpenCode server, creates/resumes sessions, streams events, and polls for completion.
37 files. Powers the `oh-my-opencode run <message>` command. Connects to OpenCode server, creates/resumes sessions, streams events, and polls for completion.
## EXECUTION FLOW

View File

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

View File

@@ -1,6 +1,6 @@
/// <reference types="bun-types" />
import { describe, it, expect } from "bun:test"
import { describe, it, expect, beforeEach, afterEach, vi } from "bun:test"
import type { OhMyOpenCodeConfig } from "../../config"
import { resolveRunAgent, waitForEventProcessorShutdown } from "./runner"
@@ -83,7 +83,6 @@ describe("resolveRunAgent", () => {
})
describe("waitForEventProcessorShutdown", () => {
it("returns quickly when event processor completes", async () => {
//#given
const eventProcessor = new Promise<void>((resolve) => {
@@ -115,3 +114,80 @@ describe("waitForEventProcessorShutdown", () => {
expect(elapsed).toBeGreaterThanOrEqual(timeoutMs - 10)
})
})
describe("run environment setup", () => {
let originalClient: string | undefined
let originalRunMode: string | undefined
beforeEach(() => {
originalClient = process.env.OPENCODE_CLIENT
originalRunMode = process.env.OPENCODE_CLI_RUN_MODE
})
afterEach(() => {
if (originalClient === undefined) {
delete process.env.OPENCODE_CLIENT
} else {
process.env.OPENCODE_CLIENT = originalClient
}
if (originalRunMode === undefined) {
delete process.env.OPENCODE_CLI_RUN_MODE
} else {
process.env.OPENCODE_CLI_RUN_MODE = originalRunMode
}
})
it("sets OPENCODE_CLIENT to 'run' to exclude question tool from registry", async () => {
//#given
delete process.env.OPENCODE_CLIENT
//#when - run() sets env vars synchronously before any async work
const { run } = await import(`./runner?env-setup-${Date.now()}`)
run({ message: "test" }).catch(() => {})
//#then
expect(String(process.env.OPENCODE_CLIENT)).toBe("run")
expect(String(process.env.OPENCODE_CLI_RUN_MODE)).toBe("true")
})
})
describe("run with invalid model", () => {
it("given invalid --model value, when run, then returns exit code 1 with error message", async () => {
// given
const originalExit = process.exit
const originalError = console.error
const errorMessages: string[] = []
const exitCodes: number[] = []
console.error = (...args: unknown[]) => {
errorMessages.push(args.map(String).join(" "))
}
process.exit = ((code?: number) => {
exitCodes.push(code ?? 0)
throw new Error("exit")
}) as typeof process.exit
try {
// when
// Note: This will actually try to run - but the issue is that resolveRunModel
// is called BEFORE the try block, so it throws an unhandled exception
// We're testing the runner's error handling
const { run } = await import("./runner")
// This will throw because model "invalid" is invalid format
try {
await run({
message: "test",
model: "invalid",
})
} catch {
// Expected to potentially throw due to unhandled model resolution error
}
} finally {
// then - verify error handling
// Currently this will fail because the error is not caught properly
console.error = originalError
process.exit = originalExit
}
})
})

View File

@@ -31,6 +31,7 @@ export async function waitForEventProcessorShutdown(
export async function run(options: RunOptions): Promise<number> {
process.env.OPENCODE_CLI_RUN_MODE = "true"
process.env.OPENCODE_CLIENT = "run"
const startTime = Date.now()
const {
@@ -47,10 +48,11 @@ export async function run(options: RunOptions): Promise<number> {
const pluginConfig = loadPluginConfig(directory, { command: "run" })
const resolvedAgent = resolveRunAgent(options, pluginConfig)
const resolvedModel = resolveRunModel(options.model)
const abortController = new AbortController()
try {
const resolvedModel = resolveRunModel(options.model)
const { client, cleanup: serverCleanup } = await createServerConnection({
port: options.port,
attach: options.attach,

View File

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

View File

@@ -4,17 +4,17 @@
## OVERVIEW
24 schema files composing `OhMyOpenAgentConfigSchema`. Zod v4 validation with `safeParse()`. All fields optional — omitted fields use plugin defaults.
24 schema files composing `OhMyOpenCodeConfigSchema`. Zod v4 validation with `safeParse()`. All fields optional — omitted fields use plugin defaults.
## SCHEMA TREE
```
config/schema/
├── oh-my-openagent-config.ts # ROOT: OhMyOpenAgentConfigSchema (composes all below)
├── oh-my-opencode-config.ts # ROOT: OhMyOpenCodeConfigSchema (composes all below)
├── agent-names.ts # BuiltinAgentNameSchema (11), OverridableAgentNameSchema (14)
├── agent-overrides.ts # AgentOverrideConfigSchema (21 fields per agent)
├── categories.ts # 8 built-in + custom categories
├── hooks.ts # HookNameSchema (46 hooks)
├── hooks.ts # HookNameSchema (48 hooks)
├── skills.ts # SkillsConfigSchema (sources, paths, recursive)
├── commands.ts # BuiltinCommandNameSchema
├── experimental.ts # Feature flags (plugin_load_timeout_ms min 1000)
@@ -49,6 +49,6 @@ config/schema/
## HOW TO ADD CONFIG
1. Create `src/config/schema/{name}.ts` with Zod schema
2. Add field to `oh-my-openagent-config.ts` root schema
2. Add field to `oh-my-opencode-config.ts` root schema
3. Reference via `z.infer<typeof YourSchema>` for TypeScript types
4. Access in handlers via `pluginConfig.{name}`

View File

@@ -0,0 +1,56 @@
import { describe, expect, test } from "bun:test"
import { ZodError } from "zod/v4"
import { BackgroundTaskConfigSchema } from "./background-task"
describe("BackgroundTaskConfigSchema.circuitBreaker", () => {
describe("#given valid circuit breaker settings", () => {
test("#when parsed #then returns nested config", () => {
const result = BackgroundTaskConfigSchema.parse({
circuitBreaker: {
maxToolCalls: 150,
consecutiveThreshold: 10,
},
})
expect(result.circuitBreaker).toEqual({
maxToolCalls: 150,
consecutiveThreshold: 10,
})
})
})
describe("#given consecutiveThreshold below minimum", () => {
test("#when parsed #then throws ZodError", () => {
let thrownError: unknown
try {
BackgroundTaskConfigSchema.parse({
circuitBreaker: {
consecutiveThreshold: 4,
},
})
} catch (error) {
thrownError = error
}
expect(thrownError).toBeInstanceOf(ZodError)
})
})
describe("#given consecutiveThreshold is zero", () => {
test("#when parsed #then throws ZodError", () => {
let thrownError: unknown
try {
BackgroundTaskConfigSchema.parse({
circuitBreaker: {
consecutiveThreshold: 0,
},
})
} catch (error) {
thrownError = error
}
expect(thrownError).toBeInstanceOf(ZodError)
})
})
})

View File

@@ -1,5 +1,11 @@
import { z } from "zod"
const CircuitBreakerConfigSchema = z.object({
enabled: z.boolean().optional(),
maxToolCalls: z.number().int().min(10).optional(),
consecutiveThreshold: z.number().int().min(5).optional(),
})
export const BackgroundTaskConfigSchema = z.object({
defaultConcurrency: z.number().min(1).optional(),
providerConcurrency: z.record(z.string(), z.number().min(0)).optional(),
@@ -11,6 +17,9 @@ export const BackgroundTaskConfigSchema = z.object({
/** Timeout for tasks that never received any progress update, falling back to startedAt (default: 1800000 = 30 minutes, minimum: 60000 = 1 minute) */
messageStalenessTimeoutMs: z.number().min(60000).optional(),
syncPollTimeoutMs: z.number().min(60000).optional(),
/** Maximum tool calls per subagent task before circuit breaker triggers (default: 200, minimum: 10). Prevents runaway loops from burning unlimited tokens. */
maxToolCalls: z.number().int().min(10).optional(),
circuitBreaker: CircuitBreakerConfigSchema.optional(),
})
export type BackgroundTaskConfig = z.infer<typeof BackgroundTaskConfigSchema>

View File

@@ -1,7 +1,6 @@
import { z } from "zod"
export const HookNameSchema = z.enum([
"gpt-permission-continuation",
"todo-continuation-enforcer",
"context-window-monitor",
"session-recovery",
@@ -51,7 +50,8 @@ export const HookNameSchema = z.enum([
"anthropic-effort",
"hashline-read-enhancer",
"read-image-resizer",
"openclaw-sender",
"todo-description-override",
"webfetch-redirect-guard",
])
export type HookName = z.infer<typeof HookNameSchema>

View File

@@ -1,51 +1,50 @@
import { z } from "zod";
import { z } from "zod"
export const OpenClawHookEventSchema = z.enum([
"session-start",
"session-end",
"session-idle",
"ask-user-question",
"stop",
]);
export const OpenClawHttpGatewayConfigSchema = z.object({
type: z.literal("http").optional(),
url: z.string(), // Allow looser URL validation as it might contain placeholders
export const OpenClawGatewaySchema = z.object({
type: z.enum(["http", "command"]).default("http"),
// HTTP specific
url: z.string().optional(),
method: z.string().default("POST"),
headers: z.record(z.string(), z.string()).optional(),
method: z.enum(["POST", "PUT"]).optional(),
// Command specific
command: z.string().optional(),
// Shared
timeout: z.number().optional(),
});
})
export const OpenClawCommandGatewayConfigSchema = z.object({
type: z.literal("command"),
command: z.string(),
timeout: z.number().optional(),
});
export const OpenClawGatewayConfigSchema = z.union([
OpenClawHttpGatewayConfigSchema,
OpenClawCommandGatewayConfigSchema,
]);
export const OpenClawHookMappingSchema = z.object({
export const OpenClawHookSchema = z.object({
enabled: z.boolean().default(true),
gateway: z.string(),
instruction: z.string(),
enabled: z.boolean(),
});
})
export const OpenClawReplyListenerConfigSchema = z.object({
discordBotToken: z.string().optional(),
discordChannelId: z.string().optional(),
discordMention: z.string().optional(), // For allowed_mentions
authorizedDiscordUserIds: z.array(z.string()).default([]),
telegramBotToken: z.string().optional(),
telegramChatId: z.string().optional(),
pollIntervalMs: z.number().default(3000),
rateLimitPerMinute: z.number().default(10),
maxMessageLength: z.number().default(500),
includePrefix: z.boolean().default(true),
})
export const OpenClawConfigSchema = z.object({
enabled: z.boolean(),
gateways: z.record(z.string(), OpenClawGatewayConfigSchema),
hooks: z
.object({
"session-start": OpenClawHookMappingSchema.optional(),
"session-end": OpenClawHookMappingSchema.optional(),
"session-idle": OpenClawHookMappingSchema.optional(),
"ask-user-question": OpenClawHookMappingSchema.optional(),
stop: OpenClawHookMappingSchema.optional(),
})
.strict()
.optional(),
});
enabled: z.boolean().default(false),
export type OpenClawConfig = z.infer<typeof OpenClawConfigSchema>;
// Outbound Configuration
gateways: z.record(z.string(), OpenClawGatewaySchema).default({}),
hooks: z.record(z.string(), OpenClawHookSchema).default({}),
// Inbound Configuration (Reply Listener)
replyListener: OpenClawReplyListenerConfigSchema.optional(),
})
export type OpenClawConfig = z.infer<typeof OpenClawConfigSchema>
export type OpenClawGateway = z.infer<typeof OpenClawGatewaySchema>
export type OpenClawHook = z.infer<typeof OpenClawHookSchema>
export type OpenClawReplyListenerConfig = z.infer<typeof OpenClawReplyListenerConfigSchema>

View File

@@ -2,9 +2,13 @@ import type { PluginInput } from "@opencode-ai/plugin"
import type { BackgroundTask, LaunchInput } from "./types"
export const TASK_TTL_MS = 30 * 60 * 1000
export const TERMINAL_TASK_TTL_MS = 30 * 60 * 1000
export const MIN_STABILITY_TIME_MS = 10 * 1000
export const DEFAULT_STALE_TIMEOUT_MS = 180_000
export const DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1_800_000
export const DEFAULT_STALE_TIMEOUT_MS = 2_700_000
export const DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 3_600_000
export const DEFAULT_MAX_TOOL_CALLS = 4000
export const DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20
export const DEFAULT_CIRCUIT_BREAKER_ENABLED = true
export const MIN_RUNTIME_BEFORE_STALE_MS = 30_000
export const MIN_IDLE_TIME_MS = 5000
export const POLLING_INTERVAL_MS = 3000

View File

@@ -21,9 +21,9 @@ function createRunningTask(startedAt: Date): BackgroundTask {
}
describe("DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS", () => {
test("uses a 30 minute default", () => {
test("uses a 60 minute default", () => {
// #given
const expectedTimeout = 30 * 60 * 1000
const expectedTimeout = 60 * 60 * 1000
// #when
const timeout = DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS

View File

@@ -0,0 +1,17 @@
declare const require: (name: string) => any
const { describe, expect, test } = require("bun:test")
import { DEFAULT_STALE_TIMEOUT_MS } from "./constants"
describe("DEFAULT_STALE_TIMEOUT_MS", () => {
test("uses a 45 minute default", () => {
// #given
const expectedTimeout = 45 * 60 * 1000
// #when
const timeout = DEFAULT_STALE_TIMEOUT_MS
// #then
expect(timeout).toBe(expectedTimeout)
})
})

View File

@@ -19,6 +19,8 @@ mock.module("../../shared/provider-model-id-transform", () => ({
import { tryFallbackRetry } from "./fallback-retry-handler"
import { shouldRetryError } from "../../shared/model-error-classifier"
import { selectFallbackProvider } from "../../shared/model-error-classifier"
import { readProviderModelsCache } from "../../shared"
import type { BackgroundTask } from "./types"
import type { ConcurrencyManager } from "./concurrency"
@@ -82,6 +84,8 @@ function createDefaultArgs(taskOverrides: Partial<BackgroundTask> = {}) {
describe("tryFallbackRetry", () => {
beforeEach(() => {
;(shouldRetryError as any).mockImplementation(() => true)
;(selectFallbackProvider as any).mockImplementation((providers: string[]) => providers[0])
;(readProviderModelsCache as any).mockReturnValue(null)
})
describe("#given retryable error with fallback chain", () => {
@@ -267,4 +271,24 @@ describe("tryFallbackRetry", () => {
expect(args.task.attemptCount).toBe(2)
})
})
describe("#given disconnected fallback providers with connected preferred provider", () => {
test("keeps fallback entry and selects connected preferred provider", () => {
;(readProviderModelsCache as any).mockReturnValue({ connected: ["provider-a"] })
;(selectFallbackProvider as any).mockImplementation(
(_providers: string[], preferredProviderID?: string) => preferredProviderID ?? "provider-b",
)
const args = createDefaultArgs({
fallbackChain: [{ model: "fallback-model-1", providers: ["provider-b"], variant: undefined }],
model: { providerID: "provider-a", modelID: "original-model" },
})
const result = tryFallbackRetry(args)
expect(result).toBe(true)
expect(args.task.model?.providerID).toBe("provider-a")
expect(args.task.model?.modelID).toBe("fallback-model-1")
})
})
})

View File

@@ -35,10 +35,14 @@ export function tryFallbackRetry(args: {
const providerModelsCache = readProviderModelsCache()
const connectedProviders = providerModelsCache?.connected ?? readConnectedProvidersCache()
const connectedSet = connectedProviders ? new Set(connectedProviders.map(p => p.toLowerCase())) : null
const preferredProvider = task.model?.providerID?.toLowerCase()
const isReachable = (entry: FallbackEntry): boolean => {
if (!connectedSet) return true
return entry.providers.some((p) => connectedSet.has(p.toLowerCase()))
if (entry.providers.some((provider) => connectedSet.has(provider.toLowerCase()))) {
return true
}
return preferredProvider ? connectedSet.has(preferredProvider) : false
}
let selectedAttemptCount = attemptCount

View File

@@ -0,0 +1,263 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import {
createToolCallSignature,
detectRepetitiveToolUse,
recordToolCall,
resolveCircuitBreakerSettings,
} from "./loop-detector"
function buildWindow(
toolNames: string[],
override?: Parameters<typeof resolveCircuitBreakerSettings>[0]
) {
const settings = resolveCircuitBreakerSettings(override)
return toolNames.reduce(
(window, toolName) => recordToolCall(window, toolName, settings),
undefined as ReturnType<typeof recordToolCall> | undefined
)
}
function buildWindowWithInputs(
calls: Array<{ tool: string; input?: Record<string, unknown> | null }>,
override?: Parameters<typeof resolveCircuitBreakerSettings>[0]
) {
const settings = resolveCircuitBreakerSettings(override)
return calls.reduce(
(window, { tool, input }) => recordToolCall(window, tool, settings, input),
undefined as ReturnType<typeof recordToolCall> | undefined
)
}
describe("loop-detector", () => {
describe("resolveCircuitBreakerSettings", () => {
describe("#given nested circuit breaker config", () => {
test("#when resolved #then nested values override defaults", () => {
const result = resolveCircuitBreakerSettings({
maxToolCalls: 200,
circuitBreaker: {
maxToolCalls: 120,
consecutiveThreshold: 7,
},
})
expect(result).toEqual({
enabled: true,
maxToolCalls: 120,
consecutiveThreshold: 7,
})
})
})
describe("#given no enabled config", () => {
test("#when resolved #then enabled defaults to true", () => {
const result = resolveCircuitBreakerSettings({
circuitBreaker: {
maxToolCalls: 100,
consecutiveThreshold: 5,
},
})
expect(result.enabled).toBe(true)
})
})
describe("#given enabled is false in config", () => {
test("#when resolved #then enabled is false", () => {
const result = resolveCircuitBreakerSettings({
circuitBreaker: {
enabled: false,
maxToolCalls: 100,
consecutiveThreshold: 5,
},
})
expect(result.enabled).toBe(false)
})
})
describe("#given enabled is true in config", () => {
test("#when resolved #then enabled is true", () => {
const result = resolveCircuitBreakerSettings({
circuitBreaker: {
enabled: true,
maxToolCalls: 100,
consecutiveThreshold: 5,
},
})
expect(result.enabled).toBe(true)
})
})
})
describe("createToolCallSignature", () => {
test("#given tool with input #when signature created #then includes tool and sorted input", () => {
const result = createToolCallSignature("read", { filePath: "/a.ts" })
expect(result).toBe('read::{"filePath":"/a.ts"}')
})
test("#given tool with undefined input #when signature created #then returns bare tool name", () => {
const result = createToolCallSignature("read", undefined)
expect(result).toBe("read")
})
test("#given tool with null input #when signature created #then returns bare tool name", () => {
const result = createToolCallSignature("read", null)
expect(result).toBe("read")
})
test("#given tool with empty object input #when signature created #then returns bare tool name", () => {
const result = createToolCallSignature("read", {})
expect(result).toBe("read")
})
test("#given same input different key order #when signatures compared #then they are equal", () => {
const first = createToolCallSignature("read", { filePath: "/a.ts", offset: 0 })
const second = createToolCallSignature("read", { offset: 0, filePath: "/a.ts" })
expect(first).toBe(second)
})
})
describe("detectRepetitiveToolUse", () => {
describe("#given recent tools are diverse", () => {
test("#when evaluated #then it does not trigger", () => {
const window = buildWindow([
"read",
"grep",
"edit",
"bash",
"read",
"glob",
"lsp_diagnostics",
"read",
"grep",
"edit",
])
const result = detectRepetitiveToolUse(window)
expect(result.triggered).toBe(false)
})
})
describe("#given the same tool is called consecutively", () => {
test("#when evaluated #then it triggers", () => {
const window = buildWindowWithInputs(
Array.from({ length: 20 }, () => ({
tool: "read",
input: { filePath: "/src/same.ts" },
}))
)
const result = detectRepetitiveToolUse(window)
expect(result).toEqual({
triggered: true,
toolName: "read",
repeatedCount: 20,
})
})
})
describe("#given consecutive calls are interrupted by different tool", () => {
test("#when evaluated #then it does not trigger", () => {
const window = buildWindow([
...Array.from({ length: 19 }, () => "read"),
"edit",
"read",
])
const result = detectRepetitiveToolUse(window)
expect(result).toEqual({ triggered: false })
})
})
describe("#given threshold boundary", () => {
test("#when below threshold #then it does not trigger", () => {
const belowThresholdWindow = buildWindowWithInputs(
Array.from({ length: 19 }, () => ({
tool: "read",
input: { filePath: "/src/same.ts" },
}))
)
const result = detectRepetitiveToolUse(belowThresholdWindow)
expect(result).toEqual({ triggered: false })
})
test("#when equal to threshold #then it triggers", () => {
const atThresholdWindow = buildWindowWithInputs(
Array.from({ length: 20 }, () => ({
tool: "read",
input: { filePath: "/src/same.ts" },
}))
)
const result = detectRepetitiveToolUse(atThresholdWindow)
expect(result).toEqual({
triggered: true,
toolName: "read",
repeatedCount: 20,
})
})
})
describe("#given same tool with different file inputs", () => {
test("#when evaluated #then it does not trigger", () => {
const calls = Array.from({ length: 20 }, (_, i) => ({
tool: "read",
input: { filePath: `/src/file-${i}.ts` },
}))
const window = buildWindowWithInputs(calls)
const result = detectRepetitiveToolUse(window)
expect(result.triggered).toBe(false)
})
})
describe("#given same tool with identical file inputs", () => {
test("#when evaluated #then it triggers with bare tool name", () => {
const calls = Array.from({ length: 20 }, () => ({
tool: "read",
input: { filePath: "/src/same.ts" },
}))
const window = buildWindowWithInputs(calls)
const result = detectRepetitiveToolUse(window)
expect(result).toEqual({
triggered: true,
toolName: "read",
repeatedCount: 20,
})
})
})
describe("#given tool calls with undefined input", () => {
test("#when evaluated #then it does not trigger", () => {
const calls = Array.from({ length: 20 }, () => ({ tool: "read" }))
const window = buildWindowWithInputs(calls)
const result = detectRepetitiveToolUse(window)
expect(result).toEqual({ triggered: false })
})
})
describe("#given tool calls with null input", () => {
test("#when evaluated #then it does not trigger", () => {
const calls = Array.from({ length: 20 }, () => ({ tool: "read", input: null }))
const window = buildWindowWithInputs(calls)
const result = detectRepetitiveToolUse(window)
expect(result).toEqual({ triggered: false })
})
})
})
})

View File

@@ -0,0 +1,102 @@
import type { BackgroundTaskConfig } from "../../config/schema"
import {
DEFAULT_CIRCUIT_BREAKER_ENABLED,
DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD,
DEFAULT_MAX_TOOL_CALLS,
} from "./constants"
import type { ToolCallWindow } from "./types"
export interface CircuitBreakerSettings {
enabled: boolean
maxToolCalls: number
consecutiveThreshold: number
}
export interface ToolLoopDetectionResult {
triggered: boolean
toolName?: string
repeatedCount?: number
}
export function resolveCircuitBreakerSettings(
config?: BackgroundTaskConfig
): CircuitBreakerSettings {
return {
enabled: config?.circuitBreaker?.enabled ?? DEFAULT_CIRCUIT_BREAKER_ENABLED,
maxToolCalls:
config?.circuitBreaker?.maxToolCalls ?? config?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS,
consecutiveThreshold:
config?.circuitBreaker?.consecutiveThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD,
}
}
export function recordToolCall(
window: ToolCallWindow | undefined,
toolName: string,
settings: CircuitBreakerSettings,
toolInput?: Record<string, unknown> | null
): ToolCallWindow {
if (toolInput === undefined || toolInput === null) {
return {
lastSignature: `${toolName}::__unknown-input__`,
consecutiveCount: 1,
threshold: settings.consecutiveThreshold,
}
}
const signature = createToolCallSignature(toolName, toolInput)
if (window && window.lastSignature === signature) {
return {
lastSignature: signature,
consecutiveCount: window.consecutiveCount + 1,
threshold: settings.consecutiveThreshold,
}
}
return {
lastSignature: signature,
consecutiveCount: 1,
threshold: settings.consecutiveThreshold,
}
}
function sortObject(obj: unknown): unknown {
if (obj === null || obj === undefined) return obj
if (typeof obj !== "object") return obj
if (Array.isArray(obj)) return obj.map(sortObject)
const sorted: Record<string, unknown> = {}
const keys = Object.keys(obj as Record<string, unknown>).sort()
for (const key of keys) {
sorted[key] = sortObject((obj as Record<string, unknown>)[key])
}
return sorted
}
export function createToolCallSignature(
toolName: string,
toolInput?: Record<string, unknown> | null
): string {
if (toolInput === undefined || toolInput === null) {
return toolName
}
if (Object.keys(toolInput).length === 0) {
return toolName
}
return `${toolName}::${JSON.stringify(sortObject(toolInput))}`
}
export function detectRepetitiveToolUse(
window: ToolCallWindow | undefined
): ToolLoopDetectionResult {
if (!window || window.consecutiveCount < window.threshold) {
return { triggered: false }
}
return {
triggered: true,
toolName: window.lastSignature.split("::")[0],
repeatedCount: window.consecutiveCount,
}
}

View File

@@ -0,0 +1,389 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin"
import { tmpdir } from "node:os"
import type { BackgroundTaskConfig } from "../../config/schema"
import { BackgroundManager } from "./manager"
import type { BackgroundTask } from "./types"
function createManager(config?: BackgroundTaskConfig): BackgroundManager {
const client = {
session: {
prompt: async () => ({}),
promptAsync: async () => ({}),
abort: async () => ({}),
},
}
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput, config)
const testManager = manager as unknown as {
enqueueNotificationForParent: (sessionID: string, fn: () => Promise<void>) => Promise<void>
notifyParentSession: (task: BackgroundTask) => Promise<void>
tasks: Map<string, BackgroundTask>
}
testManager.enqueueNotificationForParent = async (_sessionID, fn) => {
await fn()
}
testManager.notifyParentSession = async () => {}
return manager
}
function getTaskMap(manager: BackgroundManager): Map<string, BackgroundTask> {
return (manager as unknown as { tasks: Map<string, BackgroundTask> }).tasks
}
async function flushAsyncWork() {
await new Promise(resolve => setTimeout(resolve, 0))
}
describe("BackgroundManager circuit breaker", () => {
describe("#given flat-format tool events have no state.input", () => {
test("#when 20 consecutive read events arrive #then the task keeps running", async () => {
const manager = createManager({
circuitBreaker: {
consecutiveThreshold: 20,
},
})
const task: BackgroundTask = {
id: "task-loop-1",
sessionID: "session-loop-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Looping task",
prompt: "loop",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (let i = 0; i < 20; i++) {
manager.handleEvent({
type: "message.part.updated",
properties: { sessionID: task.sessionID, type: "tool", tool: "read" },
})
}
await flushAsyncWork()
expect(task.status).toBe("running")
expect(task.progress?.toolCalls).toBe(20)
})
})
describe("#given recent tool calls are diverse", () => {
test("#when the window fills #then the task keeps running", async () => {
const manager = createManager({
circuitBreaker: {
consecutiveThreshold: 10,
},
})
const task: BackgroundTask = {
id: "task-diverse-1",
sessionID: "session-diverse-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Healthy task",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (const toolName of [
"read",
"grep",
"edit",
"bash",
"glob",
"read",
"lsp_diagnostics",
"grep",
"edit",
"read",
]) {
manager.handleEvent({
type: "message.part.updated",
properties: { sessionID: task.sessionID, type: "tool", tool: toolName },
})
}
await flushAsyncWork()
expect(task.status).toBe("running")
expect(task.progress?.toolCalls).toBe(10)
})
})
describe("#given the absolute cap is configured lower than the repetition detector needs", () => {
test("#when repeated flat-format tool events reach maxToolCalls #then the backstop still cancels the task", async () => {
const manager = createManager({
maxToolCalls: 3,
circuitBreaker: {
consecutiveThreshold: 95,
},
})
const task: BackgroundTask = {
id: "task-cap-1",
sessionID: "session-cap-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Backstop task",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (let i = 0; i < 3; i++) {
manager.handleEvent({
type: "message.part.updated",
properties: { sessionID: task.sessionID, type: "tool", tool: "read" },
})
}
await flushAsyncWork()
expect(task.status).toBe("cancelled")
expect(task.error).toContain("maximum tool call limit (3)")
})
})
describe("#given the same running tool part emits multiple updates", () => {
test("#when duplicate running updates arrive #then it only counts the tool once", async () => {
const manager = createManager({
maxToolCalls: 2,
circuitBreaker: {
consecutiveThreshold: 5,
},
})
const task: BackgroundTask = {
id: "task-dedupe-1",
sessionID: "session-dedupe-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Dedupe task",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (let index = 0; index < 3; index += 1) {
manager.handleEvent({
type: "message.part.updated",
properties: {
part: {
id: "tool-1",
sessionID: task.sessionID,
type: "tool",
tool: "bash",
state: { status: "running" },
},
},
})
}
await flushAsyncWork()
expect(task.status).toBe("running")
expect(task.progress?.toolCalls).toBe(1)
expect(task.progress?.countedToolPartIDs).toEqual(new Set(["tool-1"]))
})
})
describe("#given same tool reading different files", () => {
test("#when tool events arrive with state.input #then task keeps running", async () => {
const manager = createManager({
circuitBreaker: {
consecutiveThreshold: 20,
},
})
const task: BackgroundTask = {
id: "task-diff-files-1",
sessionID: "session-diff-files-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Reading different files",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (let i = 0; i < 20; i++) {
manager.handleEvent({
type: "message.part.updated",
properties: {
part: {
sessionID: task.sessionID,
type: "tool",
tool: "read",
state: { status: "running", input: { filePath: `/src/file-${i}.ts` } },
},
},
})
}
await flushAsyncWork()
expect(task.status).toBe("running")
expect(task.progress?.toolCalls).toBe(20)
})
})
describe("#given same tool reading same file repeatedly", () => {
test("#when tool events arrive with state.input #then task is cancelled with bare tool name in error", async () => {
const manager = createManager({
circuitBreaker: {
consecutiveThreshold: 20,
},
})
const task: BackgroundTask = {
id: "task-same-file-1",
sessionID: "session-same-file-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Reading same file repeatedly",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (let i = 0; i < 20; i++) {
manager.handleEvent({
type: "message.part.updated",
properties: {
part: {
sessionID: task.sessionID,
type: "tool",
tool: "read",
state: { status: "running", input: { filePath: "/src/same.ts" } },
},
},
})
}
await flushAsyncWork()
expect(task.status).toBe("cancelled")
expect(task.error).toContain("read 20 consecutive times")
expect(task.error).not.toContain("::")
})
})
describe("#given circuit breaker enabled is false", () => {
test("#when repetitive tools arrive #then task keeps running", async () => {
const manager = createManager({
circuitBreaker: {
enabled: false,
consecutiveThreshold: 20,
},
})
const task: BackgroundTask = {
id: "task-disabled-1",
sessionID: "session-disabled-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Disabled circuit breaker task",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (let i = 0; i < 20; i++) {
manager.handleEvent({
type: "message.part.updated",
properties: {
sessionID: task.sessionID,
type: "tool",
tool: "read",
},
})
}
await flushAsyncWork()
expect(task.status).toBe("running")
})
})
describe("#given circuit breaker enabled is false but absolute cap is low", () => {
test("#when max tool calls exceeded #then task is still cancelled by absolute cap", async () => {
const manager = createManager({
maxToolCalls: 3,
circuitBreaker: {
enabled: false,
consecutiveThreshold: 95,
},
})
const task: BackgroundTask = {
id: "task-cap-disabled-1",
sessionID: "session-cap-disabled-1",
parentSessionID: "parent-1",
parentMessageID: "msg-1",
description: "Backstop task with disabled circuit breaker",
prompt: "work",
agent: "explore",
status: "running",
startedAt: new Date(Date.now() - 60_000),
progress: {
toolCalls: 0,
lastUpdate: new Date(Date.now() - 60_000),
},
}
getTaskMap(manager).set(task.id, task)
for (const toolName of ["read", "grep", "edit"]) {
manager.handleEvent({
type: "message.part.updated",
properties: { sessionID: task.sessionID, type: "tool", tool: toolName },
})
}
await flushAsyncWork()
expect(task.status).toBe("cancelled")
expect(task.error).toContain("maximum tool call limit (3)")
})
})
})

View File

@@ -153,4 +153,42 @@ describe("BackgroundManager pollRunningTasks", () => {
expect(task.status).toBe("running")
})
})
describe("#given a running task whose session has terminal non-idle status", () => {
test('#when session status is "interrupted" #then completes the task', async () => {
//#given
const manager = createManagerWithClient({
status: async () => ({ data: { "ses-interrupted": { type: "interrupted" } } }),
})
const task = createRunningTask("ses-interrupted")
injectTask(manager, task)
//#when
const poll = (manager as unknown as { pollRunningTasks: () => Promise<void> }).pollRunningTasks
await poll.call(manager)
manager.shutdown()
//#then
expect(task.status).toBe("completed")
expect(task.completedAt).toBeDefined()
})
test('#when session status is an unknown type #then completes the task', async () => {
//#given
const manager = createManagerWithClient({
status: async () => ({ data: { "ses-unknown": { type: "some-weird-status" } } }),
})
const task = createRunningTask("ses-unknown")
injectTask(manager, task)
//#when
const poll = (manager as unknown as { pollRunningTasks: () => Promise<void> }).pollRunningTasks
await poll.call(manager)
manager.shutdown()
//#then
expect(task.status).toBe("completed")
expect(task.completedAt).toBeDefined()
})
})
})

View File

@@ -1,5 +1,5 @@
declare const require: (name: string) => any
const { describe, test, expect, beforeEach, afterEach } = require("bun:test")
const { describe, test, expect, beforeEach, afterEach, spyOn } = require("bun:test")
import { tmpdir } from "node:os"
import type { PluginInput } from "@opencode-ai/plugin"
import type { BackgroundTask, ResumeInput } from "./types"
@@ -1806,9 +1806,9 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
expect(task.sessionID).toBeUndefined()
})
test("should return immediately even with concurrency limit", async () => {
// given
const config = { defaultConcurrency: 1 }
test("should return immediately even with concurrency limit", async () => {
// given
const config = { defaultConcurrency: 1 }
manager.shutdown()
manager = new BackgroundManager({ client: mockClient, directory: tmpdir() } as unknown as PluginInput, config)
@@ -1828,9 +1828,76 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
// then
expect(endTime - startTime).toBeLessThan(100) // Should be instant
expect(task1.status).toBe("pending")
expect(task2.status).toBe("pending")
expect(task1.status).toBe("pending")
expect(task2.status).toBe("pending")
})
test("should keep agent when launch has model and keep agent without model", async () => {
// given
const promptBodies: Array<Record<string, unknown>> = []
let resolveFirstPromptStarted: (() => void) | undefined
let resolveSecondPromptStarted: (() => void) | undefined
const firstPromptStarted = new Promise<void>((resolve) => {
resolveFirstPromptStarted = resolve
})
const secondPromptStarted = new Promise<void>((resolve) => {
resolveSecondPromptStarted = resolve
})
const customClient = {
session: {
create: async (_args?: unknown) => ({ data: { id: `ses_${crypto.randomUUID()}` } }),
get: async () => ({ data: { directory: "/test/dir" } }),
prompt: async () => ({}),
promptAsync: async (args: { path: { id: string }; body: Record<string, unknown> }) => {
promptBodies.push(args.body)
if (promptBodies.length === 1) {
resolveFirstPromptStarted?.()
}
if (promptBodies.length === 2) {
resolveSecondPromptStarted?.()
}
return {}
},
messages: async () => ({ data: [] }),
todo: async () => ({ data: [] }),
status: async () => ({ data: {} }),
abort: async () => ({}),
},
}
manager.shutdown()
manager = new BackgroundManager({ client: customClient, directory: tmpdir() } as unknown as PluginInput)
const launchInputWithModel = {
description: "Test task with model",
prompt: "Do something",
agent: "test-agent",
parentSessionID: "parent-session",
parentMessageID: "parent-message",
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
}
const launchInputWithoutModel = {
description: "Test task without model",
prompt: "Do something else",
agent: "test-agent",
parentSessionID: "parent-session",
parentMessageID: "parent-message",
}
// when
const taskWithModel = await manager.launch(launchInputWithModel)
await firstPromptStarted
const taskWithoutModel = await manager.launch(launchInputWithoutModel)
await secondPromptStarted
// then
expect(taskWithModel.status).toBe("pending")
expect(taskWithoutModel.status).toBe("pending")
expect(promptBodies).toHaveLength(2)
expect(promptBodies[0].model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
expect(promptBodies[0].agent).toBe("test-agent")
expect(promptBodies[1].agent).toBe("test-agent")
expect("model" in promptBodies[1]).toBe(false)
})
test("should queue multiple tasks without blocking", async () => {
// given
@@ -2781,6 +2848,18 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
})
describe("BackgroundManager.checkAndInterruptStaleTasks", () => {
const originalDateNow = Date.now
let fixedTime: number
beforeEach(() => {
fixedTime = Date.now()
spyOn(globalThis.Date, "now").mockReturnValue(fixedTime)
})
afterEach(() => {
Date.now = originalDateNow
})
test("should NOT interrupt task running less than 30 seconds (min runtime guard)", async () => {
const client = {
session: {
@@ -3027,10 +3106,10 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => {
prompt: "Test",
agent: "test-agent",
status: "running",
startedAt: new Date(Date.now() - 300_000),
startedAt: new Date(Date.now() - 50 * 60 * 1000),
progress: {
toolCalls: 1,
lastUpdate: new Date(Date.now() - 200_000),
lastUpdate: new Date(Date.now() - 46 * 60 * 1000),
},
}
@@ -4673,6 +4752,53 @@ describe("BackgroundManager - tool permission spread order", () => {
manager.shutdown()
})
test("startTask keeps agent when explicit model is configured", async () => {
//#given
const promptCalls: Array<{ path: { id: string }; body: Record<string, unknown> }> = []
const client = {
session: {
get: async () => ({ data: { directory: "/test/dir" } }),
create: async () => ({ data: { id: "session-1" } }),
promptAsync: async (args: { path: { id: string }; body: Record<string, unknown> }) => {
promptCalls.push(args)
return {}
},
},
}
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
const task: BackgroundTask = {
id: "task-explicit-model",
status: "pending",
queuedAt: new Date(),
description: "test task",
prompt: "test prompt",
agent: "sisyphus-junior",
parentSessionID: "parent-session",
parentMessageID: "parent-message",
model: { providerID: "openai", modelID: "gpt-5.4", variant: "medium" },
}
const input: import("./types").LaunchInput = {
description: task.description,
prompt: task.prompt,
agent: task.agent,
parentSessionID: task.parentSessionID,
parentMessageID: task.parentMessageID,
model: task.model,
}
//#when
await (manager as unknown as { startTask: (item: { task: BackgroundTask; input: import("./types").LaunchInput }) => Promise<void> })
.startTask({ task, input })
//#then
expect(promptCalls).toHaveLength(1)
expect(promptCalls[0].body.agent).toBe("sisyphus-junior")
expect(promptCalls[0].body.model).toEqual({ providerID: "openai", modelID: "gpt-5.4" })
expect(promptCalls[0].body.variant).toBe("medium")
manager.shutdown()
})
test("resume respects explore agent restrictions", async () => {
//#given
let capturedTools: Record<string, unknown> | undefined
@@ -4717,4 +4843,48 @@ describe("BackgroundManager - tool permission spread order", () => {
manager.shutdown()
})
test("resume keeps agent when explicit model is configured", async () => {
//#given
let promptCall: { path: { id: string }; body: Record<string, unknown> } | undefined
const client = {
session: {
promptAsync: async (args: { path: { id: string }; body: Record<string, unknown> }) => {
promptCall = args
return {}
},
abort: async () => ({}),
},
}
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
const task: BackgroundTask = {
id: "task-explicit-model-resume",
sessionID: "session-3",
parentSessionID: "parent-session",
parentMessageID: "parent-message",
description: "resume task",
prompt: "resume prompt",
agent: "explore",
status: "completed",
startedAt: new Date(),
completedAt: new Date(),
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
}
getTaskMap(manager).set(task.id, task)
//#when
await manager.resume({
sessionId: "session-3",
prompt: "continue",
parentSessionID: "parent-session",
parentMessageID: "parent-message",
})
//#then
expect(promptCall).toBeDefined()
expect(promptCall?.body.agent).toBe("explore")
expect(promptCall?.body.model).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-20250514" })
manager.shutdown()
})
})

View File

@@ -27,6 +27,7 @@ import {
import {
POLLING_INTERVAL_MS,
TASK_CLEANUP_DELAY_MS,
TASK_TTL_MS,
} from "./constants"
import { subagentSessions } from "../claude-code-session-state"
@@ -51,6 +52,13 @@ import { join } from "node:path"
import { pruneStaleTasksAndNotifications } from "./task-poller"
import { checkAndInterruptStaleTasks } from "./task-poller"
import { removeTaskToastTracking } from "./remove-task-toast-tracking"
import { isActiveSessionStatus, isTerminalSessionStatus } from "./session-status-classifier"
import {
detectRepetitiveToolUse,
recordToolCall,
resolveCircuitBreakerSettings,
type CircuitBreakerSettings,
} from "./loop-detector"
import {
createSubagentDepthLimitError,
createSubagentDescendantLimitError,
@@ -64,9 +72,11 @@ type OpencodeClient = PluginInput["client"]
interface MessagePartInfo {
id?: string
sessionID?: string
type?: string
tool?: string
state?: { status?: string; input?: Record<string, unknown> }
}
interface EventProperties {
@@ -80,6 +90,19 @@ interface Event {
properties?: EventProperties
}
function resolveMessagePartInfo(properties: EventProperties | undefined): MessagePartInfo | undefined {
if (!properties || typeof properties !== "object") {
return undefined
}
const nestedPart = properties.part
if (nestedPart && typeof nestedPart === "object") {
return nestedPart as MessagePartInfo
}
return properties as MessagePartInfo
}
interface Todo {
content: string
status: string
@@ -100,6 +123,8 @@ export interface SubagentSessionCreatedEvent {
export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise<void>
const MAX_TASK_REMOVAL_RESCHEDULES = 6
export class BackgroundManager {
@@ -128,6 +153,7 @@ export class BackgroundManager {
private preStartDescendantReservations: Set<string>
private enableParentSessionNotifications: boolean
readonly taskHistory = new TaskHistory()
private cachedCircuitBreakerSettings?: CircuitBreakerSettings
constructor(
ctx: PluginInput,
@@ -720,6 +746,8 @@ export class BackgroundManager {
existingTask.progress = {
toolCalls: existingTask.progress?.toolCalls ?? 0,
toolCallWindow: existingTask.progress?.toolCallWindow,
countedToolPartIDs: existingTask.progress?.countedToolPartIDs,
lastUpdate: new Date(),
}
@@ -852,8 +880,7 @@ export class BackgroundManager {
}
if (event.type === "message.part.updated" || event.type === "message.part.delta") {
if (!props || typeof props !== "object" || !("sessionID" in props)) return
const partInfo = props as unknown as MessagePartInfo
const partInfo = resolveMessagePartInfo(props)
const sessionID = partInfo?.sessionID
if (!sessionID) return
@@ -876,8 +903,65 @@ export class BackgroundManager {
task.progress.lastUpdate = new Date()
if (partInfo?.type === "tool" || partInfo?.tool) {
const countedToolPartIDs = task.progress.countedToolPartIDs ?? new Set<string>()
const shouldCountToolCall =
!partInfo.id ||
partInfo.state?.status !== "running" ||
!countedToolPartIDs.has(partInfo.id)
if (!shouldCountToolCall) {
return
}
if (partInfo.id && partInfo.state?.status === "running") {
countedToolPartIDs.add(partInfo.id)
task.progress.countedToolPartIDs = countedToolPartIDs
}
task.progress.toolCalls += 1
task.progress.lastTool = partInfo.tool
const circuitBreaker = this.cachedCircuitBreakerSettings ?? (this.cachedCircuitBreakerSettings = resolveCircuitBreakerSettings(this.config))
if (partInfo.tool) {
task.progress.toolCallWindow = recordToolCall(
task.progress.toolCallWindow,
partInfo.tool,
circuitBreaker,
partInfo.state?.input
)
if (circuitBreaker.enabled) {
const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow)
if (loopDetection.triggered) {
log("[background-agent] Circuit breaker: consecutive tool usage detected", {
taskId: task.id,
agent: task.agent,
sessionID,
toolName: loopDetection.toolName,
repeatedCount: loopDetection.repeatedCount,
})
void this.cancelTask(task.id, {
source: "circuit-breaker",
reason: `Subagent called ${loopDetection.toolName} ${loopDetection.repeatedCount} consecutive times (threshold: ${circuitBreaker.consecutiveThreshold}). This usually indicates an infinite loop. The task was automatically cancelled to prevent excessive token usage.`,
})
return
}
}
}
const maxToolCalls = circuitBreaker.maxToolCalls
if (task.progress.toolCalls >= maxToolCalls) {
log("[background-agent] Circuit breaker: tool call limit reached", {
taskId: task.id,
toolCalls: task.progress.toolCalls,
maxToolCalls,
agent: task.agent,
sessionID,
})
void this.cancelTask(task.id, {
source: "circuit-breaker",
reason: `Subagent exceeded maximum tool call limit (${maxToolCalls}). This usually indicates an infinite loop. The task was automatically cancelled to prevent excessive token usage.`,
})
}
}
}
@@ -1188,7 +1272,7 @@ export class BackgroundManager {
this.completedTaskSummaries.delete(parentSessionID)
}
private scheduleTaskRemoval(taskId: string): void {
private scheduleTaskRemoval(taskId: string, rescheduleCount = 0): void {
const existingTimer = this.completionTimers.get(taskId)
if (existingTimer) {
clearTimeout(existingTimer)
@@ -1198,17 +1282,29 @@ export class BackgroundManager {
const timer = setTimeout(() => {
this.completionTimers.delete(taskId)
const task = this.tasks.get(taskId)
if (task) {
this.clearNotificationsForTask(taskId)
this.tasks.delete(taskId)
this.clearTaskHistoryWhenParentTasksGone(task.parentSessionID)
if (task.sessionID) {
subagentSessions.delete(task.sessionID)
SessionCategoryRegistry.remove(task.sessionID)
if (!task) return
if (task.parentSessionID) {
const siblings = this.getTasksByParentSession(task.parentSessionID)
const runningOrPendingSiblings = siblings.filter(
sibling => sibling.id !== taskId && (sibling.status === "running" || sibling.status === "pending"),
)
const completedAtTimestamp = task.completedAt?.getTime()
const reachedTaskTtl = completedAtTimestamp !== undefined && (Date.now() - completedAtTimestamp) >= TASK_TTL_MS
if (runningOrPendingSiblings.length > 0 && rescheduleCount < MAX_TASK_REMOVAL_RESCHEDULES && !reachedTaskTtl) {
this.scheduleTaskRemoval(taskId, rescheduleCount + 1)
return
}
log("[background-agent] Removed completed task from memory:", taskId)
this.clearTaskHistoryWhenParentTasksGone(task?.parentSessionID)
}
this.clearNotificationsForTask(taskId)
this.tasks.delete(taskId)
this.clearTaskHistoryWhenParentTasksGone(task.parentSessionID)
if (task.sessionID) {
subagentSessions.delete(task.sessionID)
SessionCategoryRegistry.remove(task.sessionID)
}
log("[background-agent] Removed completed task from memory:", taskId)
}, TASK_CLEANUP_DELAY_MS)
this.completionTimers.set(taskId, timer)
@@ -1688,11 +1784,9 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
}
}
// Match sync-session-poller pattern: only skip completion check when
// status EXISTS and is not idle (i.e., session is actively running).
// When sessionStatus is undefined, the session has completed and dropped
// from the status response — fall through to completion detection.
if (sessionStatus && sessionStatus.type !== "idle") {
// Only skip completion when session status is actively running.
// Unknown or terminal statuses (like "interrupted") fall through to completion.
if (sessionStatus && isActiveSessionStatus(sessionStatus.type)) {
log("[background-agent] Session still running, relying on event-based progress:", {
taskId: task.id,
sessionID,
@@ -1702,6 +1796,24 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
continue
}
// Explicit terminal non-idle status (e.g., "interrupted") — complete immediately,
// skipping output validation (session will never produce more output).
// Unknown statuses fall through to the idle/gone path with output validation.
if (sessionStatus && isTerminalSessionStatus(sessionStatus.type)) {
await this.tryCompleteTask(task, `polling (terminal session status: ${sessionStatus.type})`)
continue
}
// Unknown non-idle status — not active, not terminal, not idle.
// Fall through to idle/gone completion path with output validation.
if (sessionStatus && sessionStatus.type !== "idle") {
log("[background-agent] Unknown session status, treating as potentially idle:", {
taskId: task.id,
sessionID,
sessionStatus: sessionStatus.type,
})
}
// Session is idle or no longer in status response (completed/disappeared)
const completionSource = sessionStatus?.type === "idle"
? "polling (idle status)"

View File

@@ -0,0 +1,66 @@
import { describe, test, expect, mock } from "bun:test"
import { isActiveSessionStatus, isTerminalSessionStatus } from "./session-status-classifier"
const mockLog = mock()
mock.module("../../shared", () => ({ log: mockLog }))
describe("isActiveSessionStatus", () => {
describe("#given a known active session status", () => {
test('#when type is "busy" #then returns true', () => {
expect(isActiveSessionStatus("busy")).toBe(true)
})
test('#when type is "retry" #then returns true', () => {
expect(isActiveSessionStatus("retry")).toBe(true)
})
test('#when type is "running" #then returns true', () => {
expect(isActiveSessionStatus("running")).toBe(true)
})
})
describe("#given a known terminal session status", () => {
test('#when type is "idle" #then returns false', () => {
expect(isActiveSessionStatus("idle")).toBe(false)
})
test('#when type is "interrupted" #then returns false and does not log', () => {
mockLog.mockClear()
expect(isActiveSessionStatus("interrupted")).toBe(false)
expect(mockLog).not.toHaveBeenCalled()
})
})
describe("#given an unknown session status", () => {
test('#when type is an arbitrary unknown string #then returns false and logs warning', () => {
mockLog.mockClear()
expect(isActiveSessionStatus("some-unknown-status")).toBe(false)
expect(mockLog).toHaveBeenCalledWith(
"[background-agent] Unknown session status type encountered:",
"some-unknown-status",
)
})
test('#when type is empty string #then returns false', () => {
expect(isActiveSessionStatus("")).toBe(false)
})
})
})
describe("isTerminalSessionStatus", () => {
test('#when type is "interrupted" #then returns true', () => {
expect(isTerminalSessionStatus("interrupted")).toBe(true)
})
test('#when type is "idle" #then returns false (idle is handled separately)', () => {
expect(isTerminalSessionStatus("idle")).toBe(false)
})
test('#when type is "busy" #then returns false', () => {
expect(isTerminalSessionStatus("busy")).toBe(false)
})
test('#when type is an unknown string #then returns false', () => {
expect(isTerminalSessionStatus("some-unknown")).toBe(false)
})
})

View File

@@ -0,0 +1,20 @@
import { log } from "../../shared"
const ACTIVE_SESSION_STATUSES = new Set(["busy", "retry", "running"])
const KNOWN_TERMINAL_STATUSES = new Set(["idle", "interrupted"])
export function isActiveSessionStatus(type: string): boolean {
if (ACTIVE_SESSION_STATUSES.has(type)) {
return true
}
if (!KNOWN_TERMINAL_STATUSES.has(type)) {
log("[background-agent] Unknown session status type encountered:", type)
}
return false
}
export function isTerminalSessionStatus(type: string): boolean {
return KNOWN_TERMINAL_STATUSES.has(type) && type !== "idle"
}

View File

@@ -64,4 +64,63 @@ describe("background-agent spawner.startTask", () => {
{ permission: "question", action: "deny", pattern: "*" },
])
})
test("keeps agent when explicit model is configured", async () => {
//#given
const promptCalls: any[] = []
const client = {
session: {
get: async () => ({ data: { directory: "/parent/dir" } }),
create: async () => ({ data: { id: "ses_child" } }),
promptAsync: async (args?: any) => {
promptCalls.push(args)
return {}
},
},
}
const task = createTask({
description: "Test task",
prompt: "Do work",
agent: "sisyphus-junior",
parentSessionID: "ses_parent",
parentMessageID: "msg_parent",
model: { providerID: "openai", modelID: "gpt-5.4", variant: "medium" },
})
const item = {
task,
input: {
description: task.description,
prompt: task.prompt,
agent: task.agent,
parentSessionID: task.parentSessionID,
parentMessageID: task.parentMessageID,
parentModel: task.parentModel,
parentAgent: task.parentAgent,
model: task.model,
},
}
const ctx = {
client,
directory: "/fallback",
concurrencyManager: { release: () => {} },
tmuxEnabled: false,
onTaskError: () => {},
}
//#when
await startTask(item as any, ctx as any)
//#then
expect(promptCalls).toHaveLength(1)
expect(promptCalls[0]?.body?.agent).toBe("sisyphus-junior")
expect(promptCalls[0]?.body?.model).toEqual({
providerID: "openai",
modelID: "gpt-5.4",
})
expect(promptCalls[0]?.body?.variant).toBe("medium")
})
})

View File

@@ -1,6 +1,5 @@
declare const require: (name: string) => any
const { describe, test, expect, afterEach } = require("bun:test")
import { tmpdir } from "node:os"
import { afterEach, describe, expect, test } from "bun:test"
import type { PluginInput } from "@opencode-ai/plugin"
import { TASK_CLEANUP_DELAY_MS } from "./constants"
import { BackgroundManager } from "./manager"
@@ -157,17 +156,19 @@ function getRequiredTimer(manager: BackgroundManager, taskID: string): ReturnTyp
}
describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
describe("#given 2 tasks for same parent and task A completed", () => {
test("#when task B is still running #then task A is cleaned up from this.tasks after delay even though task B is not done", async () => {
describe("#given 3 tasks for same parent and task A completed first", () => {
test("#when siblings are still running or pending #then task A remains until siblings also complete", async () => {
// given
const { manager } = createManager(false)
managerUnderTest = manager
fakeTimers = installFakeTimers()
const taskA = createTask({ id: "task-a", parentSessionID: "parent-1", description: "task A", status: "completed", completedAt: new Date("2026-03-11T00:01:00.000Z") })
const taskA = createTask({ id: "task-a", parentSessionID: "parent-1", description: "task A", status: "completed", completedAt: new Date() })
const taskB = createTask({ id: "task-b", parentSessionID: "parent-1", description: "task B", status: "running" })
const taskC = createTask({ id: "task-c", parentSessionID: "parent-1", description: "task C", status: "pending" })
getTasks(manager).set(taskA.id, taskA)
getTasks(manager).set(taskB.id, taskB)
getPendingByParent(manager).set(taskA.parentSessionID, new Set([taskA.id, taskB.id]))
getTasks(manager).set(taskC.id, taskC)
getPendingByParent(manager).set(taskA.parentSessionID, new Set([taskA.id, taskB.id, taskC.id]))
// when
await notifyParentSessionForTest(manager, taskA)
@@ -177,8 +178,23 @@ describe("BackgroundManager.notifyParentSession cleanup scheduling", () => {
// then
expect(fakeTimers.getDelay(taskATimer)).toBeUndefined()
expect(getTasks(manager).has(taskA.id)).toBe(false)
expect(getTasks(manager).has(taskA.id)).toBe(true)
expect(getTasks(manager).get(taskB.id)).toBe(taskB)
expect(getTasks(manager).get(taskC.id)).toBe(taskC)
// when
taskB.status = "completed"
taskB.completedAt = new Date()
taskC.status = "completed"
taskC.completedAt = new Date()
await notifyParentSessionForTest(manager, taskB)
await notifyParentSessionForTest(manager, taskC)
const rescheduledTaskATimer = getRequiredTimer(manager, taskA.id)
expect(fakeTimers.getDelay(rescheduledTaskATimer)).toBe(TASK_CLEANUP_DELAY_MS)
fakeTimers.run(rescheduledTaskATimer)
// then
expect(getTasks(manager).has(taskA.id)).toBe(false)
})
})

View File

@@ -1,5 +1,5 @@
declare const require: (name: string) => any
const { describe, it, expect, mock } = require("bun:test")
const { describe, it, expect, mock, spyOn, beforeEach, afterEach } = require("bun:test")
import { checkAndInterruptStaleTasks, pruneStaleTasksAndNotifications } from "./task-poller"
import type { BackgroundTask } from "./types"
@@ -29,6 +29,18 @@ describe("checkAndInterruptStaleTasks", () => {
...overrides,
}
}
const originalDateNow = Date.now
let fixedTime: number
beforeEach(() => {
fixedTime = Date.now()
spyOn(globalThis.Date, "now").mockReturnValue(fixedTime)
})
afterEach(() => {
Date.now = originalDateNow
})
it("should interrupt tasks with lastUpdate exceeding stale timeout", async () => {
//#given
@@ -117,13 +129,13 @@ describe("checkAndInterruptStaleTasks", () => {
})
it("should use DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS when messageStalenessTimeoutMs is not configured", async () => {
//#given — task started 35 minutes ago, no config for messageStalenessTimeoutMs
//#given — task started 65 minutes ago, no config for messageStalenessTimeoutMs
const task = createRunningTask({
startedAt: new Date(Date.now() - 35 * 60 * 1000),
startedAt: new Date(Date.now() - 65 * 60 * 1000),
progress: undefined,
})
//#when — default is 30 minutes (1_800_000ms)
//#when — default is 60 minutes (3_600_000ms)
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
@@ -417,6 +429,56 @@ describe("checkAndInterruptStaleTasks", () => {
expect(task.status).toBe("cancelled")
expect(onTaskInterrupted).toHaveBeenCalledWith(task)
})
it('should NOT protect task when session has terminal non-idle status like "interrupted"', async () => {
//#given — lastUpdate is 5min old, session is "interrupted" (terminal, not active)
const task = createRunningTask({
startedAt: new Date(Date.now() - 300_000),
progress: {
toolCalls: 2,
lastUpdate: new Date(Date.now() - 300_000),
},
})
//#when — session status is "interrupted" (terminal)
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { staleTimeoutMs: 180_000 },
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
sessionStatuses: { "ses-1": { type: "interrupted" } },
})
//#then — terminal statuses should not protect from stale timeout
expect(task.status).toBe("cancelled")
expect(task.error).toContain("Stale timeout")
})
it('should NOT protect task when session has unknown status type', async () => {
//#given — lastUpdate is 5min old, session has an unknown status
const task = createRunningTask({
startedAt: new Date(Date.now() - 300_000),
progress: {
toolCalls: 2,
lastUpdate: new Date(Date.now() - 300_000),
},
})
//#when — session has unknown status type
await checkAndInterruptStaleTasks({
tasks: [task],
client: mockClient as never,
config: { staleTimeoutMs: 180_000 },
concurrencyManager: mockConcurrencyManager as never,
notifyParentSession: mockNotify,
sessionStatuses: { "ses-1": { type: "some-weird-status" } },
})
//#then — unknown statuses should not protect from stale timeout
expect(task.status).toBe("cancelled")
expect(task.error).toContain("Stale timeout")
})
})
describe("pruneStaleTasksAndNotifications", () => {

View File

@@ -9,12 +9,12 @@ import {
DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS,
DEFAULT_STALE_TIMEOUT_MS,
MIN_RUNTIME_BEFORE_STALE_MS,
TERMINAL_TASK_TTL_MS,
TASK_TTL_MS,
} from "./constants"
import { removeTaskToastTracking } from "./remove-task-toast-tracking"
const TERMINAL_TASK_TTL_MS = 30 * 60 * 1000
import { isActiveSessionStatus } from "./session-status-classifier"
const TERMINAL_TASK_STATUSES = new Set<BackgroundTask["status"]>([
"completed",
"error",
@@ -121,7 +121,7 @@ export async function checkAndInterruptStaleTasks(args: {
if (!startedAt || !sessionID) continue
const sessionStatus = sessionStatuses?.[sessionID]?.type
const sessionIsRunning = sessionStatus !== undefined && sessionStatus !== "idle"
const sessionIsRunning = sessionStatus !== undefined && isActiveSessionStatus(sessionStatus)
const runtime = now - startedAt.getTime()
if (!task.progress?.lastUpdate) {
@@ -130,7 +130,7 @@ export async function checkAndInterruptStaleTasks(args: {
const staleMinutes = Math.round(runtime / 60000)
task.status = "cancelled"
task.error = `Stale timeout (no activity for ${staleMinutes}min since start)`
task.error = `Stale timeout (no activity for ${staleMinutes}min since start). This is a FINAL cancellation - do NOT create a replacement task. If the timeout is too short, increase 'background_task.staleTimeoutMs' in .opencode/oh-my-opencode.json.`
task.completedAt = new Date()
if (task.concurrencyKey) {
@@ -159,10 +159,10 @@ export async function checkAndInterruptStaleTasks(args: {
if (timeSinceLastUpdate <= staleTimeoutMs) continue
if (task.status !== "running") continue
const staleMinutes = Math.round(timeSinceLastUpdate / 60000)
task.status = "cancelled"
task.error = `Stale timeout (no activity for ${staleMinutes}min)`
task.completedAt = new Date()
const staleMinutes = Math.round(timeSinceLastUpdate / 60000)
task.status = "cancelled"
task.error = `Stale timeout (no activity for ${staleMinutes}min). This is a FINAL cancellation - do NOT create a replacement task. If the timeout is too short, increase 'background_task.staleTimeoutMs' in .opencode/oh-my-opencode.json.`
task.completedAt = new Date()
if (task.concurrencyKey) {
concurrencyManager.release(task.concurrencyKey)

View File

@@ -9,9 +9,17 @@ export type BackgroundTaskStatus =
| "cancelled"
| "interrupt"
export interface ToolCallWindow {
lastSignature: string
consecutiveCount: number
threshold: number
}
export interface TaskProgress {
toolCalls: number
lastTool?: string
toolCallWindow?: ToolCallWindow
countedToolPartIDs?: Set<string>
lastUpdate: Date
lastMessage?: string
lastMessageAt?: Date

View File

@@ -1,3 +1,4 @@
export * from "./types"
export * from "./constants"
export * from "./storage"
export * from "./top-level-task"

View File

@@ -11,8 +11,11 @@ import {
getPlanName,
createBoulderState,
findPrometheusPlans,
getTaskSessionState,
upsertTaskSessionState,
} from "./storage"
import type { BoulderState } from "./types"
import { readCurrentTopLevelTask } from "./top-level-task"
describe("boulder-state", () => {
const TEST_DIR = join(tmpdir(), "boulder-state-test-" + Date.now())
@@ -134,6 +137,24 @@ describe("boulder-state", () => {
expect(result?.session_ids).toEqual(["session-1", "session-2"])
expect(result?.plan_name).toBe("my-plan")
})
test("should default task_sessions to empty object when missing from JSON", () => {
// given - boulder.json without task_sessions field
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-01T00:00:00Z",
session_ids: ["session-1"],
plan_name: "plan",
}))
// when
const result = readBoulderState(TEST_DIR)
// then
expect(result).not.toBeNull()
expect(result!.task_sessions).toEqual({})
})
})
describe("writeBoulderState", () => {
@@ -249,6 +270,115 @@ describe("boulder-state", () => {
})
})
describe("task session state", () => {
test("should persist and read preferred session for a top-level plan task", () => {
// given - existing boulder state
const state: BoulderState = {
active_plan: "/plan.md",
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "plan",
}
writeBoulderState(TEST_DIR, state)
// when
upsertTaskSessionState(TEST_DIR, {
taskKey: "todo:1",
taskLabel: "1",
taskTitle: "Implement auth flow",
sessionId: "ses_task_123",
agent: "sisyphus-junior",
category: "deep",
})
const result = getTaskSessionState(TEST_DIR, "todo:1")
// then
expect(result).not.toBeNull()
expect(result?.session_id).toBe("ses_task_123")
expect(result?.task_title).toBe("Implement auth flow")
expect(result?.agent).toBe("sisyphus-junior")
expect(result?.category).toBe("deep")
})
test("should overwrite preferred session for the same top-level plan task", () => {
// given - existing boulder state with prior preferred session
const state: BoulderState = {
active_plan: "/plan.md",
started_at: "2026-01-02T10:00:00Z",
session_ids: ["session-1"],
plan_name: "plan",
task_sessions: {
"todo:1": {
task_key: "todo:1",
task_label: "1",
task_title: "Implement auth flow",
session_id: "ses_old",
updated_at: "2026-01-02T10:00:00Z",
},
},
}
writeBoulderState(TEST_DIR, state)
// when
upsertTaskSessionState(TEST_DIR, {
taskKey: "todo:1",
taskLabel: "1",
taskTitle: "Implement auth flow",
sessionId: "ses_new",
})
const result = getTaskSessionState(TEST_DIR, "todo:1")
// then
expect(result?.session_id).toBe("ses_new")
})
})
describe("readCurrentTopLevelTask", () => {
test("should return the first unchecked top-level task in TODOs", () => {
// given - plan with nested and top-level unchecked tasks
const planPath = join(TEST_DIR, "current-task-plan.md")
writeFileSync(planPath, `# Plan
## TODOs
- [x] 1. Finished task
- [ ] nested acceptance checkbox
- [ ] 2. Current task
## Final Verification Wave
- [ ] F1. Final review
`)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).not.toBeNull()
expect(result?.key).toBe("todo:2")
expect(result?.title).toBe("Current task")
})
test("should fall back to final-wave task when implementation tasks are complete", () => {
// given - plan with only final-wave work remaining
const planPath = join(TEST_DIR, "final-wave-current-task-plan.md")
writeFileSync(planPath, `# Plan
## TODOs
- [x] 1. Finished task
## Final Verification Wave
- [ ] F1. Final review
`)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).not.toBeNull()
expect(result?.key).toBe("final-wave:f1")
expect(result?.title).toBe("Final review")
})
})
describe("getPlanProgress", () => {
test("should count completed and uncompleted checkboxes", () => {
// given - plan file with checkboxes
@@ -351,7 +481,7 @@ describe("boulder-state", () => {
expect(progress.isComplete).toBe(true)
})
test("should return isComplete true for empty plan", () => {
test("should return isComplete false for empty plan", () => {
// given - plan with no checkboxes
const planPath = join(TEST_DIR, "empty-plan.md")
writeFileSync(planPath, "# Plan\nNo tasks here")
@@ -361,7 +491,7 @@ describe("boulder-state", () => {
// then
expect(progress.total).toBe(0)
expect(progress.isComplete).toBe(true)
expect(progress.isComplete).toBe(false)
})
test("should handle non-existent file", () => {

View File

@@ -6,9 +6,11 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"
import { dirname, join, basename } from "node:path"
import type { BoulderState, PlanProgress } from "./types"
import type { BoulderState, PlanProgress, TaskSessionState } from "./types"
import { BOULDER_DIR, BOULDER_FILE, PROMETHEUS_PLANS_DIR } from "./constants"
const RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"])
export function getBoulderFilePath(directory: string): string {
return join(directory, BOULDER_DIR, BOULDER_FILE)
}
@@ -29,6 +31,9 @@ export function readBoulderState(directory: string): BoulderState | null {
if (!Array.isArray(parsed.session_ids)) {
parsed.session_ids = []
}
if (!parsed.task_sessions || typeof parsed.task_sessions !== "object" || Array.isArray(parsed.task_sessions)) {
parsed.task_sessions = {}
}
return parsed as BoulderState
} catch {
return null
@@ -59,10 +64,13 @@ export function appendSessionId(directory: string, sessionId: string): BoulderSt
if (!Array.isArray(state.session_ids)) {
state.session_ids = []
}
const originalSessionIds = [...state.session_ids]
state.session_ids.push(sessionId)
if (writeBoulderState(directory, state)) {
return state
}
state.session_ids = originalSessionIds
return null
}
return state
@@ -82,6 +90,54 @@ export function clearBoulderState(directory: string): boolean {
}
}
export function getTaskSessionState(directory: string, taskKey: string): TaskSessionState | null {
const state = readBoulderState(directory)
if (!state?.task_sessions) {
return null
}
return state.task_sessions[taskKey] ?? null
}
export function upsertTaskSessionState(
directory: string,
input: {
taskKey: string
taskLabel: string
taskTitle: string
sessionId: string
agent?: string
category?: string
},
): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
if (RESERVED_KEYS.has(input.taskKey)) {
return null
}
const taskSessions = state.task_sessions ?? {}
taskSessions[input.taskKey] = {
task_key: input.taskKey,
task_label: input.taskLabel,
task_title: input.taskTitle,
session_id: input.sessionId,
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.category !== undefined ? { category: input.category } : {}),
updated_at: new Date().toISOString(),
}
state.task_sessions = taskSessions
if (writeBoulderState(directory, state)) {
return state
}
return null
}
/**
* Find Prometheus plan files for this project.
* Prometheus stores plans at: {project}/.sisyphus/plans/{name}.md
@@ -130,7 +186,7 @@ export function getPlanProgress(planPath: string): PlanProgress {
return {
total,
completed,
isComplete: total === 0 || completed === total,
isComplete: total > 0 && completed === total,
}
} catch {
return { total: 0, completed: 0, isComplete: true }

View File

@@ -0,0 +1,268 @@
import { describe, expect, test } from "bun:test"
import { writeFileSync } from "node:fs"
import { join } from "node:path"
import { tmpdir } from "node:os"
import { readCurrentTopLevelTask } from "./top-level-task"
function writePlanFile(fileName: string, content: string): string {
const planPath = join(tmpdir(), fileName)
writeFileSync(planPath, content, "utf-8")
return planPath
}
describe("readCurrentTopLevelTask", () => {
test("returns first unchecked top-level task in TODOs", () => {
// given
const planPath = writePlanFile(
`top-level-task-happy-${Date.now()}.md`,
`# Plan
## TODOs
- [x] 1. Done task
- [ ] 2. Current task
## Final Verification Wave
- [ ] F1. Final review
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toEqual({
key: "todo:2",
section: "todo",
label: "2",
title: "Current task",
})
})
test("returns null when all tasks are checked", () => {
// given
const planPath = writePlanFile(
`top-level-task-all-checked-${Date.now()}.md`,
`# Plan
## TODOs
- [x] 1. Done task
- [x] 2. Another done task
## Final Verification Wave
- [x] F1. Final done review
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toBeNull()
})
test("returns null for empty plan file", () => {
// given
const planPath = writePlanFile(`top-level-task-empty-${Date.now()}.md`, "")
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toBeNull()
})
test("returns null when plan file does not exist", () => {
// given
const planPath = join(tmpdir(), `top-level-task-missing-${Date.now()}.md`)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toBeNull()
})
test("skips nested or indented checkboxes", () => {
// given
const planPath = writePlanFile(
`top-level-task-nested-${Date.now()}.md`,
`# Plan
## TODOs
- [x] 1. Done task
- [ ] nested should be ignored
- [ ] 2. Top-level pending
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result?.key).toBe("todo:2")
})
test("falls back to Final Verification Wave when TODOs are all checked", () => {
// given
const planPath = writePlanFile(
`top-level-task-fallback-${Date.now()}.md`,
`# Plan
## TODOs
- [x] 1. Done task
- [x] 2. Done task
## Final Verification Wave
- [ ] F1. Final review pending
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toEqual({
key: "final-wave:f1",
section: "final-wave",
label: "F1",
title: "Final review pending",
})
})
test("selects the first unchecked task among mixed checked and unchecked TODOs", () => {
// given
const planPath = writePlanFile(
`top-level-task-mixed-${Date.now()}.md`,
`# Plan
## TODOs
- [x] 1. Done task
- [ ] 2. First unchecked
- [ ] 3. Second unchecked
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result?.key).toBe("todo:2")
expect(result?.title).toBe("First unchecked")
})
test("ignores malformed labels and continues to next unchecked task", () => {
// given
const planPath = writePlanFile(
`top-level-task-malformed-${Date.now()}.md`,
`# Plan
## TODOs
- [ ] no number prefix
- [ ] 2. Valid task after malformed label
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toEqual({
key: "todo:2",
section: "todo",
label: "2",
title: "Valid task after malformed label",
})
})
test("supports unchecked tasks with asterisk bullets", () => {
// given
const planPath = writePlanFile(
`top-level-task-asterisk-${Date.now()}.md`,
`# Plan
## TODOs
* [ ] 1. Task using asterisk bullet
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result?.key).toBe("todo:1")
expect(result?.title).toBe("Task using asterisk bullet")
})
test("returns final-wave task when plan has only Final Verification Wave section", () => {
// given
const planPath = writePlanFile(
`top-level-task-final-only-${Date.now()}.md`,
`# Plan
## Final Verification Wave
- [ ] F2. Final-only task
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result).toEqual({
key: "final-wave:f2",
section: "final-wave",
label: "F2",
title: "Final-only task",
})
})
test("returns the first unchecked task when multiple unchecked tasks exist", () => {
// given
const planPath = writePlanFile(
`top-level-task-multiple-${Date.now()}.md`,
`# Plan
## TODOs
- [ ] 1. First unchecked task
- [ ] 2. Second unchecked task
- [ ] 3. Third unchecked task
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result?.label).toBe("1")
expect(result?.title).toBe("First unchecked task")
})
test("ignores unchecked content in non-target sections during section transitions", () => {
// given
const planPath = writePlanFile(
`top-level-task-sections-${Date.now()}.md`,
`# Plan
## Notes
- [ ] 99. Should be ignored because section is not tracked
## TODOs
- [x] 1. Done implementation task
## Decisions
- [ ] 100. Should also be ignored
## Final Verification Wave
- [ ] F3. Final verification task
`,
)
// when
const result = readCurrentTopLevelTask(planPath)
// then
expect(result?.key).toBe("final-wave:f3")
expect(result?.section).toBe("final-wave")
})
})

View File

@@ -0,0 +1,77 @@
import { existsSync, readFileSync } from "node:fs"
import type { TopLevelTaskRef } from "./types"
const TODO_HEADING_PATTERN = /^##\s+TODOs\b/i
const FINAL_VERIFICATION_HEADING_PATTERN = /^##\s+Final Verification Wave\b/i
const SECOND_LEVEL_HEADING_PATTERN = /^##\s+/
const UNCHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[\s*\]\s*(.+)$/
const TODO_TASK_PATTERN = /^(\d+)\.\s+(.+)$/
const FINAL_WAVE_TASK_PATTERN = /^(F\d+)\.\s+(.+)$/i
type PlanSection = "todo" | "final-wave" | "other"
function buildTaskRef(
section: "todo" | "final-wave",
taskLabel: string,
): TopLevelTaskRef | null {
const pattern = section === "todo" ? TODO_TASK_PATTERN : FINAL_WAVE_TASK_PATTERN
const match = taskLabel.match(pattern)
if (!match) {
return null
}
const rawLabel = match[1]
const title = match[2].trim()
return {
key: `${section}:${rawLabel.toLowerCase()}`,
section,
label: rawLabel,
title,
}
}
export function readCurrentTopLevelTask(planPath: string): TopLevelTaskRef | null {
if (!existsSync(planPath)) {
return null
}
try {
const content = readFileSync(planPath, "utf-8")
const lines = content.split(/\r?\n/)
let section: PlanSection = "other"
for (const line of lines) {
if (SECOND_LEVEL_HEADING_PATTERN.test(line)) {
section = TODO_HEADING_PATTERN.test(line)
? "todo"
: FINAL_VERIFICATION_HEADING_PATTERN.test(line)
? "final-wave"
: "other"
}
const uncheckedTaskMatch = line.match(UNCHECKED_CHECKBOX_PATTERN)
if (!uncheckedTaskMatch) {
continue
}
if (uncheckedTaskMatch[1].length > 0) {
continue
}
if (section !== "todo" && section !== "final-wave") {
continue
}
const taskRef = buildTaskRef(section, uncheckedTaskMatch[2].trim())
if (taskRef) {
return taskRef
}
}
return null
} catch {
return null
}
}

View File

@@ -18,6 +18,8 @@ export interface BoulderState {
agent?: string
/** Absolute path to the git worktree root where work happens */
worktree_path?: string
/** Preferred reusable subagent sessions keyed by current top-level plan task */
task_sessions?: Record<string, TaskSessionState>
}
export interface PlanProgress {
@@ -28,3 +30,31 @@ export interface PlanProgress {
/** Whether all tasks are done */
isComplete: boolean
}
export interface TaskSessionState {
/** Stable identifier for the current top-level plan task (e.g. todo:1 / final-wave:F1) */
task_key: string
/** Original task label from the plan file */
task_label: string
/** Full task title from the plan file */
task_title: string
/** Preferred reusable subagent session */
session_id: string
/** Agent associated with the task session, when known */
agent?: string
/** Category associated with the task session, when known */
category?: string
/** Last update timestamp */
updated_at: string
}
export interface TopLevelTaskRef {
/** Stable identifier for the current top-level plan task */
key: string
/** Task section in the Prometheus plan */
section: "todo" | "final-wave"
/** Original label token (e.g. 1 / F1) */
label: string
/** Full task title extracted from the checkbox line */
title: string
}

View File

@@ -7,7 +7,7 @@ export const START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
- \`--worktree <path>\` (optional): absolute path to an existing git worktree to work in
- If specified and valid: hook pre-sets worktree_path in boulder.json
- If specified but invalid: you must run \`git worktree add <path> <branch>\` first
- If omitted: you MUST choose or create a worktree (see Worktree Setup below)
- If omitted: work directly in the current project directory (no worktree)
## WHAT TO DO
@@ -24,7 +24,7 @@ export const START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
- If ONE plan: auto-select it
- If MULTIPLE plans: show list with timestamps, ask user to select
4. **Worktree Setup** (when \`worktree_path\` not already set in boulder.json):
4. **Worktree Setup** (ONLY when \`--worktree\` was explicitly specified and \`worktree_path\` not already set in boulder.json):
1. \`git worktree list --porcelain\` — see available worktrees
2. Create: \`git worktree add <absolute-path> <branch-or-HEAD>\`
3. Update boulder.json to add \`"worktree_path": "<absolute-path>"\`
@@ -86,6 +86,38 @@ Reading plan and beginning execution...
- The session_id is injected by the hook - use it directly
- Always update boulder.json BEFORE starting work
- Always set worktree_path in boulder.json before executing any tasks
- If worktree_path is set in boulder.json, all work happens inside that worktree directory
- Read the FULL plan file before delegating any tasks
- Follow atlas delegation protocols (7-section format)`
- Follow atlas delegation protocols (7-section format)
## TASK BREAKDOWN (MANDATORY)
After reading the plan file, you MUST decompose every plan task into granular, implementation-level sub-steps and register ALL of them as task/todo items BEFORE starting any work.
**How to break down**:
- Each plan checkbox item (e.g., \`- [ ] Add user authentication\`) must be split into concrete, actionable sub-tasks
- Sub-tasks should be specific enough that each one touches a clear set of files/functions
- Include: file to modify, what to change, expected behavior, and how to verify
- Do NOT leave any task vague — "implement feature X" is NOT acceptable; "add validateToken() to src/auth/middleware.ts that checks JWT expiry and returns 401" IS acceptable
**Example breakdown**:
Plan task: \`- [ ] Add rate limiting to API\`
→ Todo items:
1. Create \`src/middleware/rate-limiter.ts\` with sliding window algorithm (max 100 req/min per IP)
2. Add RateLimiter middleware to \`src/app.ts\` router chain, before auth middleware
3. Add rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) to response in \`rate-limiter.ts\`
4. Add test: verify 429 response after exceeding limit in \`src/middleware/rate-limiter.test.ts\`
5. Add test: verify headers are present on normal responses
Register these as task/todo items so progress is tracked and visible throughout the session.
## WORKTREE COMPLETION
When working in a worktree (\`worktree_path\` is set in boulder.json) and ALL plan tasks are complete:
1. Commit all remaining changes in the worktree
2. Switch to the main working directory (the original repo, NOT the worktree)
3. Merge the worktree branch into the current branch: \`git merge <worktree-branch>\`
4. If merge succeeds, clean up: \`git worktree remove <worktree-path>\`
5. Remove the boulder.json state
This is the DEFAULT behavior when \`--worktree\` was used. Skip merge only if the user explicitly instructs otherwise (e.g., asks to create a PR instead).`

View File

@@ -16,7 +16,7 @@ This guide covers installation for all platforms: macOS, Linux, and Windows.
git clone https://github.com/sawyerhood/dev-browser /tmp/dev-browser-skill
# Copy to skills directory (adjust path as needed)
# For oh-my-openagent: already bundled
# For oh-my-opencode: already bundled
# For manual installation:
mkdir -p ~/.config/opencode/skills
cp -r /tmp/dev-browser-skill/skills/dev-browser ~/.config/opencode/skills/dev-browser

View File

@@ -0,0 +1,112 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
import type { PluginComponentsResult } from "./loader"
describe("loadAllPluginComponents", () => {
const originalEnv = { ...process.env }
beforeEach(() => {
delete process.env.OPENCODE_DISABLE_CLAUDE_CODE
delete process.env.OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS
})
afterEach(() => {
process.env = { ...originalEnv }
})
describe("when OPENCODE_DISABLE_CLAUDE_CODE is set to 'true'", () => {
it("returns empty result without loading any plugins", async () => {
// given
process.env.OPENCODE_DISABLE_CLAUDE_CODE = "true"
// when
const { loadAllPluginComponents } = await import("./loader")
const result: PluginComponentsResult = await loadAllPluginComponents()
// then
expect(result.commands).toEqual({})
expect(result.skills).toEqual({})
expect(result.agents).toEqual({})
expect(result.mcpServers).toEqual({})
expect(result.hooksConfigs).toEqual([])
expect(result.plugins).toEqual([])
expect(result.errors).toEqual([])
})
})
describe("when OPENCODE_DISABLE_CLAUDE_CODE is set to '1'", () => {
it("returns empty result without loading any plugins", async () => {
// given
process.env.OPENCODE_DISABLE_CLAUDE_CODE = "1"
// when
const { loadAllPluginComponents } = await import("./loader")
const result: PluginComponentsResult = await loadAllPluginComponents()
// then
expect(result.commands).toEqual({})
expect(result.plugins).toEqual([])
})
})
describe("when OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS is set to 'true'", () => {
it("returns empty result without loading any plugins", async () => {
// given
process.env.OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS = "true"
// when
const { loadAllPluginComponents } = await import("./loader")
const result: PluginComponentsResult = await loadAllPluginComponents()
// then
expect(result.commands).toEqual({})
expect(result.plugins).toEqual([])
})
})
describe("when OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS is set to '1'", () => {
it("returns empty result without loading any plugins", async () => {
// given
process.env.OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS = "1"
// when
const { loadAllPluginComponents } = await import("./loader")
const result: PluginComponentsResult = await loadAllPluginComponents()
// then
expect(result.commands).toEqual({})
expect(result.plugins).toEqual([])
})
})
describe("when neither env var is set", () => {
it("does not skip plugin loading", async () => {
// given
delete process.env.OPENCODE_DISABLE_CLAUDE_CODE
delete process.env.OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS
// when
const { loadAllPluginComponents } = await import("./loader")
const result: PluginComponentsResult = await loadAllPluginComponents()
// then — should attempt to load (may find 0 plugins, but shouldn't early-return)
expect(result).toBeDefined()
expect(result).toHaveProperty("commands")
expect(result).toHaveProperty("plugins")
})
})
describe("when env var is set to unrecognized value", () => {
it("does not skip plugin loading", async () => {
// given
process.env.OPENCODE_DISABLE_CLAUDE_CODE = "yes"
// when
const { loadAllPluginComponents } = await import("./loader")
const result: PluginComponentsResult = await loadAllPluginComponents()
// then — "yes" is not "true" or "1", should not skip
expect(result).toBeDefined()
expect(result).toHaveProperty("plugins")
})
})
})

View File

@@ -27,7 +27,26 @@ export interface PluginComponentsResult {
errors: PluginLoadError[]
}
function isClaudeCodePluginsDisabled(): boolean {
const disableFlag = process.env.OPENCODE_DISABLE_CLAUDE_CODE
const disablePluginsFlag = process.env.OPENCODE_DISABLE_CLAUDE_CODE_PLUGINS
return disableFlag === "true" || disableFlag === "1" || disablePluginsFlag === "true" || disablePluginsFlag === "1"
}
export async function loadAllPluginComponents(options?: PluginLoaderOptions): Promise<PluginComponentsResult> {
if (isClaudeCodePluginsDisabled()) {
log("Claude Code plugin loading disabled via OPENCODE_DISABLE_CLAUDE_CODE env var")
return {
commands: {},
skills: {},
agents: {},
mcpServers: {},
hooksConfigs: [],
plugins: [],
errors: [],
}
}
const { plugins, errors } = discoverInstalledPlugins(options)
const [commands, skills, agents, mcpServers, hooksConfigs] = await Promise.all([

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