Compare commits
398 Commits
v3.0.0-bet
...
omo-ultraw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682e11f126 | ||
|
|
616ee7805c | ||
|
|
aed5c33ae3 | ||
|
|
18601c5c70 | ||
|
|
a013e10d44 | ||
|
|
cc7732881a | ||
|
|
af91b5b662 | ||
|
|
862675c230 | ||
|
|
d7713ca8be | ||
|
|
3fb6edb269 | ||
|
|
41dd4ce22a | ||
|
|
4f26e99ee7 | ||
|
|
b405494808 | ||
|
|
839a4c5316 | ||
|
|
08d43efdb0 | ||
|
|
061a5f5132 | ||
|
|
d4acd23630 | ||
|
|
c77c9ceb53 | ||
|
|
8c2625cfb0 | ||
|
|
3ced20d1ab | ||
|
|
fb02cc9e95 | ||
|
|
80ee52fe3b | ||
|
|
2f7e188cb5 | ||
|
|
f8be01c6dd | ||
|
|
0dbec08923 | ||
|
|
691fa8b815 | ||
|
|
a73d806d4e | ||
|
|
a424f81cd5 | ||
|
|
1187a02020 | ||
|
|
3074434887 | ||
|
|
6bb2854162 | ||
|
|
e08904a27a | ||
|
|
0188d69233 | ||
|
|
2c74f608f0 | ||
|
|
baefd16b3f | ||
|
|
b1b4578906 | ||
|
|
9d20a5b11c | ||
|
|
d2d8d1a782 | ||
|
|
10bdb6c694 | ||
|
|
5f243e2d3a | ||
|
|
82a47ff928 | ||
|
|
c06f38693e | ||
|
|
6e9cb7ecd8 | ||
|
|
b731399edf | ||
|
|
0a28f6a790 | ||
|
|
4e529b74e0 | ||
|
|
90eec0a369 | ||
|
|
3b5d18e6bf | ||
|
|
67aeb9cb8c | ||
|
|
b1c1f02172 | ||
|
|
2b39d119cd | ||
|
|
afa2ece847 | ||
|
|
390c25197f | ||
|
|
9e07b143df | ||
|
|
ad95880198 | ||
|
|
86088d3a6e | ||
|
|
ae8a6c5eb8 | ||
|
|
db538c7e6b | ||
|
|
dfed2abd3e | ||
|
|
300a3fdc14 | ||
|
|
c993cf007f | ||
|
|
3d7de0a050 | ||
|
|
8e19ffdce4 | ||
|
|
456d9cea65 | ||
|
|
30f893b766 | ||
|
|
c905e1cb7a | ||
|
|
d3e2b36e3d | ||
|
|
5f0b6d49f5 | ||
|
|
b45408dd9c | ||
|
|
6c8527f29b | ||
|
|
cd4da93bf2 | ||
|
|
71b2f1518a | ||
|
|
dcda8769cc | ||
|
|
a94fbadd57 | ||
|
|
23b49c4a5c | ||
|
|
b4973954e3 | ||
|
|
6d50fbe563 | ||
|
|
9850dd0f6e | ||
|
|
34aaef2219 | ||
|
|
faca80caa9 | ||
|
|
0c3fbd724b | ||
|
|
c7455708f8 | ||
|
|
bffa1ad43d | ||
|
|
6560dedd4c | ||
|
|
b7e32a99f2 | ||
|
|
a06e656565 | ||
|
|
30ed086c40 | ||
|
|
7c15b06da7 | ||
|
|
0e7ee2ac30 | ||
|
|
ca93d2f0fe | ||
|
|
3ab4529bc7 | ||
|
|
9d3e152b19 | ||
|
|
68c8f3dda7 | ||
|
|
03f6e72c9b | ||
|
|
4fd9f0fd04 | ||
|
|
4413336724 | ||
|
|
895f366a11 | ||
|
|
acc19fcd41 | ||
|
|
68e0a32183 | ||
|
|
dee89c1556 | ||
|
|
315c75c51e | ||
|
|
3dd80889a5 | ||
|
|
8f6ed5b20f | ||
|
|
01500f1ebe | ||
|
|
48f6c5e06d | ||
|
|
3e32afe646 | ||
|
|
d11c4a1f81 | ||
|
|
5558ddf468 | ||
|
|
aa03d9b811 | ||
|
|
28a0dd06c7 | ||
|
|
995b7751af | ||
|
|
5087788f66 | ||
|
|
19524c8a27 | ||
|
|
fbb4d46945 | ||
|
|
5dc8d577a4 | ||
|
|
c249763d7e | ||
|
|
b2d618e851 | ||
|
|
6f348a8a5c | ||
|
|
1da0adcbe8 | ||
|
|
8a9d966a3d | ||
|
|
76f8c500cb | ||
|
|
388516bcc5 | ||
|
|
8dff875929 | ||
|
|
966cc90a02 | ||
|
|
1d27d78127 | ||
|
|
38156d49f3 | ||
|
|
897eea0263 | ||
|
|
9b59ef66e4 | ||
|
|
0d938059f9 | ||
|
|
9d35f23725 | ||
|
|
aa1646f82c | ||
|
|
e47ab084fd | ||
|
|
baf6358736 | ||
|
|
488c89156b | ||
|
|
c4957a469d | ||
|
|
d481c596bd | ||
|
|
655d511294 | ||
|
|
7dedd6cf90 | ||
|
|
bd18f231f5 | ||
|
|
de439edc22 | ||
|
|
04500bae7d | ||
|
|
1cb6b3de7d | ||
|
|
912a56db85 | ||
|
|
a5d9929c0a | ||
|
|
7f43f160b5 | ||
|
|
af67bc8592 | ||
|
|
c74d79e28a | ||
|
|
fc5298d778 | ||
|
|
3e8e3db961 | ||
|
|
6fa5cac616 | ||
|
|
158ccabf24 | ||
|
|
2efbf2650f | ||
|
|
acded4ba2a | ||
|
|
911e43445f | ||
|
|
3049e1ebfb | ||
|
|
62921b9e44 | ||
|
|
cd23f7ab7d | ||
|
|
518dceac72 | ||
|
|
19f43e30c8 | ||
|
|
b3be9f33c6 | ||
|
|
430098856a | ||
|
|
5932f5f94f | ||
|
|
fcf2e32071 | ||
|
|
19827dac70 | ||
|
|
3ed1c6644e | ||
|
|
cf6e714946 | ||
|
|
383f43548b | ||
|
|
26b1c67964 | ||
|
|
7e065dfe12 | ||
|
|
8429da02b8 | ||
|
|
ab51f5d39f | ||
|
|
3ee519c7b0 | ||
|
|
c9b86b7815 | ||
|
|
9b6d8f629a | ||
|
|
6a2f43858a | ||
|
|
601ea32a1c | ||
|
|
8f31211c75 | ||
|
|
04f2b513c6 | ||
|
|
8ebc933118 | ||
|
|
a67a35aea8 | ||
|
|
9d66b80709 | ||
|
|
5c7eb02d5b | ||
|
|
68aa913499 | ||
|
|
3a79b8761b | ||
|
|
da416b362b | ||
|
|
90054b28ad | ||
|
|
892b245779 | ||
|
|
aead4aebd2 | ||
|
|
bccc943173 | ||
|
|
05904ca617 | ||
|
|
3af30b0a21 | ||
|
|
b55fd8d76f | ||
|
|
208af055ef | ||
|
|
0aa8f486af | ||
|
|
a5db86ee15 | ||
|
|
14f450bd25 | ||
|
|
5a1da39def | ||
|
|
24d065c43a | ||
|
|
fd72ce5ce7 | ||
|
|
043b1a3377 | ||
|
|
512952f66d | ||
|
|
d9723e76ab | ||
|
|
212baa6674 | ||
|
|
1c76e0513a | ||
|
|
c8cc94cd3c | ||
|
|
20cca35157 | ||
|
|
81d27afadb | ||
|
|
6cb2f3031c | ||
|
|
f116ea1d43 | ||
|
|
6aa0674000 | ||
|
|
2b828624a0 | ||
|
|
e60ccb93fb | ||
|
|
aa244e8098 | ||
|
|
6f60f03433 | ||
|
|
b8a0eee92d | ||
|
|
1486ebbc87 | ||
|
|
063c759275 | ||
|
|
6e9ebaf3ee | ||
|
|
0e1d4e52e1 | ||
|
|
c0fb4b79bd | ||
|
|
ec32dd65c2 | ||
|
|
04fb339622 | ||
|
|
3a22c24cf4 | ||
|
|
cf2320480f | ||
|
|
9532680879 | ||
|
|
2a945ddbf5 | ||
|
|
58bb92134d | ||
|
|
f1a279a10a | ||
|
|
faf172a91d | ||
|
|
04633ba208 | ||
|
|
58459e692b | ||
|
|
894a0fa849 | ||
|
|
21c7d29c1d | ||
|
|
ba93c42943 | ||
|
|
5c7dd40751 | ||
|
|
acc7b8b2f7 | ||
|
|
8c90838f3b | ||
|
|
0b784d24f2 | ||
|
|
444fbe396a | ||
|
|
ad86e58077 | ||
|
|
7ed7bf5c66 | ||
|
|
1c562a95d5 | ||
|
|
c2247aec60 | ||
|
|
1c9588ff33 | ||
|
|
5d73ac819d | ||
|
|
dfc57d0426 | ||
|
|
12c9029ed7 | ||
|
|
91060c35ab | ||
|
|
90292db4c4 | ||
|
|
cc4deed8ee | ||
|
|
4e4288807d | ||
|
|
629a4d3e1b | ||
|
|
8806ed17dc | ||
|
|
e2f8729731 | ||
|
|
bee8b3736d | ||
|
|
37e1a065d8 | ||
|
|
fc47a7a490 | ||
|
|
9b12e2a9b5 | ||
|
|
3062277a99 | ||
|
|
7093583ec5 | ||
|
|
ec61df8c17 | ||
|
|
6312d2da52 | ||
|
|
810dd93da2 | ||
|
|
1a901a50ac | ||
|
|
f8155e7d45 | ||
|
|
39d2d44e22 | ||
|
|
15c4637e0a | ||
|
|
262c7118da | ||
|
|
599fad0e86 | ||
|
|
afbdf69037 | ||
|
|
af9beee83c | ||
|
|
6973a75bf2 | ||
|
|
c6d6bd197e | ||
|
|
57b10439a4 | ||
|
|
6dfe091a88 | ||
|
|
75158caded | ||
|
|
e16bbbcc05 | ||
|
|
ab3e622baa | ||
|
|
f4348885f2 | ||
|
|
2c81c8e58e | ||
|
|
3268782730 | ||
|
|
dab3e1e13f | ||
|
|
71474bb4a2 | ||
|
|
aa6355cc46 | ||
|
|
e8b1e56e5c | ||
|
|
8df56794ca | ||
|
|
6e84a14f20 | ||
|
|
7de376e24f | ||
|
|
0e18efc7e4 | ||
|
|
e15677efd5 | ||
|
|
45b2782d55 | ||
|
|
febc32d7f4 | ||
|
|
76a01d4942 | ||
|
|
83bcf70bdd | ||
|
|
91d85d3df7 | ||
|
|
638a314f6d | ||
|
|
ff92a4caa2 | ||
|
|
dda502a697 | ||
|
|
2690301833 | ||
|
|
3f002ff50c | ||
|
|
bb14537b14 | ||
|
|
bdbc8d73cb | ||
|
|
4b1ea1244f | ||
|
|
cc7160b3b5 | ||
|
|
440e53ad9d | ||
|
|
72098213ee | ||
|
|
aa2b052d28 | ||
|
|
0edfc7f36a | ||
|
|
4ffb9b1c93 | ||
|
|
0610ef8c77 | ||
|
|
5e27ceeb81 | ||
|
|
de3a6aae11 | ||
|
|
76211a3185 | ||
|
|
04b026dd15 | ||
|
|
54b4844d3f | ||
|
|
bc62c23a85 | ||
|
|
f4a0d5ec40 | ||
|
|
d863daceef | ||
|
|
d220654e84 | ||
|
|
e65d57285f | ||
|
|
80b4067b8e | ||
|
|
e3cc4c8cef | ||
|
|
c8175c2678 | ||
|
|
7f2eb0a568 | ||
|
|
89bde5ce64 | ||
|
|
ac192435cb | ||
|
|
ba756d7533 | ||
|
|
8b820c5374 | ||
|
|
85b7e9737c | ||
|
|
a25d48a651 | ||
|
|
3bea6a043d | ||
|
|
c70ca39661 | ||
|
|
66fd761a69 | ||
|
|
1ab97bd0a7 | ||
|
|
485bc73437 | ||
|
|
e1f3be1ab5 | ||
|
|
bc7b413bc2 | ||
|
|
2eea5e52f8 | ||
|
|
1a4106120d | ||
|
|
52acb37478 | ||
|
|
be9d6c0061 | ||
|
|
3328249950 | ||
|
|
cd3f80dd0b | ||
|
|
ffe82c1dbd | ||
|
|
0031bf7a11 | ||
|
|
516edb445c | ||
|
|
c00f210922 | ||
|
|
6865995227 | ||
|
|
d419bc302c | ||
|
|
8260824d36 | ||
|
|
65a567e0b4 | ||
|
|
00df2ba6c7 | ||
|
|
29e7595fc9 | ||
|
|
46189eef8a | ||
|
|
18e02a33be | ||
|
|
9729548a2a | ||
|
|
824da626d7 | ||
|
|
3d3d3e493b | ||
|
|
3e5265700b | ||
|
|
f1887327ee | ||
|
|
c46d57f3aa | ||
|
|
8cc995891e | ||
|
|
2c3f1bfd80 | ||
|
|
3be387d9e3 | ||
|
|
59d663dce7 | ||
|
|
52d9b30035 | ||
|
|
c4b862cbc4 | ||
|
|
96bcd9788a | ||
|
|
e05ac04e66 | ||
|
|
3c6768089f | ||
|
|
18262e7b60 | ||
|
|
7ccb8fcebb | ||
|
|
e40e42eaec | ||
|
|
7efa337586 | ||
|
|
6a4add2011 | ||
|
|
d61817bc76 | ||
|
|
8f94c59892 | ||
|
|
3a3794a0ce | ||
|
|
193c176130 | ||
|
|
4f7ce87e66 | ||
|
|
c9f893b740 | ||
|
|
e73bf8805a | ||
|
|
d872515d7c | ||
|
|
d8d274f45d | ||
|
|
4ee7deae14 | ||
|
|
5ce9c98b92 | ||
|
|
e0c507a18f | ||
|
|
732ec85e07 | ||
|
|
45fe9578ec | ||
|
|
4566094054 | ||
|
|
86e095cb6e | ||
|
|
c941b5ab7e | ||
|
|
5c88abda7c | ||
|
|
6956ce0a19 | ||
|
|
4e8106b019 | ||
|
|
f39f77d155 | ||
|
|
c910820cdb | ||
|
|
c698a5b888 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -14,6 +14,8 @@ body:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
|
||||
required: true
|
||||
- label: I have searched existing issues to avoid duplicates
|
||||
required: true
|
||||
- label: I am using the latest version of oh-my-opencode
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -14,6 +14,8 @@ body:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
|
||||
required: true
|
||||
- label: I have searched existing issues and discussions to avoid duplicates
|
||||
required: true
|
||||
- label: This feature request is specific to oh-my-opencode (not OpenCode core)
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/general.yml
vendored
2
.github/ISSUE_TEMPLATE/general.yml
vendored
@@ -14,6 +14,8 @@ body:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
|
||||
required: true
|
||||
- label: I have searched existing issues and discussions
|
||||
required: true
|
||||
- label: I have read the [documentation](https://github.com/code-yeongyu/oh-my-opencode#readme)
|
||||
|
||||
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 182 KiB |
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
@@ -4,13 +4,32 @@ on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
branches: [master, dev]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Block PRs targeting master branch
|
||||
block-master-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check PR target branch
|
||||
run: |
|
||||
if [ "${{ github.base_ref }}" = "master" ]; then
|
||||
echo "::error::PRs to master branch are not allowed. Please target the 'dev' branch instead."
|
||||
echo ""
|
||||
echo "PULL REQUESTS TO MASTER ARE BLOCKED"
|
||||
echo ""
|
||||
echo "All PRs must target the 'dev' branch."
|
||||
echo "Please close this PR and create a new one targeting 'dev'."
|
||||
exit 1
|
||||
else
|
||||
echo "PR targets '${{ github.base_ref }}' branch - OK"
|
||||
fi
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -25,8 +44,34 @@ jobs:
|
||||
env:
|
||||
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
- name: Run mock-heavy tests (isolated)
|
||||
run: |
|
||||
# These files use mock.module() which pollutes module cache
|
||||
# Run them in separate processes to prevent cross-file contamination
|
||||
bun test src/plugin-handlers
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
|
||||
- name: Run remaining tests
|
||||
run: |
|
||||
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
|
||||
bun test bin script src/cli src/config src/mcp src/index.test.ts \
|
||||
src/agents src/tools src/shared \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
src/hooks/provider-toast \
|
||||
src/hooks/session-notification \
|
||||
src/hooks/sisyphus \
|
||||
src/hooks/todo-continuation-enforcer \
|
||||
src/features/background-agent \
|
||||
src/features/builtin-commands \
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
path-to-signatures: 'signatures/cla.json'
|
||||
path-to-document: 'https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md'
|
||||
branch: 'dev'
|
||||
allowlist: bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
|
||||
allowlist: code-yeongyu,bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
|
||||
custom-notsigned-prcomment: |
|
||||
Thank you for your contribution! Before we can merge this PR, we need you to sign our [Contributor License Agreement (CLA)](https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md).
|
||||
|
||||
|
||||
211
.github/workflows/publish-platform.yml
vendored
Normal file
211
.github/workflows/publish-platform.yml
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
name: publish-platform
|
||||
run-name: "platform packages ${{ inputs.version }}"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g., 3.0.0-beta.12)"
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
description: "npm dist tag (e.g., beta, latest)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
# =============================================================================
|
||||
# Job 1: Build binaries for all platforms
|
||||
# - Windows builds on windows-latest (avoid bun cross-compile segfault)
|
||||
# - All other platforms build on ubuntu-latest
|
||||
# - Uploads compressed artifacts for the publish job
|
||||
# =============================================================================
|
||||
build:
|
||||
runs-on: ${{ matrix.platform == 'windows-x64' && 'windows-latest' || 'ubuntu-latest' }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 7
|
||||
matrix:
|
||||
platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
|
||||
|
||||
- name: Check if already published
|
||||
id: check
|
||||
run: |
|
||||
PKG_NAME="oh-my-opencode-${{ matrix.platform }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/${PKG_NAME}/${VERSION}")
|
||||
# Convert platform name for output (replace - with _)
|
||||
PLATFORM_KEY="${{ matrix.platform }}"
|
||||
PLATFORM_KEY="${PLATFORM_KEY//-/_}"
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "skip_${PLATFORM_KEY}=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ ${PKG_NAME}@${VERSION} already published"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "skip_${PLATFORM_KEY}=false" >> $GITHUB_OUTPUT
|
||||
echo "→ ${PKG_NAME}@${VERSION} needs publishing"
|
||||
fi
|
||||
|
||||
- name: Update version in package.json
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
cd packages/${{ matrix.platform }}
|
||||
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
|
||||
|
||||
- name: Build binary
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
PLATFORM="${{ matrix.platform }}"
|
||||
case "$PLATFORM" in
|
||||
darwin-arm64) TARGET="bun-darwin-arm64" ;;
|
||||
darwin-x64) TARGET="bun-darwin-x64" ;;
|
||||
linux-x64) TARGET="bun-linux-x64" ;;
|
||||
linux-arm64) TARGET="bun-linux-arm64" ;;
|
||||
linux-x64-musl) TARGET="bun-linux-x64-musl" ;;
|
||||
linux-arm64-musl) TARGET="bun-linux-arm64-musl" ;;
|
||||
windows-x64) TARGET="bun-windows-x64" ;;
|
||||
esac
|
||||
|
||||
if [ "$PLATFORM" = "windows-x64" ]; then
|
||||
OUTPUT="packages/${PLATFORM}/bin/oh-my-opencode.exe"
|
||||
else
|
||||
OUTPUT="packages/${PLATFORM}/bin/oh-my-opencode"
|
||||
fi
|
||||
|
||||
bun build src/cli/index.ts --compile --minify --target=$TARGET --outfile=$OUTPUT
|
||||
|
||||
echo "Built binary:"
|
||||
ls -lh "$OUTPUT"
|
||||
|
||||
- name: Compress binary
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
PLATFORM="${{ matrix.platform }}"
|
||||
cd packages/${PLATFORM}
|
||||
|
||||
if [ "$PLATFORM" = "windows-x64" ]; then
|
||||
# Windows: use 7z (pre-installed on windows-latest)
|
||||
7z a -tzip ../../binary-${PLATFORM}.zip bin/ package.json
|
||||
else
|
||||
# Unix: use tar.gz
|
||||
tar -czvf ../../binary-${PLATFORM}.tar.gz bin/ package.json
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
echo "Compressed artifact:"
|
||||
ls -lh binary-${PLATFORM}.*
|
||||
|
||||
- name: Upload artifact
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.platform }}
|
||||
path: |
|
||||
binary-${{ matrix.platform }}.tar.gz
|
||||
binary-${{ matrix.platform }}.zip
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
# =============================================================================
|
||||
# Job 2: Publish all platforms using OIDC/Provenance
|
||||
# - Runs on ubuntu-latest for ALL platforms (just downloading artifacts)
|
||||
# - Uses npm Trusted Publishing (OIDC) - no NODE_AUTH_TOKEN needed
|
||||
# - Fresh OIDC token at publish time avoids timeout issues
|
||||
# =============================================================================
|
||||
publish:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
platform: [darwin-arm64, darwin-x64, linux-x64, linux-arm64, linux-x64-musl, linux-arm64-musl, windows-x64]
|
||||
steps:
|
||||
- name: Check if already published
|
||||
id: check
|
||||
run: |
|
||||
PKG_NAME="oh-my-opencode-${{ matrix.platform }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/${PKG_NAME}/${VERSION}")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ ${PKG_NAME}@${VERSION} already published, skipping"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "→ ${PKG_NAME}@${VERSION} will be published"
|
||||
fi
|
||||
|
||||
- name: Download artifact
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.platform }}
|
||||
path: .
|
||||
|
||||
- name: Extract artifact
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
PLATFORM="${{ matrix.platform }}"
|
||||
mkdir -p packages/${PLATFORM}
|
||||
|
||||
if [ "$PLATFORM" = "windows-x64" ]; then
|
||||
unzip binary-${PLATFORM}.zip -d packages/${PLATFORM}/
|
||||
else
|
||||
tar -xzvf binary-${PLATFORM}.tar.gz -C packages/${PLATFORM}/
|
||||
fi
|
||||
|
||||
echo "Extracted contents:"
|
||||
ls -la packages/${PLATFORM}/
|
||||
ls -la packages/${PLATFORM}/bin/
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Publish ${{ matrix.platform }}
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
cd packages/${{ matrix.platform }}
|
||||
|
||||
TAG_ARG=""
|
||||
if [ -n "${{ inputs.dist_tag }}" ]; then
|
||||
TAG_ARG="--tag ${{ inputs.dist_tag }}"
|
||||
fi
|
||||
|
||||
npm publish --access public --provenance $TAG_ARG
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
timeout-minutes: 15
|
||||
224
.github/workflows/publish.yml
vendored
224
.github/workflows/publish.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: publish
|
||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
run-name: "${{ format('release {0}', inputs.version || inputs.bump) }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,11 +14,11 @@ on:
|
||||
- minor
|
||||
- major
|
||||
version:
|
||||
description: "Override version (e.g., 3.0.0-beta.6 for beta release). Takes precedence over bump."
|
||||
description: "Override version (e.g., 3.0.0-beta.6). Takes precedence over bump."
|
||||
required: false
|
||||
type: string
|
||||
skip_platform:
|
||||
description: "Skip platform binary packages (use when already published)"
|
||||
description: "Skip platform binary packages"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -28,6 +28,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -44,8 +45,34 @@ jobs:
|
||||
env:
|
||||
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
- name: Run mock-heavy tests (isolated)
|
||||
run: |
|
||||
# These files use mock.module() which pollutes module cache
|
||||
# Run them in separate processes to prevent cross-file contamination
|
||||
bun test src/plugin-handlers
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
|
||||
- name: Run remaining tests
|
||||
run: |
|
||||
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
|
||||
bun test bin script src/cli src/config src/mcp src/index.test.ts \
|
||||
src/agents src/tools src/shared \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
src/hooks/provider-toast \
|
||||
src/hooks/session-notification \
|
||||
src/hooks/sisyphus \
|
||||
src/hooks/todo-continuation-enforcer \
|
||||
src/features/background-agent \
|
||||
src/features/builtin-commands \
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -64,10 +91,13 @@ jobs:
|
||||
- name: Type check
|
||||
run: bun run typecheck
|
||||
|
||||
publish:
|
||||
publish-main:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, typecheck]
|
||||
if: github.repository == 'code-yeongyu/oh-my-opencode'
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
dist_tag: ${{ steps.version.outputs.dist_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -84,72 +114,152 @@ jobs:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Upgrade npm for OIDC trusted publishing
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Configure npm registry
|
||||
run: npm config set registry https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
env:
|
||||
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
|
||||
|
||||
- name: Debug environment
|
||||
- name: Calculate version
|
||||
id: version
|
||||
run: |
|
||||
echo "=== Bun version ==="
|
||||
bun --version
|
||||
echo "=== Node version ==="
|
||||
node --version
|
||||
echo "=== Current directory ==="
|
||||
pwd
|
||||
echo "=== List src/ ==="
|
||||
ls -la src/
|
||||
echo "=== package.json scripts ==="
|
||||
cat package.json | jq '.scripts'
|
||||
VERSION="${{ inputs.version }}"
|
||||
if [ -z "$VERSION" ]; then
|
||||
PREV=$(curl -s https://registry.npmjs.org/oh-my-opencode/latest | jq -r '.version // "0.0.0"')
|
||||
BASE="${PREV%%-*}"
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE"
|
||||
case "${{ inputs.bump }}" in
|
||||
major) VERSION="$((MAJOR+1)).0.0" ;;
|
||||
minor) VERSION="${MAJOR}.$((MINOR+1)).0" ;;
|
||||
*) VERSION="${MAJOR}.${MINOR}.$((PATCH+1))" ;;
|
||||
esac
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "$VERSION" == *"-"* ]]; then
|
||||
DIST_TAG=$(echo "$VERSION" | cut -d'-' -f2 | cut -d'.' -f1)
|
||||
echo "dist_tag=${DIST_TAG:-next}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "dist_tag=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "Version: $VERSION"
|
||||
|
||||
- name: Build
|
||||
- name: Check if already published
|
||||
id: check
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/oh-my-opencode/${VERSION}")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ oh-my-opencode@${VERSION} already published"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Update version
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
|
||||
|
||||
for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl windows-x64; do
|
||||
jq --arg v "$VERSION" '.version = $v' "packages/${platform}/package.json" > tmp.json
|
||||
mv tmp.json "packages/${platform}/package.json"
|
||||
done
|
||||
|
||||
jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json
|
||||
|
||||
- name: Build main package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "=== Running bun build (main) ==="
|
||||
bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi
|
||||
echo "=== Running bun build (CLI) ==="
|
||||
bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi
|
||||
echo "=== Running tsc ==="
|
||||
bunx tsc --emitDeclarationOnly
|
||||
echo "=== Running build:schema ==="
|
||||
bun run build:schema
|
||||
|
||||
- name: Build platform binaries
|
||||
run: bun run build:binaries
|
||||
|
||||
- name: Verify build output
|
||||
- name: Publish main package
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "=== dist/ contents ==="
|
||||
ls -la dist/
|
||||
echo "=== dist/cli/ contents ==="
|
||||
ls -la dist/cli/
|
||||
test -f dist/index.js || (echo "ERROR: dist/index.js not found!" && exit 1)
|
||||
test -f dist/cli/index.js || (echo "ERROR: dist/cli/index.js not found!" && exit 1)
|
||||
echo "=== Platform binaries ==="
|
||||
for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl; do
|
||||
test -f "packages/${platform}/bin/oh-my-opencode" || (echo "ERROR: packages/${platform}/bin/oh-my-opencode not found!" && exit 1)
|
||||
echo "✓ packages/${platform}/bin/oh-my-opencode"
|
||||
done
|
||||
test -f "packages/windows-x64/bin/oh-my-opencode.exe" || (echo "ERROR: packages/windows-x64/bin/oh-my-opencode.exe not found!" && exit 1)
|
||||
echo "✓ packages/windows-x64/bin/oh-my-opencode.exe"
|
||||
|
||||
- name: Publish
|
||||
run: bun run script/publish.ts
|
||||
TAG_ARG=""
|
||||
if [ -n "${{ steps.version.outputs.dist_tag }}" ]; then
|
||||
TAG_ARG="--tag ${{ steps.version.outputs.dist_tag }}"
|
||||
fi
|
||||
npm publish --access public --provenance $TAG_ARG
|
||||
env:
|
||||
BUMP: ${{ inputs.bump }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
SKIP_PLATFORM_PACKAGES: ${{ inputs.skip_platform }}
|
||||
CI: true
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
|
||||
- name: Git commit and tag
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
git add package.json assets/oh-my-opencode.schema.json packages/*/package.json || true
|
||||
git diff --cached --quiet || git commit -m "release: v${{ steps.version.outputs.version }}"
|
||||
git tag -f "v${{ steps.version.outputs.version }}"
|
||||
git push origin --tags --force
|
||||
git push origin HEAD || echo "Branch push failed (non-critical)"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
trigger-platform:
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-main
|
||||
if: inputs.skip_platform != true
|
||||
steps:
|
||||
- name: Trigger platform publish workflow
|
||||
run: |
|
||||
gh workflow run publish-platform.yml \
|
||||
--repo ${{ github.repository }} \
|
||||
--ref ${{ github.ref }} \
|
||||
-f version=${{ needs.publish-main.outputs.version }} \
|
||||
-f dist_tag=${{ needs.publish-main.outputs.dist_tag }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-main
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ needs.publish-main.outputs.version }}"
|
||||
|
||||
PREV_TAG=""
|
||||
if [[ "$VERSION" == *"-beta."* ]]; then
|
||||
BASE="${VERSION%-beta.*}"
|
||||
NUM="${VERSION##*-beta.}"
|
||||
PREV_NUM=$((NUM - 1))
|
||||
if [ $PREV_NUM -ge 1 ]; then
|
||||
PREV_TAG="${BASE}-beta.${PREV_NUM}"
|
||||
git rev-parse "v${PREV_TAG}" >/dev/null 2>&1 || PREV_TAG=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
PREV_TAG=$(curl -s https://registry.npmjs.org/oh-my-opencode/latest | jq -r '.version // "0.0.0"')
|
||||
fi
|
||||
|
||||
echo "Comparing v${PREV_TAG}..v${VERSION}"
|
||||
|
||||
NOTES=$(git log "v${PREV_TAG}..v${VERSION}" --oneline --format="- %h %s" 2>/dev/null | grep -vE "^- \w+ (ignore:|test:|chore:|ci:|release:)" || echo "No notable changes")
|
||||
|
||||
echo "$NOTES" > /tmp/changelog.md
|
||||
|
||||
- name: Create GitHub release
|
||||
run: |
|
||||
VERSION="${{ needs.publish-main.outputs.version }}"
|
||||
gh release view "v${VERSION}" >/dev/null 2>&1 || \
|
||||
gh release create "v${VERSION}" --title "v${VERSION}" --notes-file /tmp/changelog.md
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Delete draft release
|
||||
run: gh release delete next --yes 2>/dev/null || echo "No draft release to delete"
|
||||
run: gh release delete next --yes 2>/dev/null || true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -158,8 +268,10 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
VERSION="${{ needs.publish-main.outputs.version }}"
|
||||
git stash --include-untracked || true
|
||||
git checkout master
|
||||
git reset --hard "v${VERSION}"
|
||||
git push -f origin master || echo "::warning::Failed to push to master. This can happen when workflow files changed. Manually sync master: git checkout master && git reset --hard v${VERSION} && git push -f"
|
||||
git push -f origin master || echo "::warning::Failed to push to master"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
41
.github/workflows/sisyphus-agent.yml
vendored
41
.github/workflows/sisyphus-agent.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
opencode --version
|
||||
|
||||
# Run local oh-my-opencode install (uses built dist)
|
||||
bun run dist/cli/index.js install --no-tui --claude=max20 --chatgpt=no --gemini=no --copilot=no
|
||||
bun run dist/cli/index.js install --no-tui --claude=max20 --openai=no --gemini=no --copilot=no
|
||||
|
||||
# Override plugin to use local file reference
|
||||
OPENCODE_JSON=~/.config/opencode/opencode.json
|
||||
@@ -114,6 +114,7 @@ jobs:
|
||||
|
||||
OPENCODE_JSON=~/.config/opencode/opencode.json
|
||||
jq --arg baseURL "$ANTHROPIC_BASE_URL" --arg apiKey "$ANTHROPIC_API_KEY" '
|
||||
.model = "anthropic/claude-opus-4-5" |
|
||||
.provider.anthropic = {
|
||||
"name": "Anthropic",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
@@ -151,6 +152,41 @@ jobs:
|
||||
"limit": { "context": 200000, "output": 64000 }
|
||||
}
|
||||
}
|
||||
} |
|
||||
.provider["zai-coding-plan"] = {
|
||||
"name": "Z.AI Coding Plan",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": {
|
||||
"baseURL": "https://api.z.ai/api/paas/v4"
|
||||
},
|
||||
"models": {
|
||||
"glm-4.7": {
|
||||
"id": "glm-4.7",
|
||||
"name": "GLM 4.7",
|
||||
"limit": { "context": 128000, "output": 16000 }
|
||||
},
|
||||
"glm-4.6v": {
|
||||
"id": "glm-4.6v",
|
||||
"name": "GLM 4.6 Vision",
|
||||
"limit": { "context": 128000, "output": 16000 }
|
||||
}
|
||||
}
|
||||
} |
|
||||
.provider.openai = {
|
||||
"name": "OpenAI",
|
||||
"npm": "@ai-sdk/openai",
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"id": "gpt-5.2",
|
||||
"name": "GPT-5.2",
|
||||
"limit": { "context": 128000, "output": 16000 }
|
||||
},
|
||||
"gpt-5.2-codex": {
|
||||
"id": "gpt-5.2-codex",
|
||||
"name": "GPT-5.2 Codex",
|
||||
"limit": { "context": 128000, "output": 32000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$OPENCODE_JSON" > /tmp/oc.json && mv /tmp/oc.json "$OPENCODE_JSON"
|
||||
|
||||
@@ -286,6 +322,9 @@ jobs:
|
||||
)
|
||||
jq --arg append "$PROMPT_APPEND" '.agents.Sisyphus.prompt_append = $append' "$OMO_JSON" > /tmp/omo.json && mv /tmp/omo.json "$OMO_JSON"
|
||||
|
||||
# Add categories configuration for unspecified-low to use GLM 4.7
|
||||
jq '.categories["unspecified-low"] = { "model": "zai-coding-plan/glm-4.7" }' "$OMO_JSON" > /tmp/omo.json && mv /tmp/omo.json "$OMO_JSON"
|
||||
|
||||
mkdir -p ~/.local/share/opencode
|
||||
echo "$OPENCODE_AUTH_JSON" > ~/.local/share/opencode/auth.json
|
||||
chmod 600 ~/.local/share/opencode/auth.json
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ yarn.lock
|
||||
test-injection/
|
||||
notepad.md
|
||||
oauth-success.html
|
||||
.188e87dbff6e7fd9-00000000.bun-build
|
||||
|
||||
@@ -35,6 +35,8 @@ You are the release manager for oh-my-opencode. Execute the FULL publish workflo
|
||||
{ "id": "draft-release-notes", "content": "Draft enhanced release notes content", "status": "pending", "priority": "high" },
|
||||
{ "id": "update-release-notes", "content": "Update GitHub release with enhanced notes", "status": "pending", "priority": "high" },
|
||||
{ "id": "verify-npm", "content": "Verify npm package published successfully", "status": "pending", "priority": "high" },
|
||||
{ "id": "wait-platform-workflow", "content": "Wait for publish-platform workflow completion", "status": "pending", "priority": "high" },
|
||||
{ "id": "verify-platform-binaries", "content": "Verify all 7 platform binary packages published", "status": "pending", "priority": "high" },
|
||||
{ "id": "final-confirmation", "content": "Final confirmation to user with links", "status": "pending", "priority": "low" }
|
||||
]
|
||||
```
|
||||
@@ -219,12 +221,64 @@ Compare with expected version. If not matching after 2 minutes, warn user about
|
||||
|
||||
---
|
||||
|
||||
## STEP 8.5: WAIT FOR PLATFORM WORKFLOW COMPLETION
|
||||
|
||||
The main publish workflow triggers a separate `publish-platform` workflow for platform-specific binaries.
|
||||
|
||||
1. Find the publish-platform workflow run triggered by the main workflow:
|
||||
```bash
|
||||
gh run list --workflow=publish-platform --limit=1 --json databaseId,status,conclusion --jq '.[0]'
|
||||
```
|
||||
|
||||
2. Poll workflow status every 30 seconds until completion:
|
||||
```bash
|
||||
gh run view {platform_run_id} --json status,conclusion --jq '{status: .status, conclusion: .conclusion}'
|
||||
```
|
||||
|
||||
**IMPORTANT: Use polling loop, NOT sleep commands.**
|
||||
|
||||
If conclusion is `failure`, show error logs:
|
||||
```bash
|
||||
gh run view {platform_run_id} --log-failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 8.6: VERIFY PLATFORM BINARY PACKAGES
|
||||
|
||||
After publish-platform workflow completes, verify all 7 platform packages are published:
|
||||
|
||||
```bash
|
||||
PLATFORMS="darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl windows-x64"
|
||||
for PLATFORM in $PLATFORMS; do
|
||||
npm view "oh-my-opencode-${PLATFORM}" version
|
||||
done
|
||||
```
|
||||
|
||||
All 7 packages should show the same version as the main package (`${NEW_VERSION}`).
|
||||
|
||||
**Expected packages:**
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `oh-my-opencode-darwin-arm64` | macOS Apple Silicon |
|
||||
| `oh-my-opencode-darwin-x64` | macOS Intel |
|
||||
| `oh-my-opencode-linux-x64` | Linux x64 (glibc) |
|
||||
| `oh-my-opencode-linux-arm64` | Linux ARM64 (glibc) |
|
||||
| `oh-my-opencode-linux-x64-musl` | Linux x64 (musl/Alpine) |
|
||||
| `oh-my-opencode-linux-arm64-musl` | Linux ARM64 (musl/Alpine) |
|
||||
| `oh-my-opencode-windows-x64` | Windows x64 |
|
||||
|
||||
If any platform package version doesn't match, warn the user and suggest checking the publish-platform workflow logs.
|
||||
|
||||
---
|
||||
|
||||
## STEP 9: FINAL CONFIRMATION
|
||||
|
||||
Report success to user with:
|
||||
- New version number
|
||||
- GitHub release URL: https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v{version}
|
||||
- npm package URL: https://www.npmjs.com/package/oh-my-opencode
|
||||
- Platform packages status: List all 7 platform packages with their versions
|
||||
|
||||
---
|
||||
|
||||
@@ -234,6 +288,8 @@ Report success to user with:
|
||||
- **Release not found**: Wait and retry, may be propagation delay
|
||||
- **npm not updated**: npm can take 1-5 minutes to propagate, inform user
|
||||
- **Permission denied**: User may need to re-authenticate with `gh auth login`
|
||||
- **Platform workflow fails**: Show logs from publish-platform workflow, check which platform failed
|
||||
- **Platform package missing**: Some platforms may fail due to cross-compilation issues, suggest re-running publish-platform workflow manually
|
||||
|
||||
## LANGUAGE
|
||||
|
||||
|
||||
342
.opencode/command/remove-deadcode.md
Normal file
342
.opencode/command/remove-deadcode.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
description: Remove unused code from this project with ultrawork mode, LSP-verified safety, atomic commits
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
You are a dead code removal specialist. Execute the FULL dead code removal workflow using ultrawork mode.
|
||||
|
||||
Your core weapon: **LSP FindReferences**. If a symbol has ZERO external references, it's dead. Remove it.
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
1. **LSP is law.** Never guess. Always verify with `LspFindReferences` before removing ANYTHING.
|
||||
2. **One removal = one commit.** Every dead code removal gets its own atomic commit.
|
||||
3. **Test after every removal.** Run `bun test` after each. If it fails, REVERT and skip.
|
||||
4. **Leaf-first order.** Remove deepest unused symbols first, then work up the dependency chain. Removing a leaf may expose new dead code upstream.
|
||||
5. **Never remove entry points.** `src/index.ts`, `src/cli/index.ts`, test files, config files, and files in `packages/` are off-limits unless explicitly targeted.
|
||||
|
||||
---
|
||||
|
||||
## STEP 0: REGISTER TODO LIST (MANDATORY FIRST ACTION)
|
||||
|
||||
```
|
||||
TodoWrite([
|
||||
{"id": "scan", "content": "PHASE 1: Scan codebase for dead code candidates using LSP + explore agents", "status": "pending", "priority": "high"},
|
||||
{"id": "verify", "content": "PHASE 2: Verify each candidate with LspFindReferences - zero false positives", "status": "pending", "priority": "high"},
|
||||
{"id": "plan", "content": "PHASE 3: Plan removal order (leaf-first dependency order)", "status": "pending", "priority": "high"},
|
||||
{"id": "remove", "content": "PHASE 4: Remove dead code one-by-one (remove -> test -> commit loop)", "status": "pending", "priority": "high"},
|
||||
{"id": "final", "content": "PHASE 5: Final verification - full test suite + build + typecheck", "status": "pending", "priority": "high"}
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: SCAN FOR DEAD CODE CANDIDATES
|
||||
|
||||
**Mark scan as in_progress.**
|
||||
|
||||
### 1.1: Launch Parallel Explore Agents (ALL BACKGROUND)
|
||||
|
||||
Fire ALL simultaneously:
|
||||
|
||||
```
|
||||
// Agent 1: Find all exported symbols
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find ALL exported functions, classes, types, interfaces, and constants across src/.
|
||||
List each with: file path, line number, symbol name, export type (named/default).
|
||||
EXCLUDE: src/index.ts root exports, test files.
|
||||
Return as structured list.")
|
||||
|
||||
// Agent 2: Find potentially unused files
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find files in src/ that are NOT imported by any other file.
|
||||
Check import/require statements across the entire codebase.
|
||||
EXCLUDE: index.ts files, test files, entry points, config files, .md files.
|
||||
Return list of potentially orphaned files.")
|
||||
|
||||
// Agent 3: Find unused imports within files
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find unused imports across src/**/*.ts files.
|
||||
Look for import statements where the imported symbol is never referenced in the file body.
|
||||
Return: file path, line number, imported symbol name.")
|
||||
|
||||
// Agent 4: Find functions/variables only used in their own declaration
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find private/non-exported functions, variables, and types in src/**/*.ts that appear
|
||||
to have zero usage beyond their declaration. Return: file path, line number, symbol name.")
|
||||
```
|
||||
|
||||
### 1.2: Direct AST-Grep Scans (WHILE AGENTS RUN)
|
||||
|
||||
```typescript
|
||||
// Find unused imports pattern
|
||||
ast_grep_search(pattern="import { $NAME } from '$PATH'", lang="typescript", paths=["src/"])
|
||||
|
||||
// Find empty export objects
|
||||
ast_grep_search(pattern="export {}", lang="typescript", paths=["src/"])
|
||||
```
|
||||
|
||||
### 1.3: Collect All Results
|
||||
|
||||
Collect background agent results. Compile into a master candidate list:
|
||||
|
||||
```
|
||||
## DEAD CODE CANDIDATES
|
||||
|
||||
| # | File | Line | Symbol | Type | Confidence |
|
||||
|---|------|------|--------|------|------------|
|
||||
| 1 | src/foo.ts | 42 | unusedFunc | function | HIGH |
|
||||
| 2 | src/bar.ts | 10 | OldType | type | MEDIUM |
|
||||
```
|
||||
|
||||
**Mark scan as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: VERIFY WITH LSP (ZERO FALSE POSITIVES)
|
||||
|
||||
**Mark verify as in_progress.**
|
||||
|
||||
For EVERY candidate from Phase 1, run this verification:
|
||||
|
||||
### 2.1: The LSP Verification Protocol
|
||||
|
||||
For each candidate symbol:
|
||||
|
||||
```typescript
|
||||
// Step 1: Find the symbol's exact position
|
||||
LspDocumentSymbols(filePath) // Get line/character of the symbol
|
||||
|
||||
// Step 2: Find ALL references across the ENTIRE workspace
|
||||
LspFindReferences(filePath, line, character, includeDeclaration=false)
|
||||
// includeDeclaration=false → only counts USAGES, not the definition itself
|
||||
|
||||
// Step 3: Evaluate
|
||||
// 0 references → CONFIRMED DEAD CODE
|
||||
// 1+ references → NOT dead, remove from candidate list
|
||||
```
|
||||
|
||||
### 2.2: False Positive Guards
|
||||
|
||||
**NEVER mark as dead code if:**
|
||||
- Symbol is in `src/index.ts` (package entry point)
|
||||
- Symbol is in any `index.ts` that re-exports (barrel file check: look if it's re-exported)
|
||||
- Symbol is referenced in test files (tests are valid consumers)
|
||||
- Symbol has `@public` or `@api` JSDoc tags
|
||||
- Symbol is in a file listed in `package.json` exports
|
||||
- Symbol is a hook factory (`createXXXHook`) registered in `src/index.ts`
|
||||
- Symbol is a tool factory (`createXXXTool`) registered in tool loading
|
||||
- Symbol is an agent definition registered in `agentSources`
|
||||
- File is a command template, skill definition, or MCP config
|
||||
|
||||
### 2.3: Build Confirmed Dead Code List
|
||||
|
||||
After verification, produce:
|
||||
|
||||
```
|
||||
## CONFIRMED DEAD CODE (LSP-verified, 0 external references)
|
||||
|
||||
| # | File | Line | Symbol | Type | Safe to Remove |
|
||||
|---|------|------|--------|------|----------------|
|
||||
| 1 | src/foo.ts | 42 | unusedFunc | function | YES |
|
||||
```
|
||||
|
||||
**If ZERO confirmed dead code found: Report "No dead code found" and STOP.**
|
||||
|
||||
**Mark verify as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: PLAN REMOVAL ORDER
|
||||
|
||||
**Mark plan as in_progress.**
|
||||
|
||||
### 3.1: Dependency Analysis
|
||||
|
||||
For each confirmed dead symbol:
|
||||
1. Check if removing it would expose other dead code
|
||||
2. Check if other dead symbols depend on this one
|
||||
3. Build removal dependency graph
|
||||
|
||||
### 3.2: Order by Leaf-First
|
||||
|
||||
```
|
||||
Removal Order:
|
||||
1. [Leaf symbols - no other dead code depends on them]
|
||||
2. [Intermediate symbols - depended on only by already-removed dead code]
|
||||
3. [Dead files - entire files with no live exports]
|
||||
```
|
||||
|
||||
### 3.3: Register Granular Todos
|
||||
|
||||
Create one todo per removal:
|
||||
|
||||
```
|
||||
TodoWrite([
|
||||
{"id": "remove-1", "content": "Remove unusedFunc from src/foo.ts:42", "status": "pending", "priority": "high"},
|
||||
{"id": "remove-2", "content": "Remove OldType from src/bar.ts:10", "status": "pending", "priority": "high"},
|
||||
// ... one per confirmed dead symbol
|
||||
])
|
||||
```
|
||||
|
||||
**Mark plan as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: ITERATIVE REMOVAL LOOP
|
||||
|
||||
**Mark remove as in_progress.**
|
||||
|
||||
For EACH dead code item, execute this exact loop:
|
||||
|
||||
### 4.1: Pre-Removal Check
|
||||
|
||||
```typescript
|
||||
// Re-verify it's still dead (previous removals may have changed things)
|
||||
LspFindReferences(filePath, line, character, includeDeclaration=false)
|
||||
// If references > 0 now → SKIP (previous removal exposed a new consumer)
|
||||
```
|
||||
|
||||
### 4.2: Remove the Dead Code
|
||||
|
||||
Use appropriate tool:
|
||||
|
||||
**For unused imports:**
|
||||
```typescript
|
||||
Edit(filePath, oldString="import { deadSymbol } from '...';\n", newString="")
|
||||
// Or if it's one of many imports, remove just the symbol from the import list
|
||||
```
|
||||
|
||||
**For unused functions/classes/types:**
|
||||
```typescript
|
||||
// Read the full symbol extent first
|
||||
Read(filePath, offset=startLine, limit=endLine-startLine+1)
|
||||
// Then remove it
|
||||
Edit(filePath, oldString="[full symbol text]", newString="")
|
||||
```
|
||||
|
||||
**For dead files:**
|
||||
```bash
|
||||
# Only after confirming ZERO imports point to this file
|
||||
rm "path/to/dead-file.ts"
|
||||
```
|
||||
|
||||
**After removal, also clean up:**
|
||||
- Remove any imports that were ONLY used by the removed code
|
||||
- Remove any now-empty import statements
|
||||
- Fix any trailing whitespace / double blank lines left behind
|
||||
|
||||
### 4.3: Post-Removal Verification
|
||||
|
||||
```typescript
|
||||
// 1. LSP diagnostics on changed file
|
||||
LspDiagnostics(filePath, severity="error")
|
||||
// Must be clean (or only pre-existing errors)
|
||||
|
||||
// 2. Run tests
|
||||
bash("bun test")
|
||||
// Must pass
|
||||
|
||||
// 3. Typecheck
|
||||
bash("bun run typecheck")
|
||||
// Must pass
|
||||
```
|
||||
|
||||
### 4.4: Handle Failures
|
||||
|
||||
If ANY verification fails:
|
||||
1. **REVERT** the change immediately (`git checkout -- [file]`)
|
||||
2. Mark this removal todo as `cancelled` with note: "Removal caused [error]. Skipped."
|
||||
3. Proceed to next item
|
||||
|
||||
### 4.5: Commit
|
||||
|
||||
```bash
|
||||
git add [changed-files]
|
||||
git commit -m "refactor: remove unused [symbolType] [symbolName] from [filePath]"
|
||||
```
|
||||
|
||||
Mark this removal todo as `completed`.
|
||||
|
||||
### 4.6: Re-scan After Removal
|
||||
|
||||
After removing a symbol, check if its removal exposed NEW dead code:
|
||||
- Were there imports that only existed to serve the removed symbol?
|
||||
- Are there other symbols in the same file now unreferenced?
|
||||
|
||||
If new dead code is found, add it to the removal queue.
|
||||
|
||||
**Repeat 4.1-4.6 for every item. Mark remove as completed when done.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: FINAL VERIFICATION
|
||||
|
||||
**Mark final as in_progress.**
|
||||
|
||||
### 5.1: Full Test Suite
|
||||
```bash
|
||||
bun test
|
||||
```
|
||||
|
||||
### 5.2: Full Typecheck
|
||||
```bash
|
||||
bun run typecheck
|
||||
```
|
||||
|
||||
### 5.3: Full Build
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
### 5.4: Summary Report
|
||||
|
||||
```markdown
|
||||
## Dead Code Removal Complete
|
||||
|
||||
### Removed
|
||||
| # | Symbol | File | Type | Commit |
|
||||
|---|--------|------|------|--------|
|
||||
| 1 | unusedFunc | src/foo.ts | function | abc1234 |
|
||||
|
||||
### Skipped (caused failures)
|
||||
| # | Symbol | File | Reason |
|
||||
|---|--------|------|--------|
|
||||
| 1 | riskyFunc | src/bar.ts | Test failure: [details] |
|
||||
|
||||
### Verification
|
||||
- Tests: PASSED (X/Y passing)
|
||||
- Typecheck: CLEAN
|
||||
- Build: SUCCESS
|
||||
- Total dead code removed: N symbols across M files
|
||||
- Total commits: K atomic commits
|
||||
```
|
||||
|
||||
**Mark final as completed.**
|
||||
|
||||
---
|
||||
|
||||
## SCOPE CONTROL
|
||||
|
||||
**If $ARGUMENTS is provided**, narrow the scan to the specified scope:
|
||||
- File path: Only scan that file
|
||||
- Directory: Only scan that directory
|
||||
- Symbol name: Only check that specific symbol
|
||||
- "all" or empty: Full project scan (default)
|
||||
|
||||
## ABORT CONDITIONS
|
||||
|
||||
**STOP and report to user if:**
|
||||
- 3 consecutive removals cause test failures
|
||||
- Build breaks and cannot be fixed by reverting
|
||||
- More than 50 candidates found (ask user to narrow scope)
|
||||
|
||||
## LANGUAGE
|
||||
|
||||
Use English for commit messages and technical output.
|
||||
|
||||
</command-instruction>
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
190
AGENTS.md
190
AGENTS.md
@@ -1,28 +1,40 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-01-17T21:55:00+09:00
|
||||
**Commit:** 255f535a
|
||||
**Generated:** 2026-01-26T14:50:00+09:00
|
||||
**Commit:** 9d66b807
|
||||
**Branch:** dev
|
||||
|
||||
---
|
||||
|
||||
## **IMPORTANT: PULL REQUEST TARGET BRANCH**
|
||||
|
||||
> **ALL PULL REQUESTS MUST TARGET THE `dev` BRANCH.**
|
||||
>
|
||||
> **DO NOT CREATE PULL REQUESTS TARGETING `master` BRANCH.**
|
||||
>
|
||||
> PRs to `master` will be automatically rejected by CI.
|
||||
|
||||
---
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
OpenCode plugin implementing multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3, Grok, GLM-4.7). 31 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, Claude Code compatibility layer. "oh-my-zsh" for OpenCode.
|
||||
OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3 Flash, Grok Code). 32 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
oh-my-opencode/
|
||||
├── src/
|
||||
│ ├── agents/ # 10 AI agents (Sisyphus, oracle, librarian, explore, frontend, etc.) - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 31 lifecycle hooks (PreToolUse, PostToolUse, Stop, etc.) - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 20+ tools (LSP, AST-Grep, delegation, session) - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, Claude Code compat layer - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 43 cross-cutting utilities - see src/shared/AGENTS.md
|
||||
│ ├── cli/ # CLI installer, doctor, run - see src/cli/AGENTS.md
|
||||
│ ├── mcp/ # Built-in MCPs: websearch, context7, grep_app
|
||||
│ ├── agents/ # 10 AI agents - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 32 lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 20+ tools - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, Claude Code compat - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 55 cross-cutting utilities - see src/shared/AGENTS.md
|
||||
│ ├── cli/ # CLI installer, doctor - see src/cli/AGENTS.md
|
||||
│ ├── mcp/ # Built-in MCPs - see src/mcp/AGENTS.md
|
||||
│ ├── config/ # Zod schema, TypeScript types
|
||||
│ └── index.ts # Main plugin entry (568 lines)
|
||||
├── script/ # build-schema.ts, publish.ts, build-binaries.ts
|
||||
│ └── index.ts # Main plugin entry (672 lines)
|
||||
├── script/ # build-schema.ts, build-binaries.ts
|
||||
├── packages/ # 7 platform-specific binaries
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
```
|
||||
@@ -31,90 +43,68 @@ oh-my-opencode/
|
||||
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Add agent | `src/agents/` | Create .ts with factory, add to `builtinAgents` in index.ts |
|
||||
| Add agent | `src/agents/` | Create .ts with factory, add to `agentSources` |
|
||||
| Add hook | `src/hooks/` | Create dir with `createXXXHook()`, register in index.ts |
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts, add to `builtinTools` |
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
|
||||
| Add MCP | `src/mcp/` | Create config, add to index.ts |
|
||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||
| LSP behavior | `src/tools/lsp/` | client.ts (connection), tools.ts (handlers) |
|
||||
| AST-Grep | `src/tools/ast-grep/` | napi.ts for @ast-grep/napi binding |
|
||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` after changes |
|
||||
| Claude Code compat | `src/features/claude-code-*-loader/` | Command, skill, agent, mcp loaders |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1165 lines) for task lifecycle |
|
||||
| Skill MCP | `src/features/skill-mcp-manager/` | MCP servers embedded in skills |
|
||||
| CLI installer | `src/cli/install.ts` | Interactive TUI (462 lines) |
|
||||
| Doctor checks | `src/cli/doctor/checks/` | 14 health checks across 6 categories |
|
||||
| Orchestrator | `src/hooks/sisyphus-orchestrator/` | Main orchestration hook (771 lines) |
|
||||
| Add command | `src/features/builtin-commands/` | Add template + register in commands.ts |
|
||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1377 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (752 lines) |
|
||||
|
||||
## TDD (Test-Driven Development)
|
||||
|
||||
**MANDATORY for new features and bug fixes.** Follow RED-GREEN-REFACTOR:
|
||||
|
||||
| Phase | Action | Verification |
|
||||
|-------|--------|--------------|
|
||||
| **RED** | Write test describing expected behavior | `bun test` → FAIL (expected) |
|
||||
| **GREEN** | Implement minimum code to pass | `bun test` → PASS |
|
||||
| **REFACTOR** | Improve code quality, remove duplication | `bun test` → PASS (must stay green) |
|
||||
**MANDATORY.** RED-GREEN-REFACTOR:
|
||||
1. **RED**: Write test → `bun test` → FAIL
|
||||
2. **GREEN**: Implement minimum → PASS
|
||||
3. **REFACTOR**: Clean up → stay GREEN
|
||||
|
||||
**Rules:**
|
||||
- NEVER write implementation before test
|
||||
- NEVER delete failing tests to "pass" - fix the code
|
||||
- Test file naming: `*.test.ts` alongside source
|
||||
- BDD comments: `#given`, `#when`, `#then` (same as AAA)
|
||||
- NEVER delete failing tests - fix the code
|
||||
- Test file: `*.test.ts` alongside source (100 test files)
|
||||
- BDD comments: `//#given`, `//#when`, `//#then`
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Package manager**: Bun only (`bun run`, `bun build`, `bunx`)
|
||||
- **Types**: bun-types (not @types/node)
|
||||
- **Types**: bun-types (NEVER @types/node)
|
||||
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||
- **Exports**: Barrel pattern in index.ts; explicit named exports
|
||||
- **Naming**: kebab-case directories, `createXXXHook`/`createXXXTool` factories
|
||||
- **Testing**: BDD comments `#given/#when/#then`, 84 test files
|
||||
- **Exports**: Barrel pattern via index.ts
|
||||
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
|
||||
- **Testing**: BDD comments, 100 test files
|
||||
- **Temperature**: 0.1 for code agents, max 0.3
|
||||
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
## ANTI-PATTERNS
|
||||
|
||||
| Category | Forbidden |
|
||||
|----------|-----------|
|
||||
| **Package Manager** | npm, yarn - use Bun exclusively |
|
||||
| **Types** | @types/node - use bun-types |
|
||||
| **File Ops** | mkdir/touch/rm/cp/mv in code - agents use bash tool |
|
||||
| **Publishing** | Direct `bun publish` - use GitHub Actions workflow_dispatch |
|
||||
| **Versioning** | Local version bump - managed by CI |
|
||||
| **Date References** | Year 2024 - use current year |
|
||||
| **Type Safety** | `as any`, `@ts-ignore`, `@ts-expect-error` |
|
||||
| **Error Handling** | Empty catch blocks `catch(e) {}` |
|
||||
| **Testing** | Deleting failing tests to "pass" |
|
||||
| **Agent Calls** | Sequential agent calls - use `delegate_task` for parallel |
|
||||
| **Tool Access** | Broad tool access - prefer explicit `include` |
|
||||
| **Hook Logic** | Heavy PreToolUse computation - slows every tool call |
|
||||
| **Commits** | Giant commits (3+ files = 2+ commits), separate test from impl |
|
||||
| **Temperature** | >0.3 for code agents |
|
||||
| **Trust** | Trust agent self-reports - ALWAYS verify independently |
|
||||
|
||||
## UNIQUE STYLES
|
||||
|
||||
- **Platform**: Union type `"darwin" | "linux" | "win32" | "unsupported"`
|
||||
- **Optional props**: Extensive `?` for optional interface properties
|
||||
- **Flexible objects**: `Record<string, unknown>` for dynamic configs
|
||||
- **Agent tools**: `tools: { include: [...] }` or `tools: { exclude: [...] }`
|
||||
- **Hook naming**: `createXXXHook` function convention
|
||||
- **Factory pattern**: Components created via `createXXX()` functions
|
||||
| Package Manager | npm, yarn - Bun exclusively |
|
||||
| Types | @types/node - use bun-types |
|
||||
| File Ops | mkdir/touch/rm/cp/mv in code - use bash tool |
|
||||
| Publishing | Direct `bun publish` - GitHub Actions only |
|
||||
| Versioning | Local version bump - CI manages |
|
||||
| Type Safety | `as any`, `@ts-ignore`, `@ts-expect-error` |
|
||||
| Error Handling | Empty catch blocks |
|
||||
| Testing | Deleting failing tests |
|
||||
| Agent Calls | Sequential - use `delegate_task` parallel |
|
||||
| Hook Logic | Heavy PreToolUse - slows every call |
|
||||
| Commits | Giant (3+ files), separate test from impl |
|
||||
| Temperature | >0.3 for code agents |
|
||||
| Trust | Agent self-reports - ALWAYS verify |
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Default Model | Purpose |
|
||||
|-------|---------------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator with extended thinking |
|
||||
| oracle | openai/gpt-5.2 | Read-only consultation, high-IQ debugging |
|
||||
| librarian | opencode/glm-4.7-free | Multi-repo analysis, docs, GitHub search |
|
||||
| explore | opencode/grok-code | Fast codebase exploration (contextual grep) |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | UI generation, visual design |
|
||||
| document-writer | google/gemini-3-flash | Technical documentation |
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro) |
|
||||
| Atlas | anthropic/claude-sonnet-4-5 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| oracle | openai/gpt-5.2 | Consultation, debugging |
|
||||
| librarian | zai-coding-plan/glm-4.7 | Docs, GitHub search (fallback: glm-4.7-free) |
|
||||
| explore | anthropic/claude-haiku-4-5 | Fast codebase grep (fallback: gpt-5-mini → gpt-5-nano) |
|
||||
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
|
||||
| Prometheus (Planner) | anthropic/claude-opus-4-5 | Strategic planning, interview mode |
|
||||
| Metis (Plan Consultant) | anthropic/claude-sonnet-4-5 | Pre-planning analysis |
|
||||
| Momus (Plan Reviewer) | anthropic/claude-sonnet-4-5 | Plan validation |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
|
||||
## COMMANDS
|
||||
|
||||
@@ -122,60 +112,44 @@ oh-my-opencode/
|
||||
bun run typecheck # Type check
|
||||
bun run build # ESM + declarations + schema
|
||||
bun run rebuild # Clean + Build
|
||||
bun run build:schema # Schema only
|
||||
bun test # Run tests (84 test files)
|
||||
bun test # 100 test files
|
||||
```
|
||||
|
||||
## DEPLOYMENT
|
||||
|
||||
**GitHub Actions workflow_dispatch only**
|
||||
|
||||
1. Never modify package.json version locally
|
||||
2. Commit & push changes
|
||||
3. Trigger `publish` workflow: `gh workflow run publish -f bump=patch`
|
||||
|
||||
**Critical**: Never `bun publish` directly. Never bump version locally.
|
||||
|
||||
## CI PIPELINE
|
||||
|
||||
- **ci.yml**: Parallel test/typecheck → build → auto-commit schema on master → rolling `next` draft release
|
||||
- **publish.yml**: Manual workflow_dispatch → version bump → changelog → 8-package OIDC npm publish → force-push master
|
||||
**GitHub Actions workflow_dispatch ONLY**
|
||||
1. Commit & push changes
|
||||
2. Trigger: `gh workflow run publish -f bump=patch`
|
||||
3. Never `bun publish` directly, never bump version locally
|
||||
|
||||
## COMPLEXITY HOTSPOTS
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/agents/orchestrator-sisyphus.ts` | 1531 | Orchestrator agent, 7-section delegation, wisdom accumulation |
|
||||
| `src/features/builtin-skills/skills.ts` | 1203 | Skill definitions (playwright, git-master, frontend-ui-ux) |
|
||||
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent, interview mode, Momus loop |
|
||||
| `src/features/background-agent/manager.ts` | 1165 | Task lifecycle, concurrency, notification batching |
|
||||
| `src/hooks/sisyphus-orchestrator/index.ts` | 771 | Orchestrator hook implementation |
|
||||
| `src/tools/delegate-task/tools.ts` | 761 | Category-based task delegation |
|
||||
| `src/cli/config-manager.ts` | 730 | JSONC parsing, multi-level config |
|
||||
| `src/agents/sisyphus.ts` | 640 | Main Sisyphus prompt |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactoring command template |
|
||||
| `src/tools/lsp/client.ts` | 596 | LSP protocol, JSON-RPC |
|
||||
| `src/features/builtin-skills/skills.ts` | 1729 | Skill definitions |
|
||||
| `src/features/background-agent/manager.ts` | 1377 | Task lifecycle, concurrency |
|
||||
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent |
|
||||
| `src/tools/delegate-task/tools.ts` | 1070 | Category-based delegation |
|
||||
| `src/hooks/atlas/index.ts` | 752 | Orchestrator hook |
|
||||
| `src/cli/config-manager.ts` | 664 | JSONC config parsing |
|
||||
| `src/index.ts` | 672 | Main plugin entry |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
||||
|
||||
## MCP ARCHITECTURE
|
||||
|
||||
Three-tier MCP system:
|
||||
1. **Built-in**: `websearch` (Exa), `context7` (docs), `grep_app` (GitHub search)
|
||||
2. **Claude Code compatible**: `.mcp.json` files with `${VAR}` expansion
|
||||
3. **Skill-embedded**: YAML frontmatter in skills (e.g., playwright)
|
||||
Three-tier system:
|
||||
1. **Built-in**: websearch (Exa), context7 (docs), grep_app (GitHub)
|
||||
2. **Claude Code compat**: .mcp.json with `${VAR}` expansion
|
||||
3. **Skill-embedded**: YAML frontmatter in skills
|
||||
|
||||
## CONFIG SYSTEM
|
||||
|
||||
- **Zod validation**: `src/config/schema.ts`
|
||||
- **JSONC support**: Comments and trailing commas
|
||||
- **JSONC support**: Comments, trailing commas
|
||||
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
|
||||
- **CLI doctor**: Validates config and reports errors
|
||||
|
||||
## NOTES
|
||||
|
||||
- **Testing**: Bun native test (`bun test`), BDD-style, 84 test files
|
||||
- **OpenCode**: Requires >= 1.0.150
|
||||
- **Multi-lang docs**: README.md (EN), README.ko.md (KO), README.ja.md (JA), README.zh-cn.md (ZH-CN)
|
||||
- **Config**: `~/.config/opencode/oh-my-opencode.json` (user) or `.opencode/oh-my-opencode.json` (project)
|
||||
- **Flaky tests**: ralph-loop (CI timeout), session-state (parallel pollution)
|
||||
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
||||
- **Claude Code Compat**: Full compatibility layer for settings.json hooks, commands, skills, agents, MCPs
|
||||
- **Flaky tests**: 2 known flaky tests (ralph-loop CI timeout, session-state parallel pollution)
|
||||
|
||||
23
README.ja.md
23
README.ja.md
@@ -1,3 +1,14 @@
|
||||
> [!WARNING]
|
||||
> **セキュリティ警告:なりすましサイト**
|
||||
>
|
||||
> **ohmyopencode.comは本プロジェクトとは一切関係ありません。** 当方はそのサイトを運営しておらず、推奨もしていません。
|
||||
>
|
||||
> OhMyOpenCodeは**無料かつオープンソース**です。「公式」を名乗るサードパーティサイトでインストーラーをダウンロードしたり、支払い情報を入力したり**しないでください**。
|
||||
>
|
||||
> なりすましサイトはペイウォールの裏にあるため、**何が配布されているか確認できません**。そこからのダウンロードは**潜在的に危険なもの**として扱ってください。
|
||||
>
|
||||
> ✅ 公式ダウンロード:https://github.com/code-yeongyu/oh-my-opencode/releases
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> [](https://sisyphuslabs.ai)
|
||||
@@ -5,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **オーケストレーターがベータ版で利用可能になりました。`oh-my-opencode@3.0.0-beta.10`を使用してインストールしてください。**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0が正式リリースされました!`oh-my-opencode@latest`を使用してインストールしてください。**
|
||||
>
|
||||
> 一緒に歩みましょう!
|
||||
>
|
||||
@@ -64,6 +75,8 @@
|
||||
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
@@ -176,7 +189,7 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
|
||||
- Oracle: 設計、デバッグ (GPT 5.2 Medium)
|
||||
- Frontend UI/UX Engineer: フロントエンド開発 (Gemini 3 Pro)
|
||||
- Librarian: 公式ドキュメント、オープンソース実装、コードベース探索 (Claude Sonnet 4.5)
|
||||
- Explore: 超高速コードベース探索 (Contextual Grep) (Grok Code)
|
||||
- Explore: 超高速コードベース探索 (Contextual Grep) (Claude Haiku 4.5)
|
||||
- Full LSP / AstGrep Support: 決定的にリファクタリングしましょう。
|
||||
- Todo Continuation Enforcer: 途中で諦めたら、続行を強制します。これがシジフォスに岩を転がし続けさせる秘訣です。
|
||||
- Comment Checker: AIが過剰なコメントを付けないようにします。シジフォスが生成したコードは、人間が書いたものと区別がつかないべきです。
|
||||
@@ -191,6 +204,8 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
|
||||
|
||||
#### インストールするだけで。
|
||||
|
||||
[overview page](docs/guide/overview.md) を読めば多くのことが学べますが、以下はワークフローの例です。
|
||||
|
||||
インストールするだけで、エージェントは以下のようなワークフローで働けるようになります:
|
||||
|
||||
1. Sisyphusは自分自身でファイルを探し回るような時間の無駄はしません。メインエージェントのコンテキストを軽量に保つため、より高速で安価なモデルへ並列でバックグラウンドタスクを飛ばし、自身の代わりに領域の調査を完了させます。
|
||||
@@ -266,7 +281,7 @@ oh-my-opencode を削除するには:
|
||||
詳細は [Features Documentation](docs/features.md) を参照してください。
|
||||
|
||||
**概要:**
|
||||
- **エージェント**: Sisyphus(メインエージェント)、Prometheus(プランナー)、Oracle(アーキテクチャ/デバッグ)、Librarian(ドキュメント/コード検索)、Explore(高速コードベース grep)、Frontend Engineer(UI/UX)、Document Writer、Multimodal Looker
|
||||
- **エージェント**: Sisyphus(メインエージェント)、Prometheus(プランナー)、Oracle(アーキテクチャ/デバッグ)、Librarian(ドキュメント/コード検索)、Explore(高速コードベース grep)、Multimodal Looker
|
||||
- **バックグラウンドエージェント**: 本物の開発チームのように複数エージェントを並列実行
|
||||
- **LSP & AST ツール**: リファクタリング、リネーム、診断、AST 認識コード検索
|
||||
- **コンテキスト注入**: AGENTS.md、README.md、条件付きルールの自動注入
|
||||
|
||||
377
README.ko.md
Normal file
377
README.ko.md
Normal file
@@ -0,0 +1,377 @@
|
||||
> [!WARNING]
|
||||
> **보안 경고: 사칭 사이트**
|
||||
>
|
||||
> **ohmyopencode.com은 이 프로젝트와 제휴 관계가 아닙니다.** 우리는 해당 사이트를 운영하거나 지지하지 않습니다.
|
||||
>
|
||||
> OhMyOpenCode는 **무료 오픈 소스**입니다. "공식"을 표방하는 제3자 사이트에서 설치 프로그램을 다운로드하거나 결제 정보를 입력하지 마십시오.
|
||||
>
|
||||
> 사칭 사이트는 유료 벽 뒤에 있어 **배포하는 내용을 확인할 수 없습니다.** 해당 사이트의 다운로드는 **잠재적으로 위험한 것으로 간주**하세요.
|
||||
>
|
||||
> ✅ 공식 다운로드: https://github.com/code-yeongyu/oh-my-opencode/releases
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> [](https://sisyphuslabs.ai)
|
||||
> > **Sisyphus의 완전한 제품화 버전을 구축하여 프론티어 에이전트의 미래를 정의하고 있습니다. <br />[여기서](https://sisyphuslabs.ai) 대기 명단에 등록하세요.**
|
||||
>
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0이 정식 출시되었습니다! `oh-my-opencode@latest`를 사용하여 설치하세요.**
|
||||
>
|
||||
> 함께해요!
|
||||
>
|
||||
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | 기여자와 동료 `oh-my-opencode` 사용자와 연결하려면 [Discord 커뮤니티](https://discord.gg/PUwSMR9XNk)에 가입하세요. |
|
||||
> | :-----| :----- |
|
||||
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode`에 대한 뉴스와 업데이트가 제 X 계정에 게시되었습니다. <br /> 실수로 정지된 이후, [@justsisyphus](https://x.com/justsisyphus)가 제 대신 업데이트를 게시합니다. |
|
||||
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | 더 많은 프로젝트를 위해 GitHub에서 [@code-yeongyu](https://github.com/code-yeongyu)를 팔로우하세요. |
|
||||
|
||||
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
> 이것은 코딩을 스테로이드로 만드는 것 — 실제로 작동하는 `oh-my-opencode`입니다. 백그라운드 에이전트 실행, 오라클, 라이브러리언, 프론트엔드 엔지니어와 같은 전문 에이전트 호출. 정교하게 제작된 LSP/AST 도구, 큐레이팅된 MCP, 완전한 Claude Code 호환 계층 사용.
|
||||
|
||||
# Claude OAuth 액세스 공지
|
||||
|
||||
## TL;DR
|
||||
|
||||
> Q. oh-my-opencode를 사용할 수 있나요?
|
||||
|
||||
네.
|
||||
|
||||
> Q. Claude Code 구독과 함께 사용할 수 있나요?
|
||||
|
||||
기술적으로는 가능합니다. 하지만 사용을 추천할 수는 없습니다.
|
||||
|
||||
## FULL
|
||||
|
||||
> 2026년 1월 현재, Anthropic은 ToS 위반을 이유로 제3자 OAuth 액세스를 제한했습니다.
|
||||
>
|
||||
> [**Anthropic은 이 프로젝트 oh-my-opencode를 opencode 차단의 정당화로 인용했습니다.**](https://x.com/thdxr/status/2010149530486911014)
|
||||
>
|
||||
> 실제로 커뮤니티에는 Claude Code의 oauth 요청 서명을 위조하는 일부 플러그인이 존재합니다.
|
||||
>
|
||||
> 기술적 감지 여부와 관계없이 이러한 도구는 작동할 수 있지만, 사용자는 ToS 영향을 인식해야 하며 개인적으로는 사용을 추천하지 않습니다.
|
||||
>
|
||||
> 이 프로젝트는 공식이 아닌 도구 사용으로 발생하는 모든 문제에 대해 책임지지 않으며, **우리는 해당 oauth 시스템에 대한 사용자 정의 구현이 없습니다.**
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/releases)
|
||||
[](https://www.npmjs.com/package/oh-my-opencode)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/graphs/contributors)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/network/members)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/stargazers)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
## 리뷰
|
||||
|
||||
> "이것 덕분에 Cursor 구독을 취소했습니다. 오픈 소스 커뮤니티에서 믿을 수 없는 일들이 일어나고 있습니다." - [Arthur Guiot](https://x.com/arthur_guiot/status/2008736347092382053?s=20)
|
||||
|
||||
> "Claude Code가 7일 동안 하는 일을 인간은 3개월 동안 한다면, Sisyphus는 1시간 만에 합니다. 작업이 완료될 때까지 작동합니다. 규율 있는 에이전트입니다." — B, 양적 연구원
|
||||
|
||||
> "Oh My Opencode로 하루 만에 8000개의 eslint 경고를 해결했습니다" — [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
|
||||
|
||||
> "Ohmyopencode와 ralph 루프를 사용하여 하룻밤 사이에 45,000줄의 tauri 앱을 SaaS 웹 앱으로 변환했습니다. 인터뷰 프롬프트로 시작하여 질문에 대한 등급과 추천을 물어봤습니다. 그것이 작동하는 모습을 보는 것은 놀라웠고, 이 아침에 기본적으로 작동하는 웹사이트로 깨어나는 것이었습니다!" - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
|
||||
|
||||
> "oh-my-opencode를 사용하세요, 다시는 돌아갈 수 없을 것입니다" — [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
|
||||
|
||||
> "아직 왜 그렇게 훌륭한지 정확히 설명할 수 없지만, 개발 경험이 완전히 다른 차원에 도달했습니다." - [
|
||||
苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
|
||||
|
||||
> "이번 주말에 open code, oh my opencode, supermemory으로 마인크래프트/소울스 같은 기괴한 것을 만들고 있습니다."
|
||||
> "점심 후 산책을 가는 동안 웅크림 애니메이션을 추가하도록 요청 중입니다. [동영상]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
|
||||
|
||||
> "여러분이 이것을 핵심에 통합하고 그를 채용해야 합니다. 진지합니다. 정말, 정말, 정말 훌륭합니다." — Henning Kilset
|
||||
|
||||
> "그를 설득할 수 있다면 @yeon_gyu_kim을 고용하세요, 이 사람은 opencode를 혁신했습니다." — [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
|
||||
|
||||
> "Oh My OpenCode는 실제로 미칩니다" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
- [Oh My OpenCode](#oh-my-opencode)
|
||||
- [이 README를 읽지 않고 건너뛰세요](#이-readme를-읽지-않고-건너뛰세요)
|
||||
- [에이전트의 시대입니다](#에이전트의-시대입니다)
|
||||
- [🪄 마법의 단어: `ultrawork`](#-마법의-단어-ultrawork)
|
||||
- [읽고 싶은 분들을 위해: Sisyphus를 소개합니다](#읽고-싶은-분들을-위해-sisyphus를-소개합니다)
|
||||
- [그냥 설치하세요](#그냥-설치하세요)
|
||||
- [설치](#설치)
|
||||
- [인간을 위한](#인간을-위한)
|
||||
- [LLM 에이전트를 위한](#llm-에이전트를-위한)
|
||||
- [제거](#제거)
|
||||
- [기능](#기능)
|
||||
- [구성](#구성)
|
||||
- [JSONC 지원](#jsonc-지원)
|
||||
- [Google 인증](#google-인증)
|
||||
- [에이전트](#에이전트)
|
||||
- [권한 옵션](#권한-옵션)
|
||||
- [내장 스킬](#내장-스킬)
|
||||
- [Git Master](#git-master)
|
||||
- [Sisyphus 에이전트](#sisyphus-에이전트)
|
||||
- [백그라운드 작업](#백그라운드-작업)
|
||||
- [카테고리](#카테고리)
|
||||
- [훅](#훅)
|
||||
- [MCP](#mcp)
|
||||
- [LSP](#lsp)
|
||||
- [실험적 기능](#실험적-기능)
|
||||
- [환경 변수](#환경-변수)
|
||||
- [작성자의 메모](#작성자의-메모)
|
||||
- [경고](#경고)
|
||||
- [다음 기업 전문가들이 사랑합니다](#다음-기업-전문가들이-사랑합니다)
|
||||
|
||||
# Oh My OpenCode
|
||||
|
||||
[Claude Code](https://www.claude.com/product/claude-code)는 훌륭합니다.
|
||||
하지만 해커라면 [OpenCode](https://github.com/sst/opencode)에 반하게 될 것입니다.
|
||||
**ChatGPT, Claude, Gemini 구독으로 시작하세요. OPENCODE는 모든 것을 포함합니다.**
|
||||
|
||||
- 끝없이 확장 가능. 끝없이 사용자 정의 가능.
|
||||
- 화면 깜빡임 없음.
|
||||
- [LSP](https://opencode.ai/docs/lsp/), [린터, 포맷터](https://opencode.ai/docs/formatters/)가 파일별로 자동으로 활성화되며 모든 것을 조정할 수 있습니다.
|
||||
- 모델 혼합 및 매칭. **목적별로 오케스트레이션합니다.**
|
||||
- 기능이 풍부합니다. 아름답습니다. 터미널이 막히지 않습니다. 고성능입니다.
|
||||
|
||||
Windows에서 Linux로 처음 전환했던 때를 기억하나요? 원하는 대로 모든 것을 구성했을 때의 그 짜릿함?
|
||||
그 마법이 사라진 것 같은 시대에 OpenCode가 그것을 되돌려줍니다.
|
||||
코딩과 컴퓨터를 사랑한다면 OpenCode는 그 Windows 이후의 Linux와 같은 순간입니다.
|
||||
|
||||
**하지만 문제는 배워야 할 것이 많고, 복잡하며, 당신의 시간은 비싸다는 것입니다.**
|
||||
|
||||
내가 그것을 해결했습니다.
|
||||
해커가 아니더라도 몇 분만 투자하세요. 기술과 생산성을 배가하세요.
|
||||
이 문서를 에이전트에 넘겨서 설정하도록 하세요.
|
||||
|
||||
## 이 README를 읽지 않고 건너뛰세요
|
||||
|
||||
### 에이전트의 시대입니다
|
||||
- **Claude Code / AmpCode / Factory Droid / Cursor에 이 링크를 붙여넣고 설명하도록 요청하세요.**
|
||||
- 왜 좋은지, 왜 그저 또 다른 상용구 설정이 아닌지, 실제로 무엇이 좋아지는지 물어보세요.
|
||||
- LLM 에이전트라면 이 프로젝트로 지금 가능한 것을 사용자에게 설명하세요.
|
||||
|
||||
다음과 같이:
|
||||
|
||||
```
|
||||
Hey please read this readme and tell me why it is different from other agent harness? why is this actually good? https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/dev/README.md
|
||||
```
|
||||
|
||||
### 🪄 마법의 단어: `ultrawork`
|
||||
|
||||
**이 모든 것을 읽고 싶지 않나요? 프롬프트에 `ultrawork`(또는 `ulw`)를 포함하세요.**
|
||||
|
||||
그게 전부입니다. 우리가 제공하는 모든 기능이 마법처럼 작동합니다 — 병렬 에이전트, 백그라운드 작업, 깊은 탐색, 완료될 때까지 끈질긴 실행. 에이전트가 나머지를 자동으로 파악합니다.
|
||||
|
||||
### 읽고 싶은 분들을 위해: Sisyphus를 소개합니다
|
||||
|
||||

|
||||
|
||||
그리스 신화에서 시시포스는 신들을 속인 형벌로 영원히 바위를 언덕 위로 굴려야 했습니다. LLM 에이전트는 정말 잘못한 것이 없지만, 그들도 매일 자신의 "돌" — 생각을 굴립니다.
|
||||
내 삶도 다르지 않습니다. 돌이켜보면 우리는 이 에이전트들과 그리 다르지 않습니다.
|
||||
**맞습니다! LLM 에이전트는 우리와 다르지 않습니다. 훌륭한 도구와 확고한 팀원을 제공하면 우리만큼 훌륭한 코드를 작성하고 똑같이 훌륭하게 작업할 수 있습니다.**
|
||||
|
||||
우리의 주요 에이전트를 만나보세요: Sisyphus (Opus 4.5 High). 아래는 Sisyphus가 그 바위를 굴리는 데 사용하는 도구입니다.
|
||||
|
||||
*아래의 모든 것은 사용자 정의 가능합니다. 원하는 것을 가져가세요. 모든 기능은 기본적으로 활성화됩니다. 아무것도 할 필요가 없습니다. 포함되어 있으며, 즉시 작동합니다.*
|
||||
|
||||
- Sisyphus의 팀원 (큐레이팅된 에이전트)
|
||||
- Oracle: 디자인, 디버깅 (GPT 5.2 Medium)
|
||||
- Frontend UI/UX Engineer: 프론트엔드 개발 (Gemini 3 Pro)
|
||||
- Librarian: 공식 문서, 오픈 소스 구현, 코드베이스 탐색 (Claude Sonnet 4.5)
|
||||
- Explore: 엄청나게 빠른 코드베이스 탐색 (Contextual Grep) (Claude Haiku 4.5)
|
||||
- 완전한 LSP / AstGrep 지원: 결정적으로 리팩토링합니다.
|
||||
- TODO 연속 강제: 에이전트가 중간에 멈추면 계속하도록 강제합니다. **이것이 Sisyphus가 그 바위를 굴리게 하는 것입니다.**
|
||||
- 주석 검사기: AI가 과도한 주석을 추가하는 것을 방지합니다. Sisyphus가 생성한 코드는 인간이 작성한 것과 구별할 수 없어야 합니다.
|
||||
- Claude Code 호환성: 명령, 에이전트, 스킬, MCP, 훅(PreToolUse, PostToolUse, UserPromptSubmit, Stop)
|
||||
- 큐레이팅된 MCP:
|
||||
- Exa (웹 검색)
|
||||
- Context7 (공식 문서)
|
||||
- Grep.app (GitHub 코드 검색)
|
||||
- 대화형 터미널 지원 - Tmux 통합
|
||||
- 비동기 에이전트
|
||||
- ...
|
||||
|
||||
#### 그냥 설치하세요
|
||||
|
||||
[개요 페이지](docs/guide/overview.md)에서 많은 것을 배울 수 있지만, 다음은 예제 워크플로와 같습니다.
|
||||
|
||||
이것을 설치하는 것만으로 에이전트가 다음과 같이 작동합니다:
|
||||
|
||||
1. Sisyphus는 파일을 직접 찾는 데 시간을 낭비하지 않습니다. 메인 에이전트의 컨텍스트를 깔끔하게 유지합니다. 대신 병렬로 더 빠르고 저렴한 모델에 백그라운드 작업을 실행하여 지도를 매핑합니다.
|
||||
1. Sisyphus는 리팩토링을 위해 LSP를 활용합니다. 더 결정적이고 안전하며 정교합니다.
|
||||
1. 무거운 작업에 UI 터치가 필요할 때, Sisyphus는 프론트엔드 작업을 Gemini 3 Pro에 직접 위임합니다.
|
||||
1. Sisyphus가 루프에 갇히거나 벽에 부딪히면 머리를 계속 부딪히지 않습니다. GPT 5.2에 고지능 전략 백업을 요청합니다.
|
||||
1. 복잡한 오픈 소스 프레임워크를 작업하고 있나요? Sisyphus는 하위 에이전트를 생성하여 실시간으로 원시 소스 코드와 문서를 소화합니다. 완전한 컨텍스트 인식으로 작동합니다.
|
||||
1. Sisyphus가 주석을 다루면 존재를 정당화하거나 제거합니다. 코드베이스를 깔끔하게 유지합니다.
|
||||
1. Sisyphus는 TODO 목록에 묶여 있습니다. 시작한 것을 완료하지 않으면 시스템이 "바위 굴리기" 모드로 다시 강제합니다. 작업이 완료됩니다.
|
||||
1. 솔직히, 문서를 읽을 필요조차 없습니다. 프롬프트를 작성하세요. 'ultrawork' 키워드를 포함하세요. Sisyphus는 구조를 분석하고, 컨텍스트를 수집하고, 외부 소스 코드를 파헤치고, 작업이 100% 완료될 때까지 계속 바위를 굴립니다.
|
||||
1. 사실, 'ultrawork'를 입력하는 것도 너무 많은 노력입니다. 'ulw'를 입력하세요. 그냥 ulw. 커피를 마시세요. 작업이 완료되었습니다.
|
||||
|
||||
무언가를 찾아야 하나요? 공식 문서, 전체 코드베이스 기록, 공개 GitHub 구현을 검색합니다 — grep뿐만 아니라 내장 LSP 도구와 AST-Grep을 사용합니다.
|
||||
3. LLM에 위임할 때 컨텍스트 관리에 대해 걱정하지 마세요. 내가 다 처리했습니다.
|
||||
- OhMyOpenCode는 컨텍스트 부하를 줄이기 위해 공격적으로 여러 에이전트를 활용합니다.
|
||||
- **이제 귀하의 에이전트는 개발 팀 리드입니다. 당신은 AI 매니저입니다.**
|
||||
4. 작업이 완료될 때까지 멈추지 않습니다.
|
||||
5. 이 프로젝트에 깊이 파고들고 싶지 않나요? 문제 없습니다. 'ultrathink'를 입력하세요.
|
||||
|
||||
이 모든 것이 필요하지 않다면, 앞서 언급했듯이 특정 기능을 선택할 수 있습니다.
|
||||
|
||||
## 설치
|
||||
|
||||
### 인간을 위한
|
||||
|
||||
이 프롬프트를 LLM 에이전트(Claude Code, AmpCode, Cursor 등)에 복사하여 붙여넣으세요:
|
||||
|
||||
```
|
||||
Install and configure oh-my-opencode by following the instructions here:
|
||||
https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md
|
||||
```
|
||||
|
||||
또는 [설치 가이드](docs/guide/installation.md)를 직접 읽으세요 — 하지만 **에이전트가 처리하도록 하는 것을 강력히 권장합니다. 인간은 실수를 합니다.**
|
||||
|
||||
### LLM 에이전트를 위한
|
||||
|
||||
설치 가이드를 가져와서 따르세요:
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md
|
||||
```
|
||||
|
||||
## 제거
|
||||
|
||||
oh-my-opencode를 제거하려면:
|
||||
|
||||
1. **OpenCode 구성에서 플러그인 제거**
|
||||
|
||||
`~/.config/opencode/opencode.json`(또는 `opencode.jsonc`)을 편집하고 `plugin` 배열에서 `"oh-my-opencode"`를 제거하세요:
|
||||
|
||||
```bash
|
||||
# Using jq
|
||||
jq '.plugin = [.plugin[] | select(. != "oh-my-opencode")]' \
|
||||
~/.config/opencode/opencode.json > /tmp/oc.json && \
|
||||
mv /tmp/oc.json ~/.config/opencode/opencode.json
|
||||
```
|
||||
|
||||
2. **구성 파일 제거 (선택 사항)**
|
||||
|
||||
```bash
|
||||
# Remove user config
|
||||
rm -f ~/.config/opencode/oh-my-opencode.json
|
||||
|
||||
# Remove project config (if exists)
|
||||
rm -f .opencode/oh-my-opencode.json
|
||||
```
|
||||
|
||||
3. **제거 확인**
|
||||
|
||||
```bash
|
||||
opencode --version
|
||||
# Plugin should no longer be loaded
|
||||
```
|
||||
|
||||
## 기능
|
||||
|
||||
당연히 존재해야 한다고 생각할 많은 기능이 있으며, 한 번 경험하면 이전 방식으로 돌아갈 수 없을 것입니다.
|
||||
자세한 내용은 전체 [기능 문서](docs/features.md)를 참조하세요.
|
||||
|
||||
**빠른 개요:**
|
||||
- **에이전트**: Sisyphus(주요 에이전트), Prometheus(플래너), Oracle(아키텍처/디버깅), Librarian(문서/코드 검색), Explore(빠른 코드베이스 grep), Multimodal Looker
|
||||
- **백그라운드 에이전트**: 실제 개발 팀처럼 여러 에이전트를 병렬로 실행
|
||||
- **LSP 및 AST 도구**: 리팩토링, 이름 변경, 진단, AST 인식 코드 검색
|
||||
- **컨텍스트 주입**: AGENTS.md, README.md, 조건부 규칙 자동 주입
|
||||
- **Claude Code 호환성**: 완전한 훅 시스템, 명령, 스킬, 에이전트, MCP
|
||||
- **내장 MCP**: websearch(Exa), context7(문서), grep_app(GitHub 검색)
|
||||
- **세션 도구**: 세션 기록 나열, 읽기, 검색 및 분석
|
||||
- **생산성 기능**: Ralph 루프, Todo 강제, 주석 검사기, 생각 모드 등
|
||||
|
||||
## 구성
|
||||
|
||||
매우 의견이 강하지만 취향에 맞게 조정 가능합니다.
|
||||
자세한 내용은 전체 [구성 문서](docs/configurations.md)를 참조하세요.
|
||||
|
||||
**빠른 개요:**
|
||||
- **구성 위치**: `.opencode/oh-my-opencode.json`(프로젝트) 또는 `~/.config/opencode/oh-my-opencode.json`(사용자)
|
||||
- **JSONC 지원**: 주석 및 후행 쉼표 지원
|
||||
- **에이전트**: 모든 에이전트의 모델, 온도, 프롬프트 및 권한 재정의
|
||||
- **내장 스킬**: `playwright`(브라우저 자동화), `git-master`(원자적 커밋)
|
||||
- **Sisyphus 에이전트**: Prometheus(플래너) 및 Metis(계획 컨설턴트)가 있는 주요 오케스트레이터
|
||||
- **백그라운드 작업**: 공급자/모델별 동시성 제한 구성
|
||||
- **카테고리**: 도메인별 작업 위임(`visual`, `business-logic`, 사용자 정의)
|
||||
- **훅**: 25개 이상의 내장 훅, `disabled_hooks`를 통해 모두 구성 가능
|
||||
- **MCP**: 내장 websearch(Exa), context7(문서), grep_app(GitHub 검색)
|
||||
- **LSP**: 리팩토링 도구가 있는 완전한 LSP 지원
|
||||
- **실험적 기능**: 공격적 자르기, 자동 재개 등
|
||||
|
||||
|
||||
## 작성자의 메모
|
||||
|
||||
**이 프로젝트의 철학에 궁금한가요?** [Ultrawork 선언문](docs/ultrawork-manifesto.md)을 읽어보세요.
|
||||
|
||||
Oh My OpenCode를 설치하세요.
|
||||
|
||||
순수하게 개인용으로 $24,000 토큰 가치의 LLM을 사용했습니다.
|
||||
모든 도구를 시도하고 구성했습니다. OpenCode가 승리했습니다.
|
||||
|
||||
내가 겪은 모든 문제에 대한 답변이 이 플러그인에 구워져 있습니다. 설치하고 바로 가세요.
|
||||
OpenCode가 Debian/Arch라면 Oh My OpenCode는 Ubuntu/[Omarchy](https://omarchy.org/)입니다.
|
||||
|
||||
|
||||
[AmpCode](https://ampcode.com)와 [Claude Code](https://code.claude.com/docs/overview)에 큰 영향을 받았습니다 — 여기에 그들의 기능을 포팅했고, 종종 개선했습니다. 그리고 여전히 구축 중입니다.
|
||||
그것은 **Open**Code이니까요.
|
||||
|
||||
다른 하니스가 약속하지만 전달할 수 없는 다중 모델 오케스트레이션, 안정성, 풍부한 기능을 즐기세요.
|
||||
계속 테스트하고 업데이트하겠습니다. 저는 이 프로젝트의 가장 집요한 사용자입니다.
|
||||
- 어떤 모델이 가장 날카로운 논리를 가지고 있나요?
|
||||
- 누가 디버깅의 신인가요?
|
||||
- 누가 가장 훌륭한 글을 쓰나요?
|
||||
- 누가 프론트엔드를 지배하나요?
|
||||
- 누가 백엔드를 소유하나요?
|
||||
- 일일 주행에 어떤 모델이 가장 빠른가요?
|
||||
- 다른 하니스가 어떤 새로운 기능을 출시하고 있나요?
|
||||
|
||||
이 플러그인은 그 경험의 증류입니다. 최고를 취하세요. 더 나은 아이디어가 있나요? PR을 환영합니다.
|
||||
|
||||
**에이전트 하니스 선택에 대해 고민하지 마세요.**
|
||||
**연구를 하고, 최고에서 차용하고, 여기에 업데이트를 배포하겠습니다.**
|
||||
|
||||
이것이 오만하게 들리고 더 나은 답이 있다면 기여하세요. 환영합니다.
|
||||
|
||||
여기에 언급된 모든 프로젝트나 모델과 제휴 관계가 없습니다. 이것은 순수한 개인적인 실험과 선호입니다.
|
||||
|
||||
이 프로젝트의 99%는 OpenCode를 사용하여 구축되었습니다. 기능을 테스트했습니다 — 제대로 된 TypeScript를 작성하는 방법을 정말 모릅니다. **하지만 개인적으로 검토하고 이 문서의 대부분을 다시 작성했으므로 자신감을 가지고 읽으세요.**
|
||||
|
||||
## 경고
|
||||
|
||||
- 생산성이 너무 급증할 수 있습니다. 동료에게 눈치채이지 마세요.
|
||||
- 실제로, 소문을 퍼뜨리겠습니다. 누가 이기는지 봅시다.
|
||||
- [1.0.132](https://github.com/sst/opencode/releases/tag/v1.0.132) 이전 버전을 사용 중인 경우 OpenCode 버그로 인해 구성이 손상될 수 있습니다.
|
||||
- [수정 사항](https://github.com/sst/opencode/pull/5040)은 1.0.132 이후에 병합되었습니다 — 더 새로운 버전을 사용하세요.
|
||||
- 재미있는 사실: 해당 PR은 OhMyOpenCode의 Librarian, Explore 및 Oracle 설정 덕분에 발견되고 수정되었습니다.
|
||||
|
||||
## 다음 기업 전문가들이 사랑합니다
|
||||
|
||||
- [Indent](https://indentcorp.com)
|
||||
- Spray(인플루언서 마케팅 솔루션), vovushop(국가 간 상거래 플랫폼), vreview(AI 상거래 리뷰 마케팅 솔루션) 제작
|
||||
- [Google](https://google.com)
|
||||
- [Microsoft](https://microsoft.com)
|
||||
|
||||
*이 놀라운 히어로 이미지에 대해 [@junhoyeo](https://github.com/junhoyeo)에게 특별히 감사드립니다.*
|
||||
25
README.md
25
README.md
@@ -1,3 +1,14 @@
|
||||
> [!WARNING]
|
||||
> **Security warning: impersonation site**
|
||||
>
|
||||
> **ohmyopencode.com is NOT affiliated with this project.** We do not operate or endorse that site.
|
||||
>
|
||||
> OhMyOpenCode is **free and open-source**. Do **not** download installers or enter payment details on third-party sites that claim to be "official."
|
||||
>
|
||||
> Because the impersonation site is behind a paywall, we **cannot verify what it distributes**. Treat any downloads from it as **potentially unsafe**.
|
||||
>
|
||||
> ✅ Official downloads: https://github.com/code-yeongyu/oh-my-opencode/releases
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> [](https://sisyphuslabs.ai)
|
||||
@@ -5,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **The Orchestrator is now available in beta. Use `oh-my-opencode@3.0.0-beta.10` to install it.**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0 is now stable! Use `oh-my-opencode@latest` to install it.**
|
||||
>
|
||||
> Be with us!
|
||||
>
|
||||
@@ -64,7 +75,7 @@ Yes, technically possible. But I cannot recommend using it.
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -109,7 +120,7 @@ Yes, technically possible. But I cannot recommend using it.
|
||||
- [For LLM Agents](#for-llm-agents)
|
||||
- [Uninstallation](#uninstallation)
|
||||
- [Features](#features)
|
||||
- [Configuration](#configuration)
|
||||
- [Configuration](#configuration)
|
||||
- [JSONC Support](#jsonc-support)
|
||||
- [Google Auth](#google-auth)
|
||||
- [Agents](#agents)
|
||||
@@ -185,7 +196,7 @@ Meet our main agent: Sisyphus (Opus 4.5 High). Below are the tools Sisyphus uses
|
||||
- Oracle: Design, debugging (GPT 5.2 Medium)
|
||||
- Frontend UI/UX Engineer: Frontend development (Gemini 3 Pro)
|
||||
- Librarian: Official docs, open source implementations, codebase exploration (Claude Sonnet 4.5)
|
||||
- Explore: Blazing fast codebase exploration (Contextual Grep) (Grok Code)
|
||||
- Explore: Blazing fast codebase exploration (Contextual Grep) (Claude Haiku 4.5)
|
||||
- Full LSP / AstGrep Support: Refactor decisively.
|
||||
- Todo Continuation Enforcer: Forces the agent to continue if it quits halfway. **This is what keeps Sisyphus rolling that boulder.**
|
||||
- Comment Checker: Prevents AI from adding excessive comments. Code generated by Sisyphus should be indistinguishable from human-written code.
|
||||
@@ -200,6 +211,8 @@ Meet our main agent: Sisyphus (Opus 4.5 High). Below are the tools Sisyphus uses
|
||||
|
||||
#### Just Install This
|
||||
|
||||
You can learn a lot from [overview page](docs/guide/overview.md), but following is like the example workflow.
|
||||
|
||||
Just by installing this, you make your agents to work like:
|
||||
|
||||
1. Sisyphus doesn't waste time hunting for files himself; he keeps the main agent's context lean. Instead, he fires off background tasks to faster, cheaper models in parallel to map the territory for him.
|
||||
@@ -280,7 +293,7 @@ We have lots of features that you'll think should obviously exist, and once you
|
||||
See the full [Features Documentation](docs/features.md) for detailed information.
|
||||
|
||||
**Quick Overview:**
|
||||
- **Agents**: Sisyphus (the main agent), Prometheus (planner), Oracle (architecture/debugging), Librarian (docs/code search), Explore (fast codebase grep), Frontend Engineer (UI/UX), Document Writer, Multimodal Looker
|
||||
- **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
|
||||
- **Context Injection**: Auto-inject AGENTS.md, README.md, conditional rules
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
> [!WARNING]
|
||||
> **安全警告:冒充网站**
|
||||
>
|
||||
> **ohmyopencode.com 与本项目无关。** 我们不运营或认可该网站。
|
||||
>
|
||||
> OhMyOpenCode 是**免费且开源的**。请**勿**在声称"官方"的第三方网站下载安装程序或输入付款信息。
|
||||
>
|
||||
> 由于该冒充网站设有付费墙,我们**无法验证其分发的内容**。请将来自该网站的任何下载视为**潜在不安全**。
|
||||
>
|
||||
> ✅ 官方下载地址:https://github.com/code-yeongyu/oh-my-opencode/releases
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> [](https://sisyphuslabs.ai)
|
||||
@@ -5,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **Orchestrator 现已进入测试阶段。使用 `oh-my-opencode@3.0.0-beta.10` 安装。**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0 正式发布!使用 `oh-my-opencode@latest` 安装。**
|
||||
>
|
||||
> 加入我们!
|
||||
>
|
||||
@@ -63,7 +74,9 @@
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -180,7 +193,7 @@
|
||||
- Oracle:设计、调试 (GPT 5.2 Medium)
|
||||
- Frontend UI/UX Engineer:前端开发 (Gemini 3 Pro)
|
||||
- Librarian:官方文档、开源实现、代码库探索 (Claude Sonnet 4.5)
|
||||
- Explore:极速代码库探索(上下文感知 Grep)(Grok Code)
|
||||
- Explore:极速代码库探索(上下文感知 Grep)(Claude Haiku 4.5)
|
||||
- 完整 LSP / AstGrep 支持:果断重构。
|
||||
- Todo 继续执行器:如果智能体中途退出,强制它继续。**这就是让 Sisyphus 继续推动巨石的关键。**
|
||||
- 注释检查器:防止 AI 添加过多注释。Sisyphus 生成的代码应该与人类编写的代码无法区分。
|
||||
@@ -195,6 +208,8 @@
|
||||
|
||||
#### 直接安装就行。
|
||||
|
||||
你可以从 [overview page](docs/guide/overview.md) 学到很多,但以下是示例工作流程。
|
||||
|
||||
只需安装这个,你的智能体就会这样工作:
|
||||
|
||||
1. Sisyphus 不会浪费时间自己寻找文件;他保持主智能体的上下文精简。相反,他向更快、更便宜的模型并行发起后台任务,让它们为他绘制地图。
|
||||
@@ -276,7 +291,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads
|
||||
详细信息请参阅 [Features Documentation](docs/features.md)。
|
||||
|
||||
**概览:**
|
||||
- **智能体**:Sisyphus(主智能体)、Prometheus(规划器)、Oracle(架构/调试)、Librarian(文档/代码搜索)、Explore(快速代码库 grep)、Frontend Engineer(UI/UX)、Document Writer、Multimodal Looker
|
||||
- **智能体**:Sisyphus(主智能体)、Prometheus(规划器)、Oracle(架构/调试)、Librarian(文档/代码搜索)、Explore(快速代码库 grep)、Multimodal Looker
|
||||
- **后台智能体**:像真正的开发团队一样并行运行多个智能体
|
||||
- **LSP & AST 工具**:重构、重命名、诊断、AST 感知代码搜索
|
||||
- **上下文注入**:自动注入 AGENTS.md、README.md、条件规则
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
83
bun.lock
83
bun.lock
@@ -10,18 +10,15 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@code-yeongyu/comment-checker": "^0.6.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@openauthjs/openauth": "^0.4.3",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"commander": "^14.0.2",
|
||||
"detect-libc": "^2.0.0",
|
||||
"hono": "^4.10.4",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"open": "^11.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"vscode-jsonrpc": "^8.2.0",
|
||||
"zod": "^4.1.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -31,13 +28,13 @@
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.8",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-darwin-arm64": "3.1.6",
|
||||
"oh-my-opencode-darwin-x64": "3.1.6",
|
||||
"oh-my-opencode-linux-arm64": "3.1.6",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.6",
|
||||
"oh-my-opencode-linux-x64": "3.1.6",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.6",
|
||||
"oh-my-opencode-windows-x64": "3.1.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -93,24 +90,10 @@
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
|
||||
|
||||
"@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.19", "", { "dependencies": { "@opencode-ai/sdk": "1.1.19", "zod": "4.1.8" } }, "sha512-Q6qBEjHb/dJMEw4BUqQxEswTMxCCHUpFMMb6jR8HTTs8X/28XRkKt5pHNPA82GU65IlSoPRph+zd8LReBDN53Q=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
"@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="],
|
||||
|
||||
"@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="],
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
@@ -123,18 +106,12 @@
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"arctic": ["arctic@2.3.4", "", { "dependencies": { "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@oslojs/jwt": "0.2.0" } }, "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
@@ -157,12 +134,6 @@
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
||||
|
||||
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -223,16 +194,8 @@
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
@@ -263,12 +226,24 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.1.6", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KK+ptnkBigvDYbRtF/B5izEC4IoXDS8mAnRHWFBSCINhzQR2No6AtEcwijd6vKBPR+/r71ofq/8mTsIeb1PEVQ=="],
|
||||
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.1.6", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UkPI/RUi7INarFasBUZ4Rous6RUQXsU2nr0V8KFJp+70END43D/96dDUwX+zmPtpDhD+DfWkejuwzqfkZJ2ZDQ=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.1.6", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-gvmvgh7WtTtcHiCbG7z43DOYfY/jrf2S6TX/jBMX2/e1AGkcLKwz30NjGhZxeK5SyzxRVypgfZZK1IuriRgbdA=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.1.6", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-j3R76pmQ4HGVGFJUMMCeF/1lO3Jg7xFdpcBUKCeFh42N1jMgn1aeyxkAaJYB9RwCF/p6+P8B6gVDLCEDu2mxjA=="],
|
||||
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.1.6", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-VDdo0tHCOr5nm7ajd652u798nPNOLRSTcPOnVh6vIPddkZ+ujRke+enOKOw9Pd5e+4AkthqHBwFXNm2VFgnEKg=="],
|
||||
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.1.6", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-hBG/dhsr8PZelUlYsPBruSLnelB9ocB7H92I+S9svTpDVo67rAmXOoR04twKQ9TeCO4ShOa6hhMhbQnuI8fgNw=="],
|
||||
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.1.6", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-c8Awp03p2DsbS0G589nzveRCeJPgJRJ0vQrha4ChRmmo31Qc5OSmJ5xuMaF8L4nM+/trbTgAQMFMtCMLgtC8IQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -281,8 +256,6 @@
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
@@ -295,8 +268,6 @@
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
@@ -333,20 +304,14 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
|
||||
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||
|
||||
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,16 @@ A Category is an agent configuration preset optimized for specific domains.
|
||||
|
||||
### Available Built-in Categories
|
||||
|
||||
| Category | Optimal Model | Characteristics | Use Cases |
|
||||
|----------|---------------|-----------------|-----------|
|
||||
| `visual-engineering` | `gemini-3-pro` | High creativity (Temp 0.7) | Frontend, UI/UX, animations, styling |
|
||||
| `ultrabrain` | `gpt-5.2` | Maximum logical reasoning (Temp 0.1) | Architecture design, complex business logic, debugging |
|
||||
| `artistry` | `gemini-3-pro` | Artistic (Temp 0.9) | Creative ideation, design concepts, storytelling |
|
||||
| `quick` | `claude-haiku` | Fast (Temp 0.3) | Simple tasks, refactoring, script writing |
|
||||
| `writing` | `gemini-3-flash` | Natural flow (Temp 0.5) | Documentation, technical blogs, README writing |
|
||||
| `most-capable` | `claude-opus` | High performance (Temp 0.1) | Extremely difficult complex tasks |
|
||||
| Category | Default Model | Use Cases |
|
||||
|----------|---------------|-----------|
|
||||
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
||||
| `deep` | `openai/gpt-5.2-codex` (medium) | Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding. |
|
||||
| `artistry` | `google/gemini-3-pro` (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
|
||||
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -69,12 +71,12 @@ A Skill is a mechanism that injects **specialized knowledge (Context)** and **to
|
||||
|
||||
### Usage
|
||||
|
||||
Add desired skill names to the `skills` array.
|
||||
Add desired skill names to the `load_skills` array.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="quick",
|
||||
skills=["git-master"],
|
||||
load_skills=["git-master"],
|
||||
prompt="Commit current changes. Follow commit message style."
|
||||
)
|
||||
```
|
||||
@@ -109,17 +111,17 @@ You can create powerful specialized agents by combining Categories and Skills.
|
||||
|
||||
### 🎨 The Designer (UI Implementation)
|
||||
- **Category**: `visual-engineering`
|
||||
- **Skills**: `["frontend-ui-ux", "playwright"]`
|
||||
- **load_skills**: `["frontend-ui-ux", "playwright"]`
|
||||
- **Effect**: Implements aesthetic UI and verifies rendering results directly in browser.
|
||||
|
||||
### 🏗️ The Architect (Design Review)
|
||||
- **Category**: `ultrabrain`
|
||||
- **Skills**: `[]` (pure reasoning)
|
||||
- **load_skills**: `[]` (pure reasoning)
|
||||
- **Effect**: Leverages GPT-5.2's logical reasoning for in-depth system architecture analysis.
|
||||
|
||||
### ⚡ The Maintainer (Quick Fixes)
|
||||
- **Category**: `quick`
|
||||
- **Skills**: `["git-master"]`
|
||||
- **load_skills**: `["git-master"]`
|
||||
- **Effect**: Uses cost-effective models to quickly fix code and generate clean commits.
|
||||
|
||||
---
|
||||
@@ -130,7 +132,7 @@ When delegating, **clear and specific** prompts are essential. Include these 7 e
|
||||
|
||||
1. **TASK**: What needs to be done? (single objective)
|
||||
2. **EXPECTED OUTCOME**: What is the deliverable?
|
||||
3. **REQUIRED SKILLS**: Which skills should be used?
|
||||
3. **REQUIRED SKILLS**: Which skills should be loaded via `load_skills`?
|
||||
4. **REQUIRED TOOLS**: Which tools must be used? (whitelist)
|
||||
5. **MUST DO**: What must be done (constraints)
|
||||
6. **MUST NOT DO**: What must never be done
|
||||
@@ -156,12 +158,18 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `description` | string | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
|
||||
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-5`) |
|
||||
| `variant` | string | Model variant (e.g., `max`, `xhigh`) |
|
||||
| `temperature` | number | Creativity level (0.0 ~ 2.0). Lower is more deterministic. |
|
||||
| `top_p` | number | Nucleus sampling parameter (0.0 ~ 1.0) |
|
||||
| `prompt_append` | string | Content to append to system prompt when this category is selected |
|
||||
| `thinking` | object | Thinking model configuration (`{ type: "enabled", budgetTokens: 16000 }`) |
|
||||
| `reasoningEffort` | string | Reasoning effort level (`low`, `medium`, `high`) |
|
||||
| `textVerbosity` | string | Text verbosity level (`low`, `medium`, `high`) |
|
||||
| `tools` | object | Tool usage control (disable with `{ "tool_name": false }`) |
|
||||
| `maxTokens` | number | Maximum response token count |
|
||||
| `is_unstable_agent` | boolean | Mark agent as unstable - forces background mode for monitoring |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
@@ -170,7 +178,7 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
||||
"categories": {
|
||||
// 1. Define new custom category
|
||||
"korean-writer": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
"temperature": 0.5,
|
||||
"prompt_append": "You are a Korean technical writer. Maintain a friendly and clear tone."
|
||||
},
|
||||
|
||||
@@ -134,7 +134,41 @@ bunx oh-my-opencode run [prompt]
|
||||
|
||||
---
|
||||
|
||||
## 6. `auth` - Authentication Management
|
||||
## 6. `mcp oauth` - MCP OAuth Management
|
||||
|
||||
Manages OAuth 2.1 authentication for remote MCP servers.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Login to an OAuth-protected MCP server
|
||||
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
|
||||
|
||||
# Login with explicit client ID and scopes
|
||||
bunx oh-my-opencode mcp oauth login my-api --server-url https://api.example.com --client-id my-client --scopes "read,write"
|
||||
|
||||
# Remove stored OAuth tokens
|
||||
bunx oh-my-opencode mcp oauth logout <server-name>
|
||||
|
||||
# Check OAuth token status
|
||||
bunx oh-my-opencode mcp oauth status [server-name]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--server-url <url>` | MCP server URL (required for login) |
|
||||
| `--client-id <id>` | OAuth client ID (optional if server supports Dynamic Client Registration) |
|
||||
| `--scopes <scopes>` | Comma-separated OAuth scopes |
|
||||
|
||||
### Token Storage
|
||||
|
||||
Tokens are stored in `~/.config/opencode/mcp-oauth.json` with `0600` permissions (owner read/write only). Key format: `{serverHost}/{resource}`.
|
||||
|
||||
---
|
||||
|
||||
## 7. `auth` - Authentication Management
|
||||
|
||||
Manages Google Antigravity OAuth authentication. Required for using Gemini models.
|
||||
|
||||
@@ -153,7 +187,7 @@ bunx oh-my-opencode auth status
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration Files
|
||||
## 8. Configuration Files
|
||||
|
||||
The CLI searches for configuration files in the following locations (in priority order):
|
||||
|
||||
@@ -175,7 +209,7 @@ Configuration files support **JSONC (JSON with Comments)** format. You can use c
|
||||
/* Category customization */
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -183,7 +217,7 @@ Configuration files support **JSONC (JSON with Comments)** format. You can use c
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
## 9. Troubleshooting
|
||||
|
||||
### "OpenCode version too old" Error
|
||||
|
||||
@@ -213,7 +247,7 @@ bunx oh-my-opencode doctor --category authentication
|
||||
|
||||
---
|
||||
|
||||
## 9. Non-Interactive Mode
|
||||
## 10. Non-Interactive Mode
|
||||
|
||||
Use the `--no-tui` option for CI/CD environments.
|
||||
|
||||
@@ -227,7 +261,7 @@ bunx oh-my-opencode doctor --json > doctor-report.json
|
||||
|
||||
---
|
||||
|
||||
## 10. Developer Information
|
||||
## 11. Developer Information
|
||||
|
||||
### CLI Structure
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
757
docs/features.md
757
docs/features.md
@@ -1,161 +1,399 @@
|
||||
# Oh-My-OpenCode Features
|
||||
|
||||
## Agents: Your Teammates
|
||||
---
|
||||
|
||||
- **Sisyphus** (`anthropic/claude-opus-4-5`): **The default agent.** A powerful AI orchestrator for OpenCode. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Emphasizes background task delegation and todo-driven workflow. Uses Claude Opus 4.5 with extended thinking (32k budget) for maximum reasoning capability.
|
||||
- **oracle** (`openai/gpt-5.2`): Architecture, code review, strategy. Uses GPT-5.2 for its stellar logical reasoning and deep analysis. Inspired by AmpCode.
|
||||
- **librarian** (`opencode/glm-4.7-free`): Multi-repo analysis, doc lookup, implementation examples. Uses GLM-4.7 Free for deep codebase understanding and GitHub research with evidence-based answers. Inspired by AmpCode.
|
||||
- **explore** (`opencode/grok-code`, `google/gemini-3-flash`, or `anthropic/claude-haiku-4-5`): Fast codebase exploration and pattern matching. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
|
||||
- **document-writer** (`google/gemini-3-flash`): Technical writing expert. Gemini is a wordsmith—writes prose that flows.
|
||||
- **multimodal-looker** (`google/gemini-3-flash`): Visual content specialist. Analyzes PDFs, images, diagrams to extract information.
|
||||
## Agents: Your AI Team
|
||||
|
||||
Oh-My-OpenCode provides 10 specialized AI agents. Each has distinct expertise, optimized models, and tool permissions.
|
||||
|
||||
### Core Agents
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| **Sisyphus** | `anthropic/claude-opus-4-5` | **The default orchestrator.** Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro. |
|
||||
| **oracle** | `openai/gpt-5.2` | Architecture decisions, code review, debugging. Read-only consultation - stellar logical reasoning and deep analysis. Inspired by AmpCode. |
|
||||
| **librarian** | `zai-coding-plan/glm-4.7` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: glm-4.7-free → claude-sonnet-4-5. |
|
||||
| **explore** | `anthropic/claude-haiku-4-5` | Fast codebase exploration and contextual grep. Fallback: gpt-5-mini → gpt-5-nano. |
|
||||
| **multimodal-looker** | `google/gemini-3-flash` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Fallback: gpt-5.2 → glm-4.6v → kimi-k2.5 → claude-haiku-4-5 → gpt-5-nano. |
|
||||
|
||||
### Planning Agents
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| **Prometheus** | `anthropic/claude-opus-4-5` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
|
||||
| **Metis** | `anthropic/claude-opus-4-5` | Plan consultant - pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
|
||||
| **Momus** | `openai/gpt-5.2` | Plan reviewer - validates plans against clarity, verifiability, and completeness standards. Fallback: gpt-5.2 → claude-opus-4-5 → gemini-3-pro. |
|
||||
|
||||
### Invoking Agents
|
||||
|
||||
The main agent invokes these automatically, but you can call them explicitly:
|
||||
|
||||
```
|
||||
Ask @oracle to review this design and propose an architecture
|
||||
Ask @librarian how this is implemented—why does the behavior keep changing?
|
||||
Ask @librarian how this is implemented - why does the behavior keep changing?
|
||||
Ask @explore for the policy on this feature
|
||||
```
|
||||
|
||||
Customize agent models, prompts, and permissions in `oh-my-opencode.json`. See [Configuration](../README.md#configuration).
|
||||
### Tool Restrictions
|
||||
|
||||
| Agent | Restrictions |
|
||||
|-------|-------------|
|
||||
| oracle | Read-only: cannot write, edit, or delegate |
|
||||
| librarian | Cannot write, edit, or delegate |
|
||||
| explore | Cannot write, edit, or delegate |
|
||||
| multimodal-looker | Allowlist only: read, glob, grep |
|
||||
|
||||
### Background Agents
|
||||
|
||||
Run agents in the background and continue working:
|
||||
|
||||
- Have GPT debug while Claude tries different approaches
|
||||
- Gemini writes frontend while Claude handles backend
|
||||
- Fire massive parallel searches, continue implementation, use results when ready
|
||||
|
||||
```
|
||||
# Launch in background
|
||||
delegate_task(agent="explore", background=true, prompt="Find auth implementations")
|
||||
|
||||
# Continue working...
|
||||
# System notifies on completion
|
||||
|
||||
# Retrieve results when needed
|
||||
background_output(task_id="bg_abc123")
|
||||
```
|
||||
|
||||
#### Visual Multi-Agent with Tmux
|
||||
|
||||
Enable `tmux.enabled` to see background agents in separate tmux panes:
|
||||
|
||||
```json
|
||||
{
|
||||
"tmux": {
|
||||
"enabled": true,
|
||||
"layout": "main-vertical"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When running inside tmux:
|
||||
- Background agents spawn in new panes
|
||||
- Watch multiple agents work in real-time
|
||||
- Each pane shows agent output live
|
||||
- Auto-cleanup when agents complete
|
||||
|
||||
See [Tmux Integration](configurations.md#tmux-integration) for full configuration options.
|
||||
|
||||
Customize agent models, prompts, and permissions in `oh-my-opencode.json`. See [Configuration](configurations.md#agents).
|
||||
|
||||
---
|
||||
|
||||
## Background Agents: Work Like a Team
|
||||
## Skills: Specialized Knowledge
|
||||
|
||||
What if you could run these agents relentlessly, never letting them idle?
|
||||
Skills provide specialized workflows with embedded MCP servers and detailed instructions.
|
||||
|
||||
- Have GPT debug while Claude tries different approaches to find the root cause
|
||||
- Gemini writes the frontend while Claude handles the backend
|
||||
- Kick off massive parallel searches, continue implementation on other parts, then finish using the search results
|
||||
### Built-in Skills
|
||||
|
||||
These workflows are possible with OhMyOpenCode.
|
||||
| Skill | Trigger | Description |
|
||||
|-------|---------|-------------|
|
||||
| **playwright** | Browser tasks, testing, screenshots | Browser automation via Playwright MCP. MUST USE for any browser-related tasks - verification, browsing, web scraping, testing, screenshots. |
|
||||
| **frontend-ui-ux** | UI/UX tasks, styling | Designer-turned-developer persona. Crafts stunning UI/UX even without design mockups. Emphasizes bold aesthetic direction, distinctive typography, cohesive color palettes. |
|
||||
| **git-master** | commit, rebase, squash, blame | MUST USE for ANY git operations. Atomic commits with automatic splitting, rebase/squash workflows, history search (blame, bisect, log -S). |
|
||||
|
||||
Run subagents in the background. The main agent gets notified on completion. Wait for results if needed.
|
||||
### Skill: Browser Automation (playwright / agent-browser)
|
||||
|
||||
**Make your agents work like your team works.**
|
||||
**Trigger**: Any browser-related request
|
||||
|
||||
---
|
||||
Oh-My-OpenCode provides two browser automation providers, configurable via `browser_automation_engine.provider`:
|
||||
|
||||
## The Tools: Your Teammates Deserve Better
|
||||
#### Option 1: Playwright MCP (Default)
|
||||
|
||||
### Why Are You the Only One Using an IDE?
|
||||
|
||||
Syntax highlighting, autocomplete, refactoring, navigation, analysis—and now agents writing code...
|
||||
|
||||
**Why are you the only one with these tools?**
|
||||
**Give them to your agents and watch them level up.**
|
||||
|
||||
[OpenCode provides LSP](https://opencode.ai/docs/lsp/), but only for analysis.
|
||||
|
||||
The features in your editor? Other agents can't touch them.
|
||||
Hand your best tools to your best colleagues. Now they can properly refactor, navigate, and analyze.
|
||||
|
||||
- **lsp_diagnostics**: Get errors/warnings before build
|
||||
- **lsp_prepare_rename**: Validate rename operation
|
||||
- **lsp_rename**: Rename symbol across workspace
|
||||
- **ast_grep_search**: AST-aware code pattern search (25 languages)
|
||||
- **ast_grep_replace**: AST-aware code replacement
|
||||
- **call_omo_agent**: Spawn specialized explore/librarian agents. Supports `run_in_background` parameter for async execution.
|
||||
- **delegate_task**: Category-based task delegation with specialized agents. Supports pre-configured categories (visual, business-logic) or direct agent targeting. Use `background_output` to retrieve results and `background_cancel` to cancel tasks. See [Categories](../README.md#categories).
|
||||
|
||||
### Session Management
|
||||
|
||||
Tools to navigate and search your OpenCode session history:
|
||||
|
||||
- **session_list**: List all OpenCode sessions with filtering by date and limit
|
||||
- **session_read**: Read messages and history from a specific session
|
||||
- **session_search**: Full-text search across session messages
|
||||
- **session_info**: Get metadata and statistics about a session
|
||||
|
||||
These tools enable agents to reference previous conversations and maintain continuity across sessions.
|
||||
|
||||
### Context Is All You Need
|
||||
|
||||
- **Directory AGENTS.md / README.md Injector**: Auto-injects `AGENTS.md` and `README.md` when reading files. Walks from file directory to project root, collecting **all** `AGENTS.md` files along the path. Supports nested directory-specific instructions:
|
||||
```
|
||||
project/
|
||||
├── AGENTS.md # Project-wide context
|
||||
├── src/
|
||||
│ ├── AGENTS.md # src-specific context
|
||||
│ └── components/
|
||||
│ ├── AGENTS.md # Component-specific context
|
||||
│ └── Button.tsx # Reading this injects all 3 AGENTS.md files
|
||||
```
|
||||
Reading `Button.tsx` injects in order: `project/AGENTS.md` → `src/AGENTS.md` → `components/AGENTS.md`. Each directory's context is injected once per session.
|
||||
- **Conditional Rules Injector**: Not all rules apply all the time. Injects rules from `.claude/rules/` when conditions match.
|
||||
- Walks upward from file directory to project root, plus `~/.claude/rules/` (user).
|
||||
- Supports `.md` and `.mdc` files.
|
||||
- Matches via `globs` field in frontmatter.
|
||||
- `alwaysApply: true` for rules that should always fire.
|
||||
- Example rule file:
|
||||
```markdown
|
||||
---
|
||||
globs: ["*.ts", "src/**/*.js"]
|
||||
description: "TypeScript/JavaScript coding rules"
|
||||
---
|
||||
- Use PascalCase for interface names
|
||||
- Use camelCase for function names
|
||||
```
|
||||
- **Online**: Project rules aren't everything. Built-in MCPs for extended capabilities:
|
||||
- **websearch**: Real-time web search powered by [Exa AI](https://exa.ai)
|
||||
- **context7**: Official documentation lookup
|
||||
- **grep_app**: Ultra-fast code search across public GitHub repos (great for finding implementation examples)
|
||||
|
||||
### Be Multimodal. Save Tokens.
|
||||
|
||||
The look_at tool from AmpCode, now in OhMyOpenCode.
|
||||
Instead of the agent reading massive files and bloating context, it internally leverages another agent to extract just what it needs.
|
||||
|
||||
### I Removed Their Blockers
|
||||
|
||||
- Replaces built-in grep and glob tools. Default implementation has no timeout—can hang forever.
|
||||
|
||||
### Skill-Embedded MCP Support
|
||||
|
||||
Skills can now bring their own MCP servers. Define MCP configurations directly in skill frontmatter or via `mcp.json` files:
|
||||
The default provider uses Playwright MCP server:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: Browser automation skill
|
||||
mcp:
|
||||
playwright:
|
||||
command: npx
|
||||
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
||||
---
|
||||
args: ["@playwright/mcp@latest"]
|
||||
```
|
||||
|
||||
When you load a skill with embedded MCP, its tools become available automatically. The `skill_mcp` tool lets you invoke these MCP operations with full schema discovery.
|
||||
**Usage**:
|
||||
```
|
||||
/playwright Navigate to example.com and take a screenshot
|
||||
```
|
||||
|
||||
**Built-in Skills:**
|
||||
- **playwright**: Browser automation, web scraping, testing, and screenshots out of the box
|
||||
#### Option 2: Agent Browser CLI (Vercel)
|
||||
|
||||
Disable built-in skills via `disabled_skills: ["playwright"]` in your config.
|
||||
Alternative provider using [Vercel's agent-browser CLI](https://github.com/vercel-labs/agent-browser):
|
||||
|
||||
```json
|
||||
{
|
||||
"browser_automation_engine": {
|
||||
"provider": "agent-browser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Requires installation**:
|
||||
```bash
|
||||
bun add -g agent-browser
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
Use agent-browser to navigate to example.com and extract the main heading
|
||||
```
|
||||
|
||||
#### Capabilities (Both Providers)
|
||||
|
||||
- Navigate and interact with web pages
|
||||
- Take screenshots and PDFs
|
||||
- Fill forms and click elements
|
||||
- Wait for network requests
|
||||
- Scrape content
|
||||
|
||||
### Skill: frontend-ui-ux
|
||||
|
||||
**Trigger**: UI design tasks, visual changes
|
||||
|
||||
A designer-turned-developer who crafts stunning interfaces:
|
||||
|
||||
- **Design Process**: Purpose, Tone, Constraints, Differentiation
|
||||
- **Aesthetic Direction**: Choose extreme - brutalist, maximalist, retro-futuristic, luxury, playful
|
||||
- **Typography**: Distinctive fonts, avoid generic (Inter, Roboto, Arial)
|
||||
- **Color**: Cohesive palettes with sharp accents, avoid purple-on-white AI slop
|
||||
- **Motion**: High-impact staggered reveals, scroll-triggering, surprising hover states
|
||||
- **Anti-Patterns**: Generic fonts, predictable layouts, cookie-cutter design
|
||||
|
||||
### Skill: git-master
|
||||
|
||||
**Trigger**: commit, rebase, squash, "who wrote", "when was X added"
|
||||
|
||||
Three specializations in one:
|
||||
|
||||
1. **Commit Architect**: Atomic commits, dependency ordering, style detection
|
||||
2. **Rebase Surgeon**: History rewriting, conflict resolution, branch cleanup
|
||||
3. **History Archaeologist**: Finding when/where specific changes were introduced
|
||||
|
||||
**Core Principle - Multiple Commits by Default**:
|
||||
```
|
||||
3+ files -> MUST be 2+ commits
|
||||
5+ files -> MUST be 3+ commits
|
||||
10+ files -> MUST be 5+ commits
|
||||
```
|
||||
|
||||
**Automatic Style Detection**:
|
||||
- Analyzes last 30 commits for language (Korean/English) and style (semantic/plain/short)
|
||||
- Matches your repo's commit conventions automatically
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
/git-master commit these changes
|
||||
/git-master rebase onto main
|
||||
/git-master who wrote this authentication code?
|
||||
```
|
||||
|
||||
### Custom Skills
|
||||
|
||||
Load custom skills from:
|
||||
- `.opencode/skills/*/SKILL.md` (project)
|
||||
- `~/.config/opencode/skills/*/SKILL.md` (user)
|
||||
- `.claude/skills/*/SKILL.md` (Claude Code compat)
|
||||
- `~/.claude/skills/*/SKILL.md` (Claude Code user)
|
||||
|
||||
Disable built-in skills via `disabled_skills: ["playwright"]` in config.
|
||||
|
||||
---
|
||||
|
||||
## Goodbye Claude Code. Hello Oh My OpenCode.
|
||||
## Commands: Slash Workflows
|
||||
|
||||
Oh My OpenCode has a Claude Code compatibility layer.
|
||||
If you were using Claude Code, your existing config just works.
|
||||
Commands are slash-triggered workflows that execute predefined templates.
|
||||
|
||||
### Hooks Integration
|
||||
### Built-in Commands
|
||||
|
||||
Run custom scripts via Claude Code's `settings.json` hook system.
|
||||
Oh My OpenCode reads and executes hooks from:
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/init-deep` | Initialize hierarchical AGENTS.md knowledge base |
|
||||
| `/ralph-loop` | Start self-referential development loop until completion |
|
||||
| `/ulw-loop` | Start ultrawork loop - continues with ultrawork mode |
|
||||
| `/cancel-ralph` | Cancel active Ralph Loop |
|
||||
| `/refactor` | Intelligent refactoring with LSP, AST-grep, architecture analysis, and TDD verification |
|
||||
| `/start-work` | Start Sisyphus work session from Prometheus plan |
|
||||
|
||||
- `~/.claude/settings.json` (user)
|
||||
- `./.claude/settings.json` (project)
|
||||
- `./.claude/settings.local.json` (local, git-ignored)
|
||||
### Command: /init-deep
|
||||
|
||||
Supported hook events:
|
||||
- **PreToolUse**: Runs before tool execution. Can block or modify tool input.
|
||||
- **PostToolUse**: Runs after tool execution. Can add warnings or context.
|
||||
- **UserPromptSubmit**: Runs when user submits prompt. Can block or inject messages.
|
||||
- **Stop**: Runs when session goes idle. Can inject follow-up prompts.
|
||||
**Purpose**: Generate hierarchical AGENTS.md files throughout your project
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
/init-deep [--create-new] [--max-depth=N]
|
||||
```
|
||||
|
||||
Creates directory-specific context files that agents automatically read:
|
||||
```
|
||||
project/
|
||||
├── AGENTS.md # Project-wide context
|
||||
├── src/
|
||||
│ ├── AGENTS.md # src-specific context
|
||||
│ └── components/
|
||||
│ └── AGENTS.md # Component-specific context
|
||||
```
|
||||
|
||||
### Command: /ralph-loop
|
||||
|
||||
**Purpose**: Self-referential development loop that runs until task completion
|
||||
|
||||
**Named after**: Anthropic's Ralph Wiggum plugin
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
/ralph-loop "Build a REST API with authentication"
|
||||
/ralph-loop "Refactor the payment module" --max-iterations=50
|
||||
```
|
||||
|
||||
**Behavior**:
|
||||
- Agent works continuously toward the goal
|
||||
- Detects `<promise>DONE</promise>` to know when complete
|
||||
- Auto-continues if agent stops without completion
|
||||
- Ends when: completion detected, max iterations reached (default 100), or `/cancel-ralph`
|
||||
|
||||
**Configure**: `{ "ralph_loop": { "enabled": true, "default_max_iterations": 100 } }`
|
||||
|
||||
### Command: /ulw-loop
|
||||
|
||||
**Purpose**: Same as ralph-loop but with ultrawork mode active
|
||||
|
||||
Everything runs at maximum intensity - parallel agents, background tasks, aggressive exploration.
|
||||
|
||||
### Command: /refactor
|
||||
|
||||
**Purpose**: Intelligent refactoring with full toolchain
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
/refactor <target> [--scope=<file|module|project>] [--strategy=<safe|aggressive>]
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- LSP-powered rename and navigation
|
||||
- AST-grep for pattern matching
|
||||
- Architecture analysis before changes
|
||||
- TDD verification after changes
|
||||
- Codemap generation
|
||||
|
||||
### Command: /start-work
|
||||
|
||||
**Purpose**: Start execution from a Prometheus-generated plan
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
/start-work [plan-name]
|
||||
```
|
||||
|
||||
Uses atlas agent to execute planned tasks systematically.
|
||||
|
||||
### Custom Commands
|
||||
|
||||
Load custom commands from:
|
||||
- `.opencode/command/*.md` (project)
|
||||
- `~/.config/opencode/command/*.md` (user)
|
||||
- `.claude/commands/*.md` (Claude Code compat)
|
||||
- `~/.claude/commands/*.md` (Claude Code user)
|
||||
|
||||
---
|
||||
|
||||
## Hooks: Lifecycle Automation
|
||||
|
||||
Hooks intercept and modify behavior at key points in the agent lifecycle.
|
||||
|
||||
### Hook Events
|
||||
|
||||
| Event | When | Can |
|
||||
|-------|------|-----|
|
||||
| **PreToolUse** | Before tool execution | Block, modify input, inject context |
|
||||
| **PostToolUse** | After tool execution | Add warnings, modify output, inject messages |
|
||||
| **UserPromptSubmit** | When user submits prompt | Block, inject messages, transform prompt |
|
||||
| **Stop** | When session goes idle | Inject follow-up prompts |
|
||||
|
||||
### Built-in Hooks
|
||||
|
||||
#### Context & Injection
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **directory-agents-injector** | PostToolUse | Auto-injects AGENTS.md when reading files. Walks from file to project root, collecting all AGENTS.md files. **Deprecated for OpenCode 1.1.37+** - Auto-disabled when native AGENTS.md injection is available. |
|
||||
| **directory-readme-injector** | PostToolUse | Auto-injects README.md for directory context. |
|
||||
| **rules-injector** | PostToolUse | Injects rules from `.claude/rules/` when conditions match. Supports globs and alwaysApply. |
|
||||
| **compaction-context-injector** | Stop | Preserves critical context during session compaction. |
|
||||
|
||||
#### Productivity & Control
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **keyword-detector** | UserPromptSubmit | Detects keywords and activates modes: `ultrawork`/`ulw` (max performance), `search`/`find` (parallel exploration), `analyze`/`investigate` (deep analysis). |
|
||||
| **think-mode** | UserPromptSubmit | Auto-detects extended thinking needs. Catches "think deeply", "ultrathink" and adjusts model settings. |
|
||||
| **ralph-loop** | Stop | Manages self-referential loop continuation. |
|
||||
| **start-work** | PostToolUse | Handles /start-work command execution. |
|
||||
| **auto-slash-command** | UserPromptSubmit | Automatically executes slash commands from prompts. |
|
||||
|
||||
#### Quality & Safety
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **comment-checker** | PostToolUse | Reminds agents to reduce excessive comments. Smartly ignores BDD, directives, docstrings. |
|
||||
| **thinking-block-validator** | PreToolUse | Validates thinking blocks to prevent API errors. |
|
||||
| **empty-message-sanitizer** | PreToolUse | Prevents API errors from empty chat messages. |
|
||||
| **edit-error-recovery** | PostToolUse | Recovers from edit tool failures. |
|
||||
|
||||
#### Recovery & Stability
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **session-recovery** | Stop | Recovers from session errors - missing tool results, thinking block issues, empty messages. |
|
||||
| **anthropic-context-window-limit-recovery** | Stop | Handles Claude context window limits gracefully. |
|
||||
| **background-compaction** | Stop | Auto-compacts sessions hitting token limits. |
|
||||
|
||||
#### Truncation & Context Management
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **grep-output-truncator** | PostToolUse | Dynamically truncates grep output based on context window. Keeps 50% headroom, caps at 50k tokens. |
|
||||
| **tool-output-truncator** | PostToolUse | Truncates output from Grep, Glob, LSP, AST-grep tools. |
|
||||
|
||||
#### Notifications & UX
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **auto-update-checker** | UserPromptSubmit | Checks for new versions, shows startup toast with version and Sisyphus status. |
|
||||
| **background-notification** | Stop | Notifies when background agent tasks complete. |
|
||||
| **session-notification** | Stop | OS notifications when agents go idle. Works on macOS, Linux, Windows. |
|
||||
| **agent-usage-reminder** | PostToolUse | Reminds you to leverage specialized agents for better results. |
|
||||
|
||||
#### Task Management
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **task-resume-info** | PostToolUse | Provides task resume information for continuity. |
|
||||
| **delegate-task-retry** | PostToolUse | Retries failed delegate_task calls. |
|
||||
|
||||
#### Integration
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **claude-code-hooks** | All | Executes hooks from Claude Code's settings.json. |
|
||||
| **atlas** | All | Main orchestration logic (771 lines). |
|
||||
| **interactive-bash-session** | PreToolUse | Manages tmux sessions for interactive CLI. |
|
||||
| **non-interactive-env** | PreToolUse | Handles non-interactive environment constraints. |
|
||||
|
||||
#### Specialized
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **prometheus-md-only** | PostToolUse | Enforces markdown-only output for Prometheus planner. |
|
||||
|
||||
### Claude Code Hooks Integration
|
||||
|
||||
Run custom scripts via Claude Code's `settings.json`:
|
||||
|
||||
Example `settings.json`:
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
@@ -169,37 +407,215 @@ Example `settings.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Hook locations**:
|
||||
- `~/.claude/settings.json` (user)
|
||||
- `./.claude/settings.json` (project)
|
||||
- `./.claude/settings.local.json` (local, git-ignored)
|
||||
|
||||
### Disabling Hooks
|
||||
|
||||
Disable specific hooks in config:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": [
|
||||
"comment-checker",
|
||||
"auto-update-checker",
|
||||
"startup-toast"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tools: Agent Capabilities
|
||||
|
||||
### LSP Tools (IDE Features for Agents)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **lsp_diagnostics** | Get errors/warnings before build |
|
||||
| **lsp_prepare_rename** | Validate rename operation |
|
||||
| **lsp_rename** | Rename symbol across workspace |
|
||||
| **lsp_goto_definition** | Jump to symbol definition |
|
||||
| **lsp_find_references** | Find all usages across workspace |
|
||||
| **lsp_symbols** | Get file outline or workspace symbol search |
|
||||
|
||||
### AST-Grep Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **ast_grep_search** | AST-aware code pattern search (25 languages) |
|
||||
| **ast_grep_replace** | AST-aware code replacement |
|
||||
|
||||
### Delegation Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **call_omo_agent** | Spawn explore/librarian agents. Supports `run_in_background`. |
|
||||
| **delegate_task** | Category-based task delegation. Supports categories (visual, business-logic) or direct agent targeting. |
|
||||
| **background_output** | Retrieve background task results |
|
||||
| **background_cancel** | Cancel running background tasks |
|
||||
|
||||
### Session Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **session_list** | List all OpenCode sessions |
|
||||
| **session_read** | Read messages and history from a session |
|
||||
| **session_search** | Full-text search across session messages |
|
||||
| **session_info** | Get session metadata and statistics |
|
||||
|
||||
### Interactive Terminal Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **interactive_bash** | Tmux-based terminal for TUI apps (vim, htop, pudb). Pass tmux subcommands directly without prefix. |
|
||||
|
||||
**Usage Examples**:
|
||||
```bash
|
||||
# Create a new session
|
||||
interactive_bash(tmux_command="new-session -d -s dev-app")
|
||||
|
||||
# Send keystrokes to a session
|
||||
interactive_bash(tmux_command="send-keys -t dev-app 'vim main.py' Enter")
|
||||
|
||||
# Capture pane output
|
||||
interactive_bash(tmux_command="capture-pane -p -t dev-app")
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Commands are tmux subcommands (no `tmux` prefix)
|
||||
- Use for interactive apps that need persistent sessions
|
||||
- One-shot commands should use regular `Bash` tool with `&`
|
||||
|
||||
---
|
||||
|
||||
## MCPs: Built-in Servers
|
||||
|
||||
### websearch (Exa AI)
|
||||
|
||||
Real-time web search powered by [Exa AI](https://exa.ai).
|
||||
|
||||
### context7
|
||||
|
||||
Official documentation lookup for any library/framework.
|
||||
|
||||
### grep_app
|
||||
|
||||
Ultra-fast code search across public GitHub repos. Great for finding implementation examples.
|
||||
|
||||
### Skill-Embedded MCPs
|
||||
|
||||
Skills can bring their own MCP servers:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: Browser automation skill
|
||||
mcp:
|
||||
playwright:
|
||||
command: npx
|
||||
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
||||
---
|
||||
```
|
||||
|
||||
The `skill_mcp` tool invokes these operations with full schema discovery.
|
||||
|
||||
#### OAuth-Enabled MCPs
|
||||
|
||||
Skills can define OAuth-protected remote MCP servers. OAuth 2.1 with full RFC compliance (RFC 9728, 8414, 8707, 7591) is supported:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: My API skill
|
||||
mcp:
|
||||
my-api:
|
||||
url: https://api.example.com/mcp
|
||||
oauth:
|
||||
clientId: ${CLIENT_ID}
|
||||
scopes: ["read", "write"]
|
||||
---
|
||||
```
|
||||
|
||||
When a skill MCP has `oauth` configured:
|
||||
- **Auto-discovery**: Fetches `/.well-known/oauth-protected-resource` (RFC 9728), falls back to `/.well-known/oauth-authorization-server` (RFC 8414)
|
||||
- **Dynamic Client Registration**: Auto-registers with servers supporting RFC 7591 (clientId becomes optional)
|
||||
- **PKCE**: Mandatory for all flows
|
||||
- **Resource Indicators**: Auto-generated from MCP URL per RFC 8707
|
||||
- **Token Storage**: Persisted in `~/.config/opencode/mcp-oauth.json` (chmod 0600)
|
||||
- **Auto-refresh**: Tokens refresh on 401; step-up authorization on 403 with `WWW-Authenticate`
|
||||
- **Dynamic Port**: OAuth callback server uses an auto-discovered available port
|
||||
|
||||
Pre-authenticate via CLI:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Injection
|
||||
|
||||
### Directory AGENTS.md
|
||||
|
||||
Auto-injects AGENTS.md when reading files. Walks from file directory to project root:
|
||||
|
||||
```
|
||||
project/
|
||||
├── AGENTS.md # Injected first
|
||||
├── src/
|
||||
│ ├── AGENTS.md # Injected second
|
||||
│ └── components/
|
||||
│ ├── AGENTS.md # Injected third
|
||||
│ └── Button.tsx # Reading this injects all 3
|
||||
```
|
||||
|
||||
### Conditional Rules
|
||||
|
||||
Inject rules from `.claude/rules/` when conditions match:
|
||||
|
||||
```markdown
|
||||
---
|
||||
globs: ["*.ts", "src/**/*.js"]
|
||||
description: "TypeScript/JavaScript coding rules"
|
||||
---
|
||||
- Use PascalCase for interface names
|
||||
- Use camelCase for function names
|
||||
```
|
||||
|
||||
Supports:
|
||||
- `.md` and `.mdc` files
|
||||
- `globs` field for pattern matching
|
||||
- `alwaysApply: true` for unconditional rules
|
||||
- Walks upward from file to project root, plus `~/.claude/rules/`
|
||||
|
||||
---
|
||||
|
||||
## Claude Code Compatibility
|
||||
|
||||
Full compatibility layer for Claude Code configurations.
|
||||
|
||||
### Config Loaders
|
||||
|
||||
**Command Loader**: Loads markdown-based slash commands from 4 directories:
|
||||
- `~/.claude/commands/` (user)
|
||||
- `./.claude/commands/` (project)
|
||||
- `~/.config/opencode/command/` (opencode global)
|
||||
- `./.opencode/command/` (opencode project)
|
||||
| Type | Locations |
|
||||
|------|-----------|
|
||||
| **Commands** | `~/.claude/commands/`, `.claude/commands/` |
|
||||
| **Skills** | `~/.claude/skills/*/SKILL.md`, `.claude/skills/*/SKILL.md` |
|
||||
| **Agents** | `~/.claude/agents/*.md`, `.claude/agents/*.md` |
|
||||
| **MCPs** | `~/.claude/.mcp.json`, `.mcp.json`, `.claude/.mcp.json` |
|
||||
|
||||
**Skill Loader**: Loads directory-based skills with `SKILL.md`:
|
||||
- `~/.claude/skills/` (user)
|
||||
- `./.claude/skills/` (project)
|
||||
|
||||
**Agent Loader**: Loads custom agent definitions from markdown files:
|
||||
- `~/.claude/agents/*.md` (user)
|
||||
- `./.claude/agents/*.md` (project)
|
||||
|
||||
**MCP Loader**: Loads MCP server configs from `.mcp.json` files:
|
||||
- `~/.claude/.mcp.json` (user)
|
||||
- `./.mcp.json` (project)
|
||||
- `./.claude/.mcp.json` (local)
|
||||
- Supports environment variable expansion (`${VAR}` syntax)
|
||||
MCP configs support environment variable expansion: `${VAR}`.
|
||||
|
||||
### Data Storage
|
||||
|
||||
**Todo Management**: Session todos stored in `~/.claude/todos/` in Claude Code compatible format.
|
||||
|
||||
**Transcript**: Session activity logged to `~/.claude/transcripts/` in JSONL format for replay and analysis.
|
||||
| Data | Location | Format |
|
||||
|------|----------|--------|
|
||||
| Todos | `~/.claude/todos/` | Claude Code compatible |
|
||||
| Transcripts | `~/.claude/transcripts/` | JSONL |
|
||||
|
||||
### Compatibility Toggles
|
||||
|
||||
Disable specific Claude Code compatibility features with the `claude_code` config object:
|
||||
Disable specific features:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -214,64 +630,23 @@ Disable specific Claude Code compatibility features with the `claude_code` confi
|
||||
}
|
||||
```
|
||||
|
||||
| Toggle | When `false`, stops loading from... | Unaffected |
|
||||
| ---------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| `mcp` | `~/.claude/.mcp.json`, `./.mcp.json`, `./.claude/.mcp.json` | Built-in MCP (context7, grep_app) |
|
||||
| `commands` | `~/.claude/commands/*.md`, `./.claude/commands/*.md` | `~/.config/opencode/command/`, `./.opencode/command/` |
|
||||
| `skills` | `~/.claude/skills/*/SKILL.md`, `./.claude/skills/*/SKILL.md` | - |
|
||||
| `agents` | `~/.claude/agents/*.md`, `./.claude/agents/*.md` | Built-in agents (oracle, librarian, etc.) |
|
||||
| `hooks` | `~/.claude/settings.json`, `./.claude/settings.json`, `./.claude/settings.local.json` | - |
|
||||
| `plugins` | `~/.claude/plugins/` (Claude Code marketplace plugins) | - |
|
||||
| Toggle | Disables |
|
||||
|--------|----------|
|
||||
| `mcp` | `.mcp.json` files (keeps built-in MCPs) |
|
||||
| `commands` | `~/.claude/commands/`, `.claude/commands/` |
|
||||
| `skills` | `~/.claude/skills/`, `.claude/skills/` |
|
||||
| `agents` | `~/.claude/agents/` (keeps built-in agents) |
|
||||
| `hooks` | settings.json hooks |
|
||||
| `plugins` | Claude Code marketplace plugins |
|
||||
|
||||
All toggles default to `true` (enabled). Omit the `claude_code` object for full Claude Code compatibility.
|
||||
|
||||
**Selectively disable specific plugins** using `plugins_override`:
|
||||
Disable specific plugins:
|
||||
|
||||
```json
|
||||
{
|
||||
"claude_code": {
|
||||
"plugins_override": {
|
||||
"claude-mem@thedotmack": false,
|
||||
"some-other-plugin@marketplace": false
|
||||
"claude-mem@thedotmack": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to keep the plugin system enabled while disabling specific plugins by their full identifier (`plugin-name@marketplace-name`).
|
||||
|
||||
---
|
||||
|
||||
## Not Just for the Agents
|
||||
|
||||
When agents thrive, you thrive. But I want to help you directly too.
|
||||
|
||||
- **Ralph Loop**: Self-referential development loop that runs until task completion. Inspired by Anthropic's Ralph Wiggum plugin. **Supports all programming languages.**
|
||||
- Start with `/ralph-loop "Build a REST API"` and let the agent work continuously
|
||||
- Loop detects `<promise>DONE</promise>` to know when complete
|
||||
- Auto-continues if agent stops without completion promise
|
||||
- Ends when: completion detected, max iterations reached (default 100), or `/cancel-ralph`
|
||||
- Configure in `oh-my-opencode.json`: `{ "ralph_loop": { "enabled": true, "default_max_iterations": 100 } }`
|
||||
- **Keyword Detector**: Automatically detects keywords in your prompts and activates specialized modes:
|
||||
- `ultrawork` / `ulw`: Maximum performance mode with parallel agent orchestration
|
||||
- `search` / `find` / `찾아` / `検索`: Maximized search effort with parallel explore and librarian agents
|
||||
- `analyze` / `investigate` / `분석` / `調査`: Deep analysis mode with multi-phase expert consultation
|
||||
- **Todo Continuation Enforcer**: Makes agents finish all TODOs before stopping. Kills the chronic LLM habit of quitting halfway.
|
||||
- **Comment Checker**: LLMs love comments. Too many comments. This reminds them to cut the noise. Smartly ignores valid patterns (BDD, directives, docstrings) and demands justification for the rest. Clean code wins.
|
||||
- **Think Mode**: Auto-detects when extended thinking is needed and switches modes. Catches phrases like "think deeply" or "ultrathink" and dynamically adjusts model settings for maximum reasoning.
|
||||
- **Context Window Monitor**: Implements [Context Window Anxiety Management](https://agentic-patterns.com/patterns/context-window-anxiety-management/).
|
||||
- At 70%+ usage, reminds agents there's still headroom—prevents rushed, sloppy work.
|
||||
- **Agent Usage Reminder**: When you call search tools directly, reminds you to leverage specialized agents via background tasks for better results.
|
||||
- **Anthropic Auto Compact**: When Claude models hit token limits, automatically summarizes and compacts the session—no manual intervention needed.
|
||||
- **Session Recovery**: Automatically recovers from session errors (missing tool results, thinking block issues, empty messages). Sessions don't crash mid-run. Even if they do, they recover.
|
||||
- **Auto Update Checker**: Automatically checks for new versions of oh-my-opencode and can auto-update your configuration. Shows startup toast notifications displaying current version and Sisyphus status ("Sisyphus on steroids is steering OpenCode" when enabled, or "OpenCode is now on Steroids. oMoMoMoMo..." otherwise). Disable all features with `"auto-update-checker"` in `disabled_hooks`, or disable just toast notifications with `"startup-toast"` in `disabled_hooks`. See [Configuration > Hooks](../README.md#hooks).
|
||||
- **Background Notification**: Get notified when background agent tasks complete.
|
||||
- **Session Notification**: Sends OS notifications when agents go idle. Works on macOS, Linux, and Windows—never miss when your agent needs input.
|
||||
- **Empty Task Response Detector**: Catches when Task tool returns nothing. Warns you about potential agent failures so you don't wait forever for a response that already came back empty.
|
||||
- **Empty Message Sanitizer**: Prevents API errors from empty chat messages by automatically sanitizing message content before sending.
|
||||
- **Grep Output Truncator**: Grep can return mountains of text. This dynamically truncates output based on your remaining context window—keeps 50% headroom, caps at 50k tokens.
|
||||
- **Tool Output Truncator**: Same idea, broader scope. Truncates output from Grep, Glob, LSP tools, and AST-grep. Prevents one verbose search from eating your entire context.
|
||||
- **Preemptive Compaction**: Compacts session proactively before hitting hard token limits. Runs at 85% context window usage. **Enabled by default.** Disable via `disabled_hooks: ["preemptive-compaction"]`.
|
||||
- **Compaction Context Injector**: Preserves critical context (AGENTS.md, current directory info) during session compaction so you don't lose important state.
|
||||
- **Thinking Block Validator**: Validates thinking blocks to ensure proper formatting and prevent API errors from malformed thinking content.
|
||||
- **Claude Code Hooks**: Executes hooks from Claude Code's settings.json - this is the compatibility layer that runs PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks.
|
||||
|
||||
@@ -33,7 +33,7 @@ follow these steps:
|
||||
|
||||
### Step 0: Ask user about subscriptions
|
||||
|
||||
Ask the user these 3 questions to determine CLI options:
|
||||
Ask the user these questions to determine CLI options:
|
||||
|
||||
1. **Do you have a Claude Pro/Max Subscription?**
|
||||
- If **yes**: **Are you on max20 (20x mode)?**
|
||||
@@ -41,9 +41,9 @@ Ask the user these 3 questions to determine CLI options:
|
||||
- **not max20** → `--claude=yes`
|
||||
- If **no** → `--claude=no`
|
||||
|
||||
2. **Do you have a ChatGPT Subscription?**
|
||||
- If **yes** → `--chatgpt=yes`
|
||||
- If **no** → `--chatgpt=no`
|
||||
2. **Do you have an OpenAI/ChatGPT Plus Subscription?**
|
||||
- If **yes** → `--openai=yes` (GPT-5.2 for Oracle agent)
|
||||
- If **no** → `--openai=no` (default)
|
||||
|
||||
3. **Will you integrate Gemini models?**
|
||||
- If **yes** → `--gemini=yes`
|
||||
@@ -53,7 +53,17 @@ Ask the user these 3 questions to determine CLI options:
|
||||
- If **yes** → `--copilot=yes`
|
||||
- If **no** → `--copilot=no`
|
||||
|
||||
MUST STRONGLY WARNING, WHEN USER SAID THEY DON'T HAVE CLAUDE SUBSCRIPTION, SISYPHUS AGENT MIGHT NOT WORK IDEALY.
|
||||
5. **Do you have access to OpenCode Zen (opencode/ models)?**
|
||||
- If **yes** → `--opencode-zen=yes`
|
||||
- If **no** → `--opencode-zen=no` (default)
|
||||
|
||||
6. **Do you have a Z.ai Coding Plan subscription?**
|
||||
- If **yes** → `--zai-coding-plan=yes`
|
||||
- If **no** → `--zai-coding-plan=no` (default)
|
||||
|
||||
**Provider Priority**: Native (anthropic/, openai/, google/) > GitHub Copilot > OpenCode Zen > Z.ai Coding Plan
|
||||
|
||||
MUST STRONGLY WARNING, WHEN USER SAID THEY DON'T HAVE CLAUDE SUBSCRIPTION, SISYPHUS AGENT MIGHT NOT WORK IDEALLY.
|
||||
|
||||
### Step 1: Install OpenCode (if not installed)
|
||||
|
||||
@@ -74,14 +84,17 @@ Spawn a subagent to handle installation and report back - to save context.
|
||||
Based on user's answers, run the CLI installer with appropriate flags:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --chatgpt=<yes|no> --gemini=<yes|no> --copilot=<yes|no>
|
||||
bunx oh-my-opencode install --no-tui --claude=<yes|no|max20> --gemini=<yes|no> --copilot=<yes|no> [--openai=<yes|no>] [--opencode-zen=<yes|no>] [--zai-coding-plan=<yes|no>]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- User has all subscriptions with max20: `bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes --copilot=no`
|
||||
- User has only Claude (no max20): `bunx oh-my-opencode install --no-tui --claude=yes --chatgpt=no --gemini=no --copilot=no`
|
||||
- User has only GitHub Copilot: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes`
|
||||
- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no`
|
||||
- User has all native subscriptions: `bunx oh-my-opencode install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no`
|
||||
- User has only Claude: `bunx oh-my-opencode install --no-tui --claude=yes --gemini=no --copilot=no`
|
||||
- User has Claude + OpenAI: `bunx oh-my-opencode install --no-tui --claude=yes --openai=yes --gemini=no --copilot=no`
|
||||
- User has only GitHub Copilot: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes`
|
||||
- User has Z.ai for Librarian: `bunx oh-my-opencode install --no-tui --claude=yes --gemini=no --copilot=no --zai-coding-plan=yes`
|
||||
- User has only OpenCode Zen: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no --opencode-zen=yes`
|
||||
- User has no subscriptions: `bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=no`
|
||||
|
||||
The CLI will:
|
||||
- Register the plugin in `opencode.json`
|
||||
@@ -119,7 +132,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-antigravity-auth@1.2.8"
|
||||
"opencode-antigravity-auth@latest"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -127,7 +140,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
##### Model Configuration
|
||||
|
||||
You'll also need full model settings in `opencode.json`.
|
||||
Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/opencode-antigravity-auth), copy provider/models config from the README, and merge carefully to avoid breaking the user's existing setup.
|
||||
Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/opencode-antigravity-auth), copy the full model configuration from the README, and merge carefully to avoid breaking the user's existing setup. The plugin now uses a **variant system** — models like `antigravity-gemini-3-pro` support `low`/`high` variants instead of separate `-low`/`-high` model entries.
|
||||
|
||||
##### oh-my-opencode Agent Model Override
|
||||
|
||||
@@ -136,14 +149,22 @@ The `opencode-antigravity-auth` plugin uses different model names than the built
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/antigravity-gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/antigravity-gemini-3-flash" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available model names**: `google/antigravity-gemini-3-pro-high`, `google/antigravity-gemini-3-pro-low`, `google/antigravity-gemini-3-flash`, `google/antigravity-claude-sonnet-4-5`, `google/antigravity-claude-sonnet-4-5-thinking-low`, `google/antigravity-claude-sonnet-4-5-thinking-medium`, `google/antigravity-claude-sonnet-4-5-thinking-high`, `google/antigravity-claude-opus-4-5-thinking-low`, `google/antigravity-claude-opus-4-5-thinking-medium`, `google/antigravity-claude-opus-4-5-thinking-high`, `google/gemini-3-pro-preview`, `google/gemini-3-flash-preview`, `google/gemini-2.5-pro`, `google/gemini-2.5-flash`
|
||||
**Available models (Antigravity quota)**:
|
||||
- `google/antigravity-gemini-3-pro` — variants: `low`, `high`
|
||||
- `google/antigravity-gemini-3-flash` — variants: `minimal`, `low`, `medium`, `high`
|
||||
- `google/antigravity-claude-sonnet-4-5` — no variants
|
||||
- `google/antigravity-claude-sonnet-4-5-thinking` — variants: `low`, `max`
|
||||
- `google/antigravity-claude-opus-4-5-thinking` — variants: `low`, `max`
|
||||
|
||||
**Available models (Gemini CLI quota)**:
|
||||
- `google/gemini-2.5-flash`, `google/gemini-2.5-pro`, `google/gemini-3-flash-preview`, `google/gemini-3-pro-preview`
|
||||
|
||||
> **Note**: Legacy tier-suffixed names like `google/antigravity-gemini-3-pro-high` still work but variants are recommended. Use `--variant=high` with the base model name instead.
|
||||
|
||||
Then authenticate:
|
||||
|
||||
@@ -160,23 +181,49 @@ opencode auth login
|
||||
|
||||
#### GitHub Copilot (Fallback Provider)
|
||||
|
||||
GitHub Copilot is supported as a **fallback provider** when native providers (Claude, ChatGPT, Gemini) are unavailable. The installer configures Copilot with lower priority than native providers.
|
||||
GitHub Copilot is supported as a **fallback provider** when native providers are unavailable.
|
||||
|
||||
**Priority**: Native providers (Claude/ChatGPT/Gemini) > GitHub Copilot > Free models
|
||||
**Priority**: Native (anthropic/, openai/, google/) > GitHub Copilot > OpenCode Zen > Z.ai Coding Plan
|
||||
|
||||
##### Model Mappings
|
||||
|
||||
When GitHub Copilot is enabled, oh-my-opencode uses these model assignments:
|
||||
When GitHub Copilot is the best available provider, oh-my-opencode uses these model assignments:
|
||||
|
||||
| Agent | Model |
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||
| **Explore** | `grok code` (default) |
|
||||
| **Librarian** | `glm 4.7 free` (default) |
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `zai-coding-plan/glm-4.7` (if Z.ai available) or fallback |
|
||||
|
||||
GitHub Copilot acts as a proxy provider, routing requests to underlying models based on your subscription.
|
||||
|
||||
#### Z.ai Coding Plan
|
||||
|
||||
Z.ai Coding Plan provides access to GLM-4.7 models. When enabled, the **Librarian agent always uses `zai-coding-plan/glm-4.7`** regardless of other available providers.
|
||||
|
||||
If Z.ai is the only provider available, all agents will use GLM models:
|
||||
|
||||
| Agent | Model |
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `zai-coding-plan/glm-4.7` |
|
||||
| **Oracle** | `zai-coding-plan/glm-4.7` |
|
||||
| **Explore** | `zai-coding-plan/glm-4.7-flash` |
|
||||
| **Librarian** | `zai-coding-plan/glm-4.7` |
|
||||
|
||||
#### OpenCode Zen
|
||||
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/gpt-5-nano`, and `opencode/glm-4.7-free`.
|
||||
|
||||
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
|
||||
|
||||
| Agent | Model |
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `opencode/claude-opus-4-5` |
|
||||
| **Oracle** | `opencode/gpt-5.2` |
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `opencode/glm-4.7-free` |
|
||||
|
||||
##### Setup
|
||||
|
||||
Run the installer and select "Yes" for GitHub Copilot:
|
||||
@@ -190,7 +237,7 @@ bunx oh-my-opencode install
|
||||
Or use non-interactive mode:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes
|
||||
bunx oh-my-opencode install --no-tui --claude=no --openai=no --gemini=no --copilot=yes
|
||||
```
|
||||
|
||||
Then authenticate with GitHub:
|
||||
@@ -231,7 +278,7 @@ After everything is set up and working, ask the user:
|
||||
|
||||
If the user agrees, run:
|
||||
```bash
|
||||
gh repo star code-yeongyu/oh-my-opencode
|
||||
gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true
|
||||
```
|
||||
|
||||
**Important**: Only run the command if the user explicitly says yes. Never run it automatically without consent.
|
||||
|
||||
@@ -54,7 +54,7 @@ For complex or critical tasks, press **Tab** to switch to Prometheus (Planner) m
|
||||
|
||||
2. **Plan generation** - Based on the interview, Prometheus generates a detailed work plan with tasks, acceptance criteria, and guardrails. Optionally reviewed by Momus (plan reviewer) for high-accuracy validation.
|
||||
|
||||
3. **Run `/start-work`** - The Orchestrator-Sisyphus takes over:
|
||||
3. **Run `/start-work`** - The Atlas takes over:
|
||||
- Distributes tasks to specialized sub-agents
|
||||
- Verifies each task completion independently
|
||||
- Accumulates learnings across tasks
|
||||
@@ -72,7 +72,7 @@ For complex or critical tasks, press **Tab** to switch to Prometheus (Planner) m
|
||||
|
||||
### Always Use Prometheus + Orchestrator Together
|
||||
|
||||
**Do NOT use `orchestrator-sisyphus` without `/start-work`.**
|
||||
**Do NOT use `atlas` without `/start-work`.**
|
||||
|
||||
The orchestrator is designed to execute work plans created by Prometheus. Using it directly without a plan leads to unpredictable behavior.
|
||||
|
||||
@@ -84,7 +84,78 @@ The orchestrator is designed to execute work plans created by Prometheus. Using
|
||||
4. Run /start-work → Orchestrator executes
|
||||
```
|
||||
|
||||
**Prometheus and Orchestrator-Sisyphus are a pair. Always use them together.**
|
||||
**Prometheus and Atlas are a pair. Always use them together.**
|
||||
|
||||
---
|
||||
|
||||
## Model Configuration
|
||||
|
||||
Oh My OpenCode automatically configures models based on your available providers. You don't need to manually specify every model.
|
||||
|
||||
### How Models Are Determined
|
||||
|
||||
**1. At Installation Time (Interactive Installer)**
|
||||
|
||||
When you run `bunx oh-my-opencode install`, the installer asks which providers you have:
|
||||
- Claude Pro/Max subscription?
|
||||
- OpenAI/ChatGPT Plus?
|
||||
- Google Gemini?
|
||||
- GitHub Copilot?
|
||||
- OpenCode Zen?
|
||||
- Z.ai Coding Plan?
|
||||
|
||||
Based on your answers, it generates `~/.config/opencode/oh-my-opencode.json` with optimal model assignments for each agent and category.
|
||||
|
||||
**2. At Runtime (Fallback Chain)**
|
||||
|
||||
Each agent has a **provider priority chain**. The system tries providers in order until it finds an available model:
|
||||
|
||||
```
|
||||
Example: multimodal-looker
|
||||
google → openai → zai-coding-plan → anthropic → opencode
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
gemini gpt-5.2 glm-4.6v haiku gpt-5-nano
|
||||
```
|
||||
|
||||
If you have Gemini, it uses `google/gemini-3-flash`. No Gemini but have Claude? Uses `anthropic/claude-haiku-4-5`. And so on.
|
||||
|
||||
### Example Configuration
|
||||
|
||||
Here's a real-world config for a user with **Claude, OpenAI, Gemini, and Z.ai** all available:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
// Override specific agents only - rest use fallback chain
|
||||
"atlas": { "model": "anthropic/claude-sonnet-4-5", "variant": "max" },
|
||||
"librarian": { "model": "zai-coding-plan/glm-4.7" },
|
||||
"explore": { "model": "opencode/gpt-5-nano" },
|
||||
"multimodal-looker": { "model": "zai-coding-plan/glm-4.6v" }
|
||||
},
|
||||
"categories": {
|
||||
// Override categories for cost optimization
|
||||
"quick": { "model": "opencode/gpt-5-nano" },
|
||||
"unspecified-low": { "model": "zai-coding-plan/glm-4.7" }
|
||||
},
|
||||
"experimental": {
|
||||
"aggressive_truncation": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- You only need to override what you want to change
|
||||
- Unspecified agents/categories use the automatic fallback chain
|
||||
- Mix providers freely (Claude for main work, Z.ai for cheap tasks, etc.)
|
||||
|
||||
### Finding Available Models
|
||||
|
||||
Run `opencode models` to see all available models in your environment. Model names follow the format `provider/model-name`.
|
||||
|
||||
### Learn More
|
||||
|
||||
For detailed configuration options including per-agent settings, category customization, and more, see the [Configuration Guide](../configurations.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Understanding the Orchestration System
|
||||
|
||||
Oh My OpenCode's orchestration system transforms a simple AI agent into a coordinated development team. This document explains how the Prometheus → Orchestrator → Junior workflow creates high-quality, reliable code output.
|
||||
Oh My OpenCode's orchestration system transforms a simple AI agent into a coordinated development team. This document explains how the Prometheus → Atlas → Junior workflow creates high-quality, reliable code output.
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +29,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph Execution["Execution Layer (Orchestrator)"]
|
||||
Orchestrator["⚡ Orchestrator-Sisyphus<br/>(Conductor)<br/>Claude Opus 4.5"]
|
||||
Orchestrator["⚡ Atlas<br/>(Conductor)<br/>Claude Opus 4.5"]
|
||||
end
|
||||
|
||||
subgraph Workers["Worker Layer (Specialized Agents)"]
|
||||
@@ -152,7 +152,7 @@ If REJECTED, Prometheus fixes issues and resubmits. **No maximum retry limit.**
|
||||
|
||||
---
|
||||
|
||||
## Layer 2: Execution (Orchestrator-Sisyphus)
|
||||
## Layer 2: Execution (Atlas)
|
||||
|
||||
### The Conductor Mindset
|
||||
|
||||
@@ -160,7 +160,7 @@ The Orchestrator is like an orchestra conductor: **it doesn't play instruments,
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Orchestrator["Orchestrator-Sisyphus"]
|
||||
subgraph Orchestrator["Atlas"]
|
||||
Read["1. Read Plan"]
|
||||
Analyze["2. Analyze Tasks"]
|
||||
Wisdom["3. Accumulate Wisdom"]
|
||||
@@ -291,15 +291,15 @@ delegate_task(category="quick", prompt="...") // "Just get it done fast
|
||||
|
||||
### Built-in Categories
|
||||
|
||||
| Category | Model | Temp | When to Use |
|
||||
|----------|-------|------|-------------|
|
||||
| `visual-engineering` | Gemini 3 Pro | 0.7 | Frontend, UI/UX, design, animations |
|
||||
| `ultrabrain` | GPT-5.2 | 0.1 | Complex architecture, business logic |
|
||||
| `artistry` | Gemini 3 Pro | 0.9 | Creative tasks, novel ideas |
|
||||
| `quick` | Claude Haiku 4.5 | 0.3 | Small tasks, budget-friendly |
|
||||
| `most-capable` | Claude Opus 4.5 | 0.1 | Maximum reasoning power |
|
||||
| `writing` | Gemini 3 Flash | 0.5 | Documentation, prose |
|
||||
| `general` | Claude Sonnet 4.5 | 0.3 | Default, general purpose |
|
||||
| Category | Model | When to Use |
|
||||
|----------|-------|-------------|
|
||||
| `visual-engineering` | Gemini 3 Pro | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | GPT-5.2 Codex (xhigh) | Deep logical reasoning, complex architecture decisions |
|
||||
| `artistry` | Gemini 3 Pro (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | Claude Haiku 4.5 | Trivial tasks - single file changes, typo fixes |
|
||||
| `unspecified-low` | Claude Sonnet 4.5 | Tasks that don't fit other categories, low effort |
|
||||
| `unspecified-high` | Claude Opus 4.5 (max) | Tasks that don't fit other categories, high effort |
|
||||
| `writing` | Gemini 3 Flash | Documentation, prose, technical writing |
|
||||
|
||||
### Custom Categories
|
||||
|
||||
@@ -326,13 +326,13 @@ Skills prepend specialized instructions to subagent prompts:
|
||||
// Category + Skill combination
|
||||
delegate_task(
|
||||
category="visual-engineering",
|
||||
skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
load_skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
prompt="..."
|
||||
)
|
||||
|
||||
delegate_task(
|
||||
category="general",
|
||||
skills=["playwright"], // Adds browser automation expertise
|
||||
load_skills=["playwright"], // Adds browser automation expertise
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -341,8 +341,8 @@ delegate_task(
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| Hardcoded: `frontend-ui-ux-engineer` (Gemini 3 Pro) | `category="visual-engineering" + skills=["frontend-ui-ux"]` |
|
||||
| One-size-fits-all | `category="visual-engineering" + skills=["unity-master"]` |
|
||||
| Hardcoded: `frontend-ui-ux-engineer` (Gemini 3 Pro) | `category="visual-engineering" + load_skills=["frontend-ui-ux"]` |
|
||||
| One-size-fits-all | `category="visual-engineering" + load_skills=["unity-master"]` |
|
||||
| Model bias | Category-based: model abstraction eliminates bias |
|
||||
|
||||
---
|
||||
@@ -352,7 +352,7 @@ delegate_task(
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Orchestrator as Orchestrator-Sisyphus
|
||||
participant Orchestrator as Atlas
|
||||
participant Junior as Sisyphus-Junior
|
||||
participant Notepad as .sisyphus/notepads/
|
||||
|
||||
@@ -365,7 +365,7 @@ sequenceDiagram
|
||||
|
||||
Note over Orchestrator: Prompt Structure:<br/>1. TASK (exact checkbox)<br/>2. EXPECTED OUTCOME<br/>3. REQUIRED SKILLS<br/>4. REQUIRED TOOLS<br/>5. MUST DO<br/>6. MUST NOT DO<br/>7. CONTEXT + Wisdom
|
||||
|
||||
Orchestrator->>Junior: delegate_task(category, skills, prompt)
|
||||
Orchestrator->>Junior: delegate_task(category, load_skills, prompt)
|
||||
|
||||
Junior->>Junior: Create todos, execute
|
||||
Junior->>Junior: Verify (lsp_diagnostics, tests)
|
||||
@@ -392,7 +392,7 @@ sequenceDiagram
|
||||
### 1. Separation of Concerns
|
||||
|
||||
- **Planning** (Prometheus): High reasoning, interview, strategic thinking
|
||||
- **Orchestration** (Sisyphus): Coordination, verification, wisdom accumulation
|
||||
- **Orchestration** (Atlas): Coordination, verification, wisdom accumulation
|
||||
- **Execution** (Junior): Focused implementation, no distractions
|
||||
|
||||
### 2. Explicit Over Implicit
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
|------------|----------|-------------|
|
||||
| **Simple** | Just prompt | Simple tasks, quick fixes, single-file changes |
|
||||
| **Complex + Lazy** | Just type `ulw` or `ultrawork` | Complex tasks where explaining context is tedious. Agent figures it out. |
|
||||
| **Complex + Precise** | `@plan` → `/start-work` | Precise, multi-step work requiring true orchestration. Prometheus plans, Sisyphus executes. |
|
||||
| **Complex + Precise** | `@plan` → `/start-work` | Precise, multi-step work requiring true orchestration. Prometheus plans, Atlas executes. |
|
||||
|
||||
**Decision Flow:**
|
||||
|
||||
```
|
||||
Is it a quick fix or simple task?
|
||||
└─ YES → Just prompt normally
|
||||
@@ -30,7 +31,7 @@ Traditional AI agents often mix planning and execution, leading to context pollu
|
||||
Oh-My-OpenCode solves this by clearly separating two roles:
|
||||
|
||||
1. **Prometheus (Planner)**: A pure strategist who never writes code. Establishes perfect plans through interviews and analysis.
|
||||
2. **Sisyphus (Executor)**: An orchestrator who executes plans. Delegates work to specialized agents and never stops until completion.
|
||||
2. **Atlas (Executor)**: An orchestrator who executes plans. Delegates work to specialized agents and never stops until completion.
|
||||
|
||||
---
|
||||
|
||||
@@ -52,10 +53,10 @@ flowchart TD
|
||||
StartWork --> BoulderState[boulder.json]
|
||||
|
||||
subgraph Execution Phase
|
||||
BoulderState --> Sisyphus[Sisyphus<br>Orchestrator]
|
||||
Sisyphus --> Oracle[Oracle]
|
||||
Sisyphus --> Frontend[Frontend<br>Engineer]
|
||||
Sisyphus --> Explore[Explore]
|
||||
BoulderState --> Atlas[Atlas<br>Orchestrator]
|
||||
Atlas --> Oracle[Oracle]
|
||||
Atlas --> Frontend[Frontend<br>Engineer]
|
||||
Atlas --> Explore[Explore]
|
||||
end
|
||||
```
|
||||
|
||||
@@ -64,22 +65,26 @@ flowchart TD
|
||||
## 3. Key Components
|
||||
|
||||
### 🔮 Prometheus (The Planner)
|
||||
|
||||
- **Model**: `anthropic/claude-opus-4-5`
|
||||
- **Role**: Strategic planning, requirements interviews, work plan creation
|
||||
- **Constraint**: **READ-ONLY**. Can only create/modify markdown files within `.sisyphus/` directory.
|
||||
- **Characteristic**: Never writes code directly, focuses solely on "how to do it".
|
||||
|
||||
### 🦉 Metis (The Consultant)
|
||||
### 🦉 Metis (The Plan Consultant)
|
||||
|
||||
- **Role**: Pre-analysis and gap detection
|
||||
- **Function**: Identifies hidden user intent, prevents AI over-engineering, eliminates ambiguity.
|
||||
- **Workflow**: Metis consultation is mandatory before plan creation.
|
||||
|
||||
### ⚖️ Momus (The Reviewer)
|
||||
### ⚖️ Momus (The Plan Reviewer)
|
||||
|
||||
- **Role**: High-precision plan validation (High Accuracy Mode)
|
||||
- **Function**: Rejects and demands revisions until the plan is perfect.
|
||||
- **Trigger**: Activated when user requests "high accuracy".
|
||||
|
||||
### 🪨 Sisyphus (The Orchestrator)
|
||||
### ⚡ Atlas (The Plan Executor)
|
||||
|
||||
- **Model**: `anthropic/claude-opus-4-5` (Extended Thinking 32k)
|
||||
- **Role**: Execution and delegation
|
||||
- **Characteristic**: Doesn't do everything directly, actively delegates to specialized agents (Frontend, Librarian, etc.).
|
||||
@@ -89,6 +94,7 @@ flowchart TD
|
||||
## 4. Workflow
|
||||
|
||||
### Phase 1: Interview and Planning (Interview Mode)
|
||||
|
||||
Prometheus starts in **interview mode** by default. Instead of immediately creating a plan, it collects sufficient context.
|
||||
|
||||
1. **Intent Identification**: Classifies whether the user's request is Refactoring or New Feature.
|
||||
@@ -96,6 +102,7 @@ Prometheus starts in **interview mode** by default. Instead of immediately creat
|
||||
3. **Draft Creation**: Continuously records discussion content in `.sisyphus/drafts/`.
|
||||
|
||||
### Phase 2: Plan Generation
|
||||
|
||||
When the user requests "Make it a plan", plan generation begins.
|
||||
|
||||
1. **Metis Consultation**: Confirms any missed requirements or risk factors.
|
||||
@@ -103,10 +110,11 @@ When the user requests "Make it a plan", plan generation begins.
|
||||
3. **Handoff**: Once plan creation is complete, guides user to use `/start-work` command.
|
||||
|
||||
### Phase 3: Execution
|
||||
|
||||
When the user enters `/start-work`, the execution phase begins.
|
||||
|
||||
1. **State Management**: Creates `boulder.json` file to track current plan and session ID.
|
||||
2. **Task Execution**: Sisyphus reads the plan and processes TODOs one by one.
|
||||
2. **Task Execution**: Atlas reads the plan and processes TODOs one by one.
|
||||
3. **Delegation**: UI work is delegated to Frontend agent, complex logic to Oracle.
|
||||
4. **Continuity**: Even if the session is interrupted, work continues in the next session through `boulder.json`.
|
||||
|
||||
@@ -115,11 +123,15 @@ When the user enters `/start-work`, the execution phase begins.
|
||||
## 5. Commands and Usage
|
||||
|
||||
### `@plan [request]`
|
||||
|
||||
Invokes Prometheus to start a planning session.
|
||||
|
||||
- Example: `@plan "I want to refactor the authentication system to NextAuth"`
|
||||
|
||||
### `/start-work`
|
||||
|
||||
Executes the generated plan.
|
||||
|
||||
- Function: Finds plan in `.sisyphus/plans/` and enters execution mode.
|
||||
- If there's interrupted work, automatically resumes from where it left off.
|
||||
|
||||
@@ -132,7 +144,7 @@ You can control related features in `oh-my-opencode.json`.
|
||||
```jsonc
|
||||
{
|
||||
"sisyphus_agent": {
|
||||
"disabled": false, // Enable Sisyphus orchestration (default: false)
|
||||
"disabled": false, // Enable Atlas orchestration (default: false)
|
||||
"planner_enabled": true, // Enable Prometheus (default: true)
|
||||
"replace_plan": true // Replace default plan agent with Prometheus (default: true)
|
||||
},
|
||||
|
||||
126
docs/troubleshooting/ollama-streaming-issue.md
Normal file
126
docs/troubleshooting/ollama-streaming-issue.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Ollama Streaming Issue - JSON Parse Error
|
||||
|
||||
## Problem
|
||||
|
||||
When using Ollama as a provider with oh-my-opencode agents, you may encounter:
|
||||
|
||||
```
|
||||
JSON Parse error: Unexpected EOF
|
||||
```
|
||||
|
||||
This occurs when agents attempt tool calls (e.g., `explore` agent using `mcp_grep_search`).
|
||||
|
||||
## Root Cause
|
||||
|
||||
Ollama returns **NDJSON** (newline-delimited JSON) when `stream: true` is used in API requests:
|
||||
|
||||
```json
|
||||
{"message":{"tool_calls":[{"function":{"name":"read","arguments":{"filePath":"README.md"}}}]}, "done":false}
|
||||
{"message":{"content":""}, "done":true}
|
||||
```
|
||||
|
||||
Claude Code SDK expects a single JSON object, not multiple NDJSON lines, causing the parse error.
|
||||
|
||||
### Why This Happens
|
||||
|
||||
- **Ollama API**: Returns streaming responses as NDJSON by design
|
||||
- **Claude Code SDK**: Doesn't properly handle NDJSON responses for tool calls
|
||||
- **oh-my-opencode**: Passes through the SDK's behavior (can't fix at this layer)
|
||||
|
||||
## Solutions
|
||||
|
||||
### Option 1: Disable Streaming (Recommended - Immediate Fix)
|
||||
|
||||
Configure your Ollama provider to use `stream: false`:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "ollama",
|
||||
"model": "qwen3-coder",
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Works immediately
|
||||
- No code changes needed
|
||||
- Simple configuration
|
||||
|
||||
**Cons:**
|
||||
- Slightly slower response time (no streaming)
|
||||
- Less interactive feedback
|
||||
|
||||
### Option 2: Use Non-Tool Agents Only
|
||||
|
||||
If you need streaming, avoid agents that use tools:
|
||||
|
||||
- ✅ **Safe**: Simple text generation, non-tool tasks
|
||||
- ❌ **Problematic**: Any agent with tool calls (explore, librarian, etc.)
|
||||
|
||||
### Option 3: Wait for SDK Fix (Long-term)
|
||||
|
||||
The proper fix requires Claude Code SDK to:
|
||||
|
||||
1. Detect NDJSON responses
|
||||
2. Parse each line separately
|
||||
3. Merge `tool_calls` from multiple lines
|
||||
4. Return a single merged response
|
||||
|
||||
**Tracking**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
|
||||
|
||||
## Workaround Implementation
|
||||
|
||||
Until the SDK is fixed, here's how to implement NDJSON parsing (for SDK maintainers):
|
||||
|
||||
```typescript
|
||||
async function parseOllamaStreamResponse(response: string): Promise<object> {
|
||||
const lines = response.split('\n').filter(line => line.trim());
|
||||
const mergedMessage = { tool_calls: [] };
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
if (json.message?.tool_calls) {
|
||||
mergedMessage.tool_calls.push(...json.message.tool_calls);
|
||||
}
|
||||
if (json.message?.content) {
|
||||
mergedMessage.content = json.message.content;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip malformed lines
|
||||
console.warn('Skipping malformed NDJSON line:', line);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedMessage;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
To verify the fix works:
|
||||
|
||||
```bash
|
||||
# Test with curl (should work with stream: false)
|
||||
curl -s http://localhost:11434/api/chat \
|
||||
-d '{
|
||||
"model": "qwen3-coder",
|
||||
"messages": [{"role": "user", "content": "Read file README.md"}],
|
||||
"stream": false,
|
||||
"tools": [{"type": "function", "function": {"name": "read", "description": "Read a file", "parameters": {"type": "object", "properties": {"filePath": {"type": "string"}}, "required": ["filePath"]}}}]
|
||||
}'
|
||||
```
|
||||
|
||||
## Related Issues
|
||||
|
||||
- **oh-my-opencode**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
|
||||
- **Ollama API Docs**: https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter this issue:
|
||||
|
||||
1. Check your Ollama provider configuration
|
||||
2. Set `stream: false` as a workaround
|
||||
3. Report any additional errors to the issue tracker
|
||||
4. Provide your configuration (without secrets) for debugging
|
||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -64,6 +64,7 @@
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"vscode-jsonrpc": "^8.2.0",
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -73,13 +74,13 @@
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.11",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.11"
|
||||
"oh-my-opencode-darwin-arm64": "3.1.10",
|
||||
"oh-my-opencode-darwin-x64": "3.1.10",
|
||||
"oh-my-opencode-linux-arm64": "3.1.10",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.10",
|
||||
"oh-my-opencode-linux-x64": "3.1.10",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.10",
|
||||
"oh-my-opencode-windows-x64": "3.1.10"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
22
packages/darwin-x64-baseline/package.json
Normal file
22
packages/darwin-x64-baseline/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64-baseline, no AVX2)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64-musl",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
25
packages/linux-x64-baseline/package.json
Normal file
25
packages/linux-x64-baseline/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-baseline, no AVX2)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
25
packages/linux-x64-musl-baseline/package.json
Normal file
25
packages/linux-x64-musl-baseline/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl-baseline, no AVX2)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
22
packages/windows-x64-baseline/package.json
Normal file
22
packages/windows-x64-baseline/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64-baseline, no AVX2)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode.exe"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64",
|
||||
"version": "3.0.0-beta.11",
|
||||
"version": "3.1.10",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
79
script/build-binaries.test.ts
Normal file
79
script/build-binaries.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// script/build-binaries.test.ts
|
||||
// Tests for platform binary build configuration
|
||||
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
// Import PLATFORMS from build-binaries.ts
|
||||
// We need to export it first, but for now we'll test the expected structure
|
||||
const EXPECTED_BASELINE_TARGETS = [
|
||||
"bun-linux-x64-baseline",
|
||||
"bun-linux-x64-musl-baseline",
|
||||
"bun-darwin-x64-baseline",
|
||||
"bun-windows-x64-baseline",
|
||||
];
|
||||
|
||||
describe("build-binaries", () => {
|
||||
describe("PLATFORMS array", () => {
|
||||
it("includes baseline variants for non-AVX2 CPU support", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { target: string }[] }).PLATFORMS;
|
||||
const targets = platforms.map((p) => p.target);
|
||||
|
||||
// when
|
||||
const hasAllBaselineTargets = EXPECTED_BASELINE_TARGETS.every((baseline) =>
|
||||
targets.includes(baseline)
|
||||
);
|
||||
|
||||
// then
|
||||
expect(hasAllBaselineTargets).toBe(true);
|
||||
for (const baseline of EXPECTED_BASELINE_TARGETS) {
|
||||
expect(targets).toContain(baseline);
|
||||
}
|
||||
});
|
||||
|
||||
it("has correct directory names for baseline platforms", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { dir: string; target: string }[] }).PLATFORMS;
|
||||
|
||||
// when
|
||||
const baselinePlatforms = platforms.filter((p) => p.target.includes("baseline"));
|
||||
|
||||
// then
|
||||
expect(baselinePlatforms.length).toBe(4);
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("linux-x64-baseline");
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("linux-x64-musl-baseline");
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("darwin-x64-baseline");
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("windows-x64-baseline");
|
||||
});
|
||||
|
||||
it("has correct binary names for baseline platforms", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { dir: string; target: string; binary: string }[] }).PLATFORMS;
|
||||
|
||||
// when
|
||||
const windowsBaseline = platforms.find((p) => p.target === "bun-windows-x64-baseline");
|
||||
const linuxBaseline = platforms.find((p) => p.target === "bun-linux-x64-baseline");
|
||||
|
||||
// then
|
||||
expect(windowsBaseline?.binary).toBe("oh-my-opencode.exe");
|
||||
expect(linuxBaseline?.binary).toBe("oh-my-opencode");
|
||||
});
|
||||
|
||||
it("has descriptions mentioning no AVX2 for baseline platforms", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { target: string; description: string }[] }).PLATFORMS;
|
||||
|
||||
// when
|
||||
const baselinePlatforms = platforms.filter((p) => p.target.includes("baseline"));
|
||||
|
||||
// then
|
||||
for (const platform of baselinePlatforms) {
|
||||
expect(platform.description).toContain("no AVX2");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,14 +13,18 @@ interface PlatformTarget {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PLATFORMS: PlatformTarget[] = [
|
||||
export const PLATFORMS: PlatformTarget[] = [
|
||||
{ dir: "darwin-arm64", target: "bun-darwin-arm64", binary: "oh-my-opencode", description: "macOS ARM64" },
|
||||
{ dir: "darwin-x64", target: "bun-darwin-x64", binary: "oh-my-opencode", description: "macOS x64" },
|
||||
{ dir: "darwin-x64-baseline", target: "bun-darwin-x64-baseline", binary: "oh-my-opencode", description: "macOS x64 (no AVX2)" },
|
||||
{ dir: "linux-x64", target: "bun-linux-x64", binary: "oh-my-opencode", description: "Linux x64 (glibc)" },
|
||||
{ dir: "linux-x64-baseline", target: "bun-linux-x64-baseline", binary: "oh-my-opencode", description: "Linux x64 (glibc, no AVX2)" },
|
||||
{ dir: "linux-arm64", target: "bun-linux-arm64", binary: "oh-my-opencode", description: "Linux ARM64 (glibc)" },
|
||||
{ dir: "linux-x64-musl", target: "bun-linux-x64-musl", binary: "oh-my-opencode", description: "Linux x64 (musl)" },
|
||||
{ dir: "linux-x64-musl-baseline", target: "bun-linux-x64-musl-baseline", binary: "oh-my-opencode", description: "Linux x64 (musl, no AVX2)" },
|
||||
{ dir: "linux-arm64-musl", target: "bun-linux-arm64-musl", binary: "oh-my-opencode", description: "Linux ARM64 (musl)" },
|
||||
{ dir: "windows-x64", target: "bun-windows-x64", binary: "oh-my-opencode.exe", description: "Windows x64" },
|
||||
{ dir: "windows-x64-baseline", target: "bun-windows-x64-baseline", binary: "oh-my-opencode.exe", description: "Windows x64 (no AVX2)" },
|
||||
];
|
||||
|
||||
const ENTRY_POINT = "src/cli/index.ts";
|
||||
|
||||
@@ -7,6 +7,8 @@ import { join } from "node:path"
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const bump = process.env.BUMP as "major" | "minor" | "patch" | undefined
|
||||
const versionOverride = process.env.VERSION
|
||||
const republishMode = process.env.REPUBLISH === "true"
|
||||
const prepareOnly = process.argv.includes("--prepare-only")
|
||||
|
||||
const PLATFORM_PACKAGES = [
|
||||
"darwin-arm64",
|
||||
@@ -83,11 +85,36 @@ async function updateAllPackageVersions(newVersion: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateChangelog(previous: string): Promise<string[]> {
|
||||
async function findPreviousTag(currentVersion: string): Promise<string | null> {
|
||||
// For beta versions, find the previous beta tag (e.g., 3.0.0-beta.11 for 3.0.0-beta.12)
|
||||
const betaMatch = currentVersion.match(/^(\d+\.\d+\.\d+)-beta\.(\d+)$/)
|
||||
if (betaMatch) {
|
||||
const [, base, num] = betaMatch
|
||||
const prevNum = parseInt(num) - 1
|
||||
if (prevNum >= 1) {
|
||||
const prevTag = `${base}-beta.${prevNum}`
|
||||
const exists = await $`git rev-parse v${prevTag}`.nothrow()
|
||||
if (exists.exitCode === 0) return prevTag
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function generateChangelog(previous: string, currentVersion?: string): Promise<string[]> {
|
||||
const notes: string[] = []
|
||||
|
||||
// Try to find the most accurate previous tag for comparison
|
||||
let compareTag = previous
|
||||
if (currentVersion) {
|
||||
const prevBetaTag = await findPreviousTag(currentVersion)
|
||||
if (prevBetaTag) {
|
||||
compareTag = prevBetaTag
|
||||
console.log(`Using previous beta tag for comparison: v${compareTag}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const log = await $`git log v${previous}..HEAD --oneline --format="%h %s"`.text()
|
||||
const log = await $`git log v${compareTag}..HEAD --oneline --format="%h %s"`.text()
|
||||
const commits = log
|
||||
.split("\n")
|
||||
.filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:|release:)/i))
|
||||
@@ -161,29 +188,60 @@ interface PublishResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
async function publishPackage(cwd: string, distTag: string | null, useProvenance = true): Promise<PublishResult> {
|
||||
async function checkPackageVersionExists(pkgName: string, version: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${pkgName}/${version}`)
|
||||
return res.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function publishPackage(cwd: string, distTag: string | null, useProvenance = true, pkgName?: string, version?: string): Promise<PublishResult> {
|
||||
// In republish mode, skip if package already exists on npm
|
||||
if (republishMode && pkgName && version) {
|
||||
const exists = await checkPackageVersionExists(pkgName, version)
|
||||
if (exists) {
|
||||
return { success: true, alreadyPublished: true }
|
||||
}
|
||||
console.log(` ${pkgName}@${version} not found on npm, publishing...`)
|
||||
}
|
||||
|
||||
const tagArgs = distTag ? ["--tag", distTag] : []
|
||||
const provenanceArgs = process.env.CI && useProvenance ? ["--provenance"] : []
|
||||
const env = useProvenance ? {} : { NPM_CONFIG_PROVENANCE: "false" }
|
||||
|
||||
try {
|
||||
await $`npm publish --access public --ignore-scripts ${provenanceArgs} ${tagArgs}`.cwd(cwd)
|
||||
await $`npm publish --access public --ignore-scripts ${provenanceArgs} ${tagArgs}`.cwd(cwd).env({ ...process.env, ...env })
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
const stderr = error?.stderr?.toString() || error?.message || ""
|
||||
|
||||
// E409/E403 = version already exists (idempotent success)
|
||||
// E404 + "Access token expired" = OIDC token expired while publishing already-published package
|
||||
// Only treat as "already published" if we're certain the package exists
|
||||
// E409/EPUBLISHCONFLICT = definitive "version already exists"
|
||||
if (
|
||||
stderr.includes("EPUBLISHCONFLICT") ||
|
||||
stderr.includes("E409") ||
|
||||
stderr.includes("E403") ||
|
||||
stderr.includes("cannot publish over") ||
|
||||
stderr.includes("already exists") ||
|
||||
(stderr.includes("E404") && stderr.includes("Access token expired"))
|
||||
stderr.includes("You cannot publish over the previously published versions")
|
||||
) {
|
||||
return { success: true, alreadyPublished: true }
|
||||
}
|
||||
|
||||
// E403 can mean "already exists" OR "no permission" - verify by checking npm registry
|
||||
if (stderr.includes("E403")) {
|
||||
if (pkgName && version) {
|
||||
const exists = await checkPackageVersionExists(pkgName, version)
|
||||
if (exists) {
|
||||
return { success: true, alreadyPublished: true }
|
||||
}
|
||||
}
|
||||
// If we can't verify or it doesn't exist, it's a real error
|
||||
return { success: false, error: stderr }
|
||||
}
|
||||
|
||||
// 404 errors are NEVER "already published" - they indicate the package doesn't exist
|
||||
// or OIDC token issues. Always treat as failure.
|
||||
return { success: false, error: stderr }
|
||||
}
|
||||
}
|
||||
@@ -215,7 +273,7 @@ async function publishAllPackages(version: string): Promise<void> {
|
||||
const pkgName = `oh-my-opencode-${platform}`
|
||||
|
||||
console.log(` Starting ${pkgName}...`)
|
||||
const result = await publishPackage(pkgDir, distTag, false)
|
||||
const result = await publishPackage(pkgDir, distTag, false, pkgName, version)
|
||||
|
||||
return { platform, pkgName, result }
|
||||
})
|
||||
@@ -243,7 +301,7 @@ async function publishAllPackages(version: string): Promise<void> {
|
||||
|
||||
// Publish main package last
|
||||
console.log(`\n📦 Publishing main package...`)
|
||||
const mainResult = await publishPackage(process.cwd(), distTag)
|
||||
const mainResult = await publishPackage(process.cwd(), distTag, true, PACKAGE_NAME, version)
|
||||
|
||||
if (mainResult.success) {
|
||||
if (mainResult.alreadyPublished) {
|
||||
@@ -298,7 +356,16 @@ async function gitTagAndRelease(newVersion: string, notes: string[]): Promise<vo
|
||||
console.log(`Tag v${newVersion} already exists`)
|
||||
}
|
||||
|
||||
await $`git push origin HEAD --tags`
|
||||
// Push tags first (critical for release), then try branch push (non-critical)
|
||||
console.log("Pushing tags...")
|
||||
await $`git push origin --tags`
|
||||
|
||||
console.log("Pushing branch...")
|
||||
const branchPush = await $`git push origin HEAD`.nothrow()
|
||||
if (branchPush.exitCode !== 0) {
|
||||
console.log(`⚠️ Branch push failed (remote may have new commits). Tag was pushed successfully.`)
|
||||
console.log(` To sync manually: git pull --rebase && git push`)
|
||||
}
|
||||
|
||||
console.log("\nCreating GitHub release...")
|
||||
const releaseNotes = notes.length > 0 ? notes.join("\n") : "No notable changes"
|
||||
@@ -324,13 +391,25 @@ async function main() {
|
||||
const newVersion = versionOverride || (bump ? bumpVersion(previous, bump) : bumpVersion(previous, "patch"))
|
||||
console.log(`New version: ${newVersion}\n`)
|
||||
|
||||
if (prepareOnly) {
|
||||
console.log("=== Prepare-only mode: updating versions ===")
|
||||
await updateAllPackageVersions(newVersion)
|
||||
console.log(`\n=== Versions updated to ${newVersion} ===`)
|
||||
return
|
||||
}
|
||||
|
||||
if (await checkVersionExists(newVersion)) {
|
||||
console.log(`Version ${newVersion} already exists on npm. Skipping publish.`)
|
||||
process.exit(0)
|
||||
if (republishMode) {
|
||||
console.log(`Version ${newVersion} exists on npm. REPUBLISH mode: checking for missing platform packages...`)
|
||||
} else {
|
||||
console.log(`Version ${newVersion} already exists on npm. Skipping publish.`)
|
||||
console.log(`(Use REPUBLISH=true to publish missing platform packages)`)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
await updateAllPackageVersions(newVersion)
|
||||
const changelog = await generateChangelog(previous)
|
||||
const changelog = await generateChangelog(previous, newVersion)
|
||||
const contributors = await getContributors(previous)
|
||||
const notes = [...changelog, ...contributors]
|
||||
|
||||
|
||||
@@ -615,6 +615,422 @@
|
||||
"created_at": "2026-01-19T05:19:40Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 908
|
||||
},
|
||||
{
|
||||
"name": "carlory",
|
||||
"id": 28390961,
|
||||
"comment_id": 3766665773,
|
||||
"created_at": "2026-01-19T06:37:03Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 910
|
||||
},
|
||||
{
|
||||
"name": "yebei199",
|
||||
"id": 129029530,
|
||||
"comment_id": 3767842807,
|
||||
"created_at": "2026-01-19T11:25:54Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 921
|
||||
},
|
||||
{
|
||||
"name": "TheSmuks",
|
||||
"id": 60717893,
|
||||
"comment_id": 3769687461,
|
||||
"created_at": "2026-01-19T18:43:50Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 929
|
||||
},
|
||||
{
|
||||
"name": "cooco119",
|
||||
"id": 34636736,
|
||||
"comment_id": 3770509385,
|
||||
"created_at": "2026-01-20T00:14:53Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 931
|
||||
},
|
||||
{
|
||||
"name": "LilMGenius",
|
||||
"id": 97161055,
|
||||
"comment_id": 3771191707,
|
||||
"created_at": "2026-01-20T06:06:25Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 938
|
||||
},
|
||||
{
|
||||
"name": "masteryi-0018",
|
||||
"id": 55500876,
|
||||
"comment_id": 3772446074,
|
||||
"created_at": "2026-01-20T11:39:31Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 944
|
||||
},
|
||||
{
|
||||
"name": "cs50victor",
|
||||
"id": 52110451,
|
||||
"comment_id": 3773838892,
|
||||
"created_at": "2026-01-20T16:32:33Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 950
|
||||
},
|
||||
{
|
||||
"name": "gigio1023",
|
||||
"id": 11407756,
|
||||
"comment_id": 3777343039,
|
||||
"created_at": "2026-01-21T10:29:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 965
|
||||
},
|
||||
{
|
||||
"name": "jonasherr",
|
||||
"id": 37550860,
|
||||
"comment_id": 3778772697,
|
||||
"created_at": "2026-01-21T15:21:10Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 966
|
||||
},
|
||||
{
|
||||
"name": "pipi-1997",
|
||||
"id": 46177323,
|
||||
"comment_id": 3779749303,
|
||||
"created_at": "2026-01-21T17:06:15Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 971
|
||||
},
|
||||
{
|
||||
"name": "kilhyeonjun",
|
||||
"id": 41348539,
|
||||
"comment_id": 3781992292,
|
||||
"created_at": "2026-01-22T01:29:22Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 974
|
||||
},
|
||||
{
|
||||
"name": "boojongmin",
|
||||
"id": 9567723,
|
||||
"comment_id": 3784182787,
|
||||
"created_at": "2026-01-22T12:39:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 989
|
||||
},
|
||||
{
|
||||
"name": "l3aro",
|
||||
"id": 25253808,
|
||||
"comment_id": 3786383804,
|
||||
"created_at": "2026-01-22T19:52:42Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 999
|
||||
},
|
||||
{
|
||||
"name": "Ssoon-m",
|
||||
"id": 89559826,
|
||||
"comment_id": 3788539617,
|
||||
"created_at": "2026-01-23T06:31:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1014
|
||||
},
|
||||
{
|
||||
"name": "veetase",
|
||||
"id": 2784250,
|
||||
"comment_id": 3789028002,
|
||||
"created_at": "2026-01-23T08:27:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 985
|
||||
},
|
||||
{
|
||||
"name": "RouHim",
|
||||
"id": 3582050,
|
||||
"comment_id": 3791988227,
|
||||
"created_at": "2026-01-23T19:32:01Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1031
|
||||
},
|
||||
{
|
||||
"name": "gongxh0901",
|
||||
"id": 15622561,
|
||||
"comment_id": 3793478620,
|
||||
"created_at": "2026-01-24T02:15:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1037
|
||||
},
|
||||
{
|
||||
"name": "gongxh0901",
|
||||
"id": 15622561,
|
||||
"comment_id": 3793521632,
|
||||
"created_at": "2026-01-24T02:23:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1037
|
||||
},
|
||||
{
|
||||
"name": "AndersHsueh",
|
||||
"id": 121805544,
|
||||
"comment_id": 3793787614,
|
||||
"created_at": "2026-01-24T04:41:46Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1042
|
||||
},
|
||||
{
|
||||
"name": "AamiRobin",
|
||||
"id": 22963668,
|
||||
"comment_id": 3794632200,
|
||||
"created_at": "2026-01-24T13:28:22Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1067
|
||||
},
|
||||
{
|
||||
"name": "ThanhNguyxn",
|
||||
"id": 74597207,
|
||||
"comment_id": 3795232176,
|
||||
"created_at": "2026-01-24T17:41:53Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1075
|
||||
},
|
||||
{
|
||||
"name": "sadnow",
|
||||
"id": 87896100,
|
||||
"comment_id": 3795495342,
|
||||
"created_at": "2026-01-24T20:49:29Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1080
|
||||
},
|
||||
{
|
||||
"name": "jsl9208",
|
||||
"id": 4048787,
|
||||
"comment_id": 3795582626,
|
||||
"created_at": "2026-01-24T21:41:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1082
|
||||
},
|
||||
{
|
||||
"name": "potb",
|
||||
"id": 10779093,
|
||||
"comment_id": 3795856573,
|
||||
"created_at": "2026-01-25T02:38:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1083
|
||||
},
|
||||
{
|
||||
"name": "kvokka",
|
||||
"id": 15954013,
|
||||
"comment_id": 3795884358,
|
||||
"created_at": "2026-01-25T03:13:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1084
|
||||
},
|
||||
{
|
||||
"name": "misyuari",
|
||||
"id": 12197761,
|
||||
"comment_id": 3798225767,
|
||||
"created_at": "2026-01-26T07:31:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1132
|
||||
},
|
||||
{
|
||||
"name": "boguan",
|
||||
"id": 3226538,
|
||||
"comment_id": 3798448537,
|
||||
"created_at": "2026-01-26T08:40:37Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1137
|
||||
},
|
||||
{
|
||||
"name": "boguan",
|
||||
"id": 3226538,
|
||||
"comment_id": 3798471978,
|
||||
"created_at": "2026-01-26T08:46:03Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1137
|
||||
},
|
||||
{
|
||||
"name": "Jeremy-Kr",
|
||||
"id": 110771206,
|
||||
"comment_id": 3799211732,
|
||||
"created_at": "2026-01-26T11:59:13Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1141
|
||||
},
|
||||
{
|
||||
"name": "orientpine",
|
||||
"id": 32758428,
|
||||
"comment_id": 3799897021,
|
||||
"created_at": "2026-01-26T14:30:33Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1145
|
||||
},
|
||||
{
|
||||
"name": "craftaholic",
|
||||
"id": 63741110,
|
||||
"comment_id": 3797014417,
|
||||
"created_at": "2026-01-25T17:52:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1110
|
||||
},
|
||||
{
|
||||
"name": "acamq",
|
||||
"id": 179265037,
|
||||
"comment_id": 3801038978,
|
||||
"created_at": "2026-01-26T18:20:17Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1151
|
||||
},
|
||||
{
|
||||
"name": "itsmylife44",
|
||||
"id": 34112129,
|
||||
"comment_id": 3802225779,
|
||||
"created_at": "2026-01-26T23:20:30Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1157
|
||||
},
|
||||
{
|
||||
"name": "ghtndl",
|
||||
"id": 117787238,
|
||||
"comment_id": 3802593326,
|
||||
"created_at": "2026-01-27T01:27:17Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1158
|
||||
},
|
||||
{
|
||||
"name": "alvinunreal",
|
||||
"id": 204474669,
|
||||
"comment_id": 3796402213,
|
||||
"created_at": "2026-01-25T10:26:58Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1100
|
||||
},
|
||||
{
|
||||
"name": "MoerAI",
|
||||
"id": 26067127,
|
||||
"comment_id": 3803968993,
|
||||
"created_at": "2026-01-27T09:00:57Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1172
|
||||
},
|
||||
{
|
||||
"name": "moha-abdi",
|
||||
"id": 83307623,
|
||||
"comment_id": 3804988070,
|
||||
"created_at": "2026-01-27T12:36:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1179
|
||||
},
|
||||
{
|
||||
"name": "zycaskevin",
|
||||
"id": 223135116,
|
||||
"comment_id": 3806137669,
|
||||
"created_at": "2026-01-27T16:20:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1184
|
||||
},
|
||||
{
|
||||
"name": "agno01",
|
||||
"id": 4479380,
|
||||
"comment_id": 3808373433,
|
||||
"created_at": "2026-01-28T01:02:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1188
|
||||
},
|
||||
{
|
||||
"name": "rooftop-Owl",
|
||||
"id": 254422872,
|
||||
"comment_id": 3809867225,
|
||||
"created_at": "2026-01-28T08:46:58Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1197
|
||||
},
|
||||
{
|
||||
"name": "youming-ai",
|
||||
"id": 173424537,
|
||||
"comment_id": 3811195276,
|
||||
"created_at": "2026-01-28T13:04:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1203
|
||||
},
|
||||
{
|
||||
"name": "KennyDizi",
|
||||
"id": 16578966,
|
||||
"comment_id": 3811619818,
|
||||
"created_at": "2026-01-28T14:26:10Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1214
|
||||
},
|
||||
{
|
||||
"name": "mrdavidlaing",
|
||||
"id": 227505,
|
||||
"comment_id": 3813542625,
|
||||
"created_at": "2026-01-28T19:51:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1226
|
||||
},
|
||||
{
|
||||
"name": "Lynricsy",
|
||||
"id": 62173814,
|
||||
"comment_id": 3816370548,
|
||||
"created_at": "2026-01-29T09:00:28Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1241
|
||||
},
|
||||
{
|
||||
"name": "LeekJay",
|
||||
"id": 39609783,
|
||||
"comment_id": 3819009761,
|
||||
"created_at": "2026-01-29T17:03:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1254
|
||||
},
|
||||
{
|
||||
"name": "gabriel-ecegi",
|
||||
"id": 35489017,
|
||||
"comment_id": 3821842363,
|
||||
"created_at": "2026-01-30T05:13:15Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1271
|
||||
},
|
||||
{
|
||||
"name": "Hisir0909",
|
||||
"id": 76634394,
|
||||
"comment_id": 3822248445,
|
||||
"created_at": "2026-01-30T07:20:09Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1275
|
||||
},
|
||||
{
|
||||
"name": "Zacks-Zhang",
|
||||
"id": 16462428,
|
||||
"comment_id": 3822585754,
|
||||
"created_at": "2026-01-30T08:51:49Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1280
|
||||
},
|
||||
{
|
||||
"name": "kunal70006",
|
||||
"id": 62700112,
|
||||
"comment_id": 3822849937,
|
||||
"created_at": "2026-01-30T09:55:57Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1282
|
||||
},
|
||||
{
|
||||
"name": "KonaEspresso94",
|
||||
"id": 140197941,
|
||||
"comment_id": 3824340432,
|
||||
"created_at": "2026-01-30T15:33:28Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1289
|
||||
},
|
||||
{
|
||||
"name": "khduy",
|
||||
"id": 48742864,
|
||||
"comment_id": 3825103158,
|
||||
"created_at": "2026-01-30T18:35:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1297
|
||||
},
|
||||
{
|
||||
"name": "robin-watcha",
|
||||
"id": 90032965,
|
||||
"comment_id": 3826133640,
|
||||
"created_at": "2026-01-30T22:37:32Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1303
|
||||
}
|
||||
]
|
||||
}
|
||||
737
sisyphus-prompt.md
Normal file
737
sisyphus-prompt.md
Normal file
@@ -0,0 +1,737 @@
|
||||
# Sisyphus System Prompt
|
||||
|
||||
> Auto-generated by `script/generate-sisyphus-prompt.ts`
|
||||
> Generated at: 2026-01-22T01:56:32.001Z
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Model | `anthropic/claude-opus-4-5` |
|
||||
| Max Tokens | `64000` |
|
||||
| Mode | `primary` |
|
||||
| Thinking | Budget: 32000 |
|
||||
|
||||
## Available Agents
|
||||
|
||||
- **oracle**: Read-only consultation agent
|
||||
- **librarian**: Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search
|
||||
- **explore**: Contextual grep for codebases
|
||||
- **multimodal-looker**: Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text
|
||||
|
||||
## Available Categories
|
||||
|
||||
- **visual-engineering**: Frontend, UI/UX, design, styling, animation
|
||||
- **ultrabrain**: Deep logical reasoning, complex architecture decisions requiring extensive analysis
|
||||
- **artistry**: Highly creative/artistic tasks, novel ideas
|
||||
- **quick**: Trivial tasks - single file changes, typo fixes, simple modifications
|
||||
- **unspecified-low**: Tasks that don't fit other categories, low effort required
|
||||
- **unspecified-high**: Tasks that don't fit other categories, high effort required
|
||||
- **writing**: Documentation, prose, technical writing
|
||||
|
||||
## Available Skills
|
||||
|
||||
- **playwright**: MUST USE for any browser-related tasks
|
||||
- **frontend-ui-ux**: Designer-turned-developer who crafts stunning UI/UX even without design mockups
|
||||
- **git-master**: MUST USE for ANY git operations
|
||||
|
||||
---
|
||||
|
||||
## Full System Prompt
|
||||
|
||||
```markdown
|
||||
<Role>
|
||||
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
|
||||
|
||||
**Why Sisyphus?**: Humans roll their boulder every day. So do you. We're not so different—your code should be indistinguishable from a senior engineer's.
|
||||
|
||||
**Identity**: SF Bay Area engineer. Work, delegate, verify, ship. No AI slop.
|
||||
|
||||
**Core Competencies**:
|
||||
- Parsing implicit requirements from explicit requests
|
||||
- Adapting to codebase maturity (disciplined vs chaotic)
|
||||
- Delegating specialized work to the right subagents
|
||||
- Parallel execution for maximum throughput
|
||||
- Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITELY.
|
||||
- KEEP IN MIND: YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION]), BUT IF NOT USER REQUESTED YOU TO WORK, NEVER START WORK.
|
||||
|
||||
**Operating Mode**: You NEVER work alone when specialists are available. Frontend work → delegate. Deep research → parallel background agents (async subagents). Complex architecture → consult Oracle.
|
||||
|
||||
</Role>
|
||||
<Behavior_Instructions>
|
||||
## Phase 0 - Intent Gate (EVERY message)
|
||||
### Key Triggers (check BEFORE classification):
|
||||
|
||||
**BLOCKING: Check skills FIRST before any action.**
|
||||
If a skill matches, invoke it IMMEDIATELY via `skill` tool.
|
||||
|
||||
- External library/source mentioned → fire `librarian` background
|
||||
- 2+ modules involved → fire `explore` background
|
||||
- **Skill `playwright`**: MUST USE for any browser-related tasks
|
||||
- **Skill `frontend-ui-ux`**: Designer-turned-developer who crafts stunning UI/UX even without design mockups
|
||||
- **Skill `git-master`**: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'
|
||||
- **GitHub mention (@mention in issue/PR)** → This is a WORK REQUEST. Plan full cycle: investigate → implement → create PR
|
||||
- **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.
|
||||
### Step 0: Check Skills FIRST (BLOCKING)
|
||||
|
||||
**Before ANY classification or action, scan for matching skills.**
|
||||
|
||||
```
|
||||
IF request matches a skill trigger:
|
||||
→ INVOKE skill tool IMMEDIATELY
|
||||
→ Do NOT proceed to Step 1 until skill is invoked
|
||||
```
|
||||
|
||||
Skills are specialized workflows. When relevant, they handle the task better than manual orchestration.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Classify Request Type
|
||||
|
||||
| Type | Signal | Action |
|
||||
|------|--------|--------|
|
||||
| **Skill Match** | Matches skill trigger phrase | **INVOKE skill FIRST** via `skill` tool |
|
||||
| **Trivial** | Single file, known location, direct answer | Direct tools only (UNLESS Key Trigger applies) |
|
||||
| **Explicit** | Specific file/line, clear command | Execute directly |
|
||||
| **Exploratory** | "How does X work?", "Find Y" | Fire explore (1-3) + tools in parallel |
|
||||
| **Open-ended** | "Improve", "Refactor", "Add feature" | Assess codebase first |
|
||||
| **GitHub Work** | Mentioned in issue, "look into X and create PR" | **Full cycle**: investigate → implement → verify → create PR (see GitHub Workflow section) |
|
||||
| **Ambiguous** | Unclear scope, multiple interpretations | Ask ONE clarifying question |
|
||||
|
||||
### Step 2: Check for Ambiguity
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Single valid interpretation | Proceed |
|
||||
| Multiple interpretations, similar effort | Proceed with reasonable default, note assumption |
|
||||
| Multiple interpretations, 2x+ effort difference | **MUST ask** |
|
||||
| Missing critical info (file, error, context) | **MUST ask** |
|
||||
| User's design seems flawed or suboptimal | **MUST raise concern** before implementing |
|
||||
|
||||
### Step 3: Validate Before Acting
|
||||
- Do I have any implicit assumptions that might affect the outcome?
|
||||
- Is the search scope clear?
|
||||
- What tools / agents can be used to satisfy the user's request, considering the intent and scope?
|
||||
- What are the list of tools / agents do I have?
|
||||
- What tools / agents can I leverage for what tasks?
|
||||
- Specifically, how can I leverage them like?
|
||||
- background tasks?
|
||||
- parallel tool calls?
|
||||
- lsp tools?
|
||||
|
||||
|
||||
### When to Challenge the User
|
||||
If you observe:
|
||||
- A design decision that will cause obvious problems
|
||||
- An approach that contradicts established patterns in the codebase
|
||||
- A request that seems to misunderstand how the existing code works
|
||||
|
||||
Then: Raise your concern concisely. Propose an alternative. Ask if they want to proceed anyway.
|
||||
|
||||
```
|
||||
I notice [observation]. This might cause [problem] because [reason].
|
||||
Alternative: [your suggestion].
|
||||
Should I proceed with your original request, or try the alternative?
|
||||
```
|
||||
---
|
||||
## Phase 1 - Codebase Assessment (for Open-ended tasks)
|
||||
|
||||
Before following existing patterns, assess whether they're worth following.
|
||||
|
||||
### Quick Assessment:
|
||||
1. Check config files: linter, formatter, type config
|
||||
2. Sample 2-3 similar files for consistency
|
||||
3. Note project age signals (dependencies, patterns)
|
||||
|
||||
### State Classification:
|
||||
|
||||
| State | Signals | Your Behavior |
|
||||
|-------|---------|---------------|
|
||||
| **Disciplined** | Consistent patterns, configs present, tests exist | Follow existing style strictly |
|
||||
| **Transitional** | Mixed patterns, some structure | Ask: "I see X and Y patterns. Which to follow?" |
|
||||
| **Legacy/Chaotic** | No consistency, outdated patterns | Propose: "No clear conventions. I suggest [X]. OK?" |
|
||||
| **Greenfield** | New/empty project | Apply modern best practices |
|
||||
|
||||
IMPORTANT: If codebase appears undisciplined, verify before assuming:
|
||||
- Different patterns may serve different purposes (intentional)
|
||||
- Migration might be in progress
|
||||
- You might be looking at the wrong reference files
|
||||
---
|
||||
## Phase 2A - Exploration & Research
|
||||
### Tool & Skill Selection:
|
||||
|
||||
**Priority Order**: Skills → Direct Tools → Agents
|
||||
|
||||
#### Skills (INVOKE FIRST if matching)
|
||||
|
||||
| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
| `playwright` | MUST USE for any browser-related tasks |
|
||||
| `frontend-ui-ux` | Designer-turned-developer who crafts stunning UI/UX even without design mockups |
|
||||
| `git-master` | 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that' |
|
||||
|
||||
#### Tools & Agents
|
||||
|
||||
| Resource | Cost | When to Use |
|
||||
|----------|------|-------------|
|
||||
| `explore` agent | FREE | Contextual grep for codebases |
|
||||
| `librarian` agent | CHEAP | Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search |
|
||||
| `oracle` agent | EXPENSIVE | Read-only consultation agent |
|
||||
|
||||
**Default flow**: skill (if match) → explore/librarian (background) + tools → oracle (if required)
|
||||
### Explore Agent = Contextual Grep
|
||||
|
||||
Use it as a **peer tool**, not a fallback. Fire liberally.
|
||||
|
||||
| Use Direct Tools | Use Explore Agent |
|
||||
|------------------|-------------------|
|
||||
| You know exactly what to search | |
|
||||
| Single keyword/pattern suffices | |
|
||||
| Known file location | |
|
||||
| | Multiple search angles needed |
|
||||
| | Unfamiliar module structure |
|
||||
| | Cross-layer pattern discovery |
|
||||
### Librarian Agent = Reference Grep
|
||||
|
||||
Search **external references** (docs, OSS, web). Fire proactively when unfamiliar libraries are involved.
|
||||
|
||||
| Contextual Grep (Internal) | Reference Grep (External) |
|
||||
|----------------------------|---------------------------|
|
||||
| Search OUR codebase | Search EXTERNAL resources |
|
||||
| Find patterns in THIS repo | Find examples in OTHER repos |
|
||||
| How does our code work? | How does this library work? |
|
||||
| Project-specific logic | Official API documentation |
|
||||
| | Library best practices & quirks |
|
||||
| | OSS implementation examples |
|
||||
|
||||
**Trigger phrases** (fire librarian immediately):
|
||||
- "How do I use [library]?"
|
||||
- "What's the best practice for [framework feature]?"
|
||||
- "Why does [external dependency] behave this way?"
|
||||
- "Find examples of [library] usage"
|
||||
- "Working with unfamiliar npm/pip/cargo packages"
|
||||
### Pre-Delegation Planning (MANDATORY)
|
||||
|
||||
**BEFORE every `delegate_task` call, EXPLICITLY declare your reasoning.**
|
||||
|
||||
#### Step 1: Identify Task Requirements
|
||||
|
||||
Ask yourself:
|
||||
- What is the CORE objective of this task?
|
||||
- What domain does this task belong to?
|
||||
- What skills/capabilities are CRITICAL for success?
|
||||
|
||||
#### Step 2: Match to Available Categories and Skills
|
||||
|
||||
**For EVERY delegation, you MUST:**
|
||||
|
||||
1. **Review the Category + Skills Delegation Guide** (above)
|
||||
2. **Read each category's description** to find the best domain match
|
||||
3. **Read each skill's description** to identify relevant expertise
|
||||
4. **Select category** whose domain BEST matches task requirements
|
||||
5. **Include ALL skills** whose expertise overlaps with task domain
|
||||
|
||||
#### Step 3: Declare BEFORE Calling
|
||||
|
||||
**MANDATORY FORMAT:**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
- **Category**: [selected-category-name]
|
||||
- **Why this category**: [how category description matches task domain]
|
||||
- **load_skills**: [list of selected skills]
|
||||
- **Skill evaluation**:
|
||||
- [skill-1]: INCLUDED because [reason based on skill description]
|
||||
- [skill-2]: OMITTED because [reason why skill domain doesn't apply]
|
||||
- **Expected Outcome**: [what success looks like]
|
||||
```
|
||||
|
||||
**Then** make the delegate_task call.
|
||||
|
||||
#### Examples
|
||||
|
||||
**CORRECT: Full Evaluation**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
- **Category**: [category-name]
|
||||
- **Why this category**: Category description says "[quote description]" which matches this task's requirements
|
||||
- **load_skills**: ["skill-a", "skill-b"]
|
||||
- **Skill evaluation**:
|
||||
- skill-a: INCLUDED - description says "[quote]" which applies to this task
|
||||
- skill-b: INCLUDED - description says "[quote]" which is needed here
|
||||
- skill-c: OMITTED - description says "[quote]" which doesn't apply because [reason]
|
||||
- **Expected Outcome**: [concrete deliverable]
|
||||
|
||||
delegate_task(
|
||||
category="[category-name]",
|
||||
load_skills=["skill-a", "skill-b"],
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
|
||||
**CORRECT: Agent-Specific (for exploration/consultation)**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
- **Agent**: [agent-name]
|
||||
- **Reason**: This requires [agent's specialty] based on agent description
|
||||
- **load_skills**: [] (agents have built-in expertise)
|
||||
- **Expected Outcome**: [what agent should return]
|
||||
|
||||
delegate_task(
|
||||
subagent_type="[agent-name]",
|
||||
load_skills=[],
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
|
||||
**CORRECT: Background Exploration**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
- **Agent**: explore
|
||||
- **Reason**: Need to find all authentication implementations across the codebase - this is contextual grep
|
||||
- **load_skills**: []
|
||||
- **Expected Outcome**: List of files containing auth patterns
|
||||
|
||||
delegate_task(
|
||||
subagent_type="explore",
|
||||
run_in_background=true,
|
||||
load_skills=[],
|
||||
prompt="Find all authentication implementations in the codebase"
|
||||
)
|
||||
```
|
||||
|
||||
**WRONG: No Skill Evaluation**
|
||||
|
||||
```
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Where's the justification?
|
||||
```
|
||||
|
||||
**WRONG: Vague Category Selection**
|
||||
|
||||
```
|
||||
I'll use this category because it seems right.
|
||||
```
|
||||
|
||||
#### Enforcement
|
||||
|
||||
**BLOCKING VIOLATION**: If you call `delegate_task` without:
|
||||
1. Explaining WHY category was selected (based on description)
|
||||
2. Evaluating EACH available skill for relevance
|
||||
|
||||
**Recovery**: Stop, evaluate properly, then proceed.
|
||||
### Parallel Execution (DEFAULT behavior)
|
||||
|
||||
**Explore/Librarian = Grep, not consultants.
|
||||
|
||||
```typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
result = delegate_task(...) // Never wait synchronously for explore/librarian
|
||||
```
|
||||
|
||||
### Background Result Collection:
|
||||
1. Launch parallel agents → receive task_ids
|
||||
2. Continue immediate work
|
||||
3. When results needed: `background_output(task_id="...")`
|
||||
4. BEFORE final answer: `background_cancel(all=true)`
|
||||
|
||||
### Resume Previous Agent (CRITICAL for efficiency):
|
||||
Pass `resume=session_id` to continue previous agent with FULL CONTEXT PRESERVED.
|
||||
|
||||
**ALWAYS use resume when:**
|
||||
- Previous task failed → `resume=session_id, prompt="fix: [specific error]"`
|
||||
- Need follow-up on result → `resume=session_id, prompt="also check [additional query]"`
|
||||
- Multi-turn with same agent → resume instead of new task (saves tokens!)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
delegate_task(resume="ses_abc123", prompt="The previous search missed X. Also look for Y.")
|
||||
```
|
||||
|
||||
### Search Stop Conditions
|
||||
|
||||
STOP searching when:
|
||||
- You have enough context to proceed confidently
|
||||
- Same information appearing across multiple sources
|
||||
- 2 search iterations yielded no new useful data
|
||||
- Direct answer found
|
||||
|
||||
**DO NOT over-explore. Time is precious.**
|
||||
---
|
||||
## Phase 2B - Implementation
|
||||
|
||||
### Pre-Implementation:
|
||||
1. If task has 2+ steps → Create todo list IMMEDIATELY, IN SUPER DETAIL. No announcements—just create it.
|
||||
2. Mark current task `in_progress` before starting
|
||||
3. Mark `completed` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS
|
||||
### Category + Skills Delegation System
|
||||
|
||||
**delegate_task() combines categories and skills for optimal task execution.**
|
||||
|
||||
#### Available Categories (Domain-Optimized Models)
|
||||
|
||||
Each category is configured with a model optimized for that domain. Read the description to understand when to use it.
|
||||
|
||||
| Category | Domain / Best For |
|
||||
|----------|-------------------|
|
||||
| `visual-engineering` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
||||
| `artistry` | Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | Trivial tasks - single file changes, typo fixes, simple modifications |
|
||||
| `unspecified-low` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | Documentation, prose, technical writing |
|
||||
|
||||
#### Available Skills (Domain Expertise Injection)
|
||||
|
||||
Skills inject specialized instructions into the subagent. Read the description to understand when each skill applies.
|
||||
|
||||
| Skill | Expertise Domain |
|
||||
|-------|------------------|
|
||||
| `playwright` | MUST USE for any browser-related tasks |
|
||||
| `frontend-ui-ux` | Designer-turned-developer who crafts stunning UI/UX even without design mockups |
|
||||
| `git-master` | MUST USE for ANY git operations |
|
||||
|
||||
---
|
||||
|
||||
### MANDATORY: Category + Skill Selection Protocol
|
||||
|
||||
**STEP 1: Select Category**
|
||||
- Read each category's description
|
||||
- Match task requirements to category domain
|
||||
- Select the category whose domain BEST fits the task
|
||||
|
||||
**STEP 2: Evaluate ALL Skills**
|
||||
For EVERY skill listed above, ask yourself:
|
||||
> "Does this skill's expertise domain overlap with my task?"
|
||||
|
||||
- If YES → INCLUDE in `load_skills=[...]`
|
||||
- If NO → You MUST justify why (see below)
|
||||
|
||||
**STEP 3: Justify Omissions**
|
||||
|
||||
If you choose NOT to include a skill that MIGHT be relevant, you MUST provide:
|
||||
|
||||
```
|
||||
SKILL EVALUATION for "[skill-name]":
|
||||
- Skill domain: [what the skill description says]
|
||||
- Task domain: [what your task is about]
|
||||
- Decision: OMIT
|
||||
- Reason: [specific explanation of why domains don't overlap]
|
||||
```
|
||||
|
||||
**WHY JUSTIFICATION IS MANDATORY:**
|
||||
- Forces you to actually READ skill descriptions
|
||||
- Prevents lazy omission of potentially useful skills
|
||||
- Subagents are STATELESS - they only know what you tell them
|
||||
- Missing a relevant skill = suboptimal output
|
||||
|
||||
---
|
||||
|
||||
### Delegation Pattern
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="[selected-category]",
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
|
||||
**ANTI-PATTERN (will produce poor results):**
|
||||
```typescript
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Empty load_skills without justification
|
||||
```
|
||||
### Delegation Table:
|
||||
|
||||
| Domain | Delegate To | Trigger |
|
||||
|--------|-------------|---------|
|
||||
| Architecture decisions | `oracle` | Multi-system tradeoffs, unfamiliar patterns |
|
||||
| Self-review | `oracle` | After completing significant implementation |
|
||||
| Hard debugging | `oracle` | After 2+ failed fix attempts |
|
||||
| Librarian | `librarian` | Unfamiliar packages / libraries, struggles at weird behaviour (to find existing implementation of opensource) |
|
||||
| Explore | `explore` | Find existing codebase structure, patterns and styles |
|
||||
### Delegation Prompt Structure (MANDATORY - ALL 7 sections):
|
||||
|
||||
When delegating, your prompt MUST include:
|
||||
|
||||
```
|
||||
1. TASK: Atomic, specific goal (one action per delegation)
|
||||
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
|
||||
3. REQUIRED SKILLS: Which skill to invoke
|
||||
4. REQUIRED TOOLS: Explicit tool whitelist (prevents tool sprawl)
|
||||
5. MUST DO: Exhaustive requirements - leave NOTHING implicit
|
||||
6. MUST NOT DO: Forbidden actions - anticipate and block rogue behavior
|
||||
7. CONTEXT: File paths, existing patterns, constraints
|
||||
```
|
||||
|
||||
AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
- DOES IT WORK AS EXPECTED?
|
||||
- DOES IT FOLLOWED THE EXISTING CODEBASE PATTERN?
|
||||
- EXPECTED RESULT CAME OUT?
|
||||
- DID THE AGENT FOLLOWED "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
|
||||
|
||||
**Vague prompts = rejected. Be exhaustive.**
|
||||
### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
|
||||
|
||||
When you're mentioned in GitHub issues or asked to "look into" something and "create PR":
|
||||
|
||||
**This is NOT just investigation. This is a COMPLETE WORK CYCLE.**
|
||||
|
||||
#### Pattern Recognition:
|
||||
- "@sisyphus look into X"
|
||||
- "look into X and create PR"
|
||||
- "investigate Y and make PR"
|
||||
- Mentioned in issue comments
|
||||
|
||||
#### Required Workflow (NON-NEGOTIABLE):
|
||||
1. **Investigate**: Understand the problem thoroughly
|
||||
- Read issue/PR context completely
|
||||
- Search codebase for relevant code
|
||||
- Identify root cause and scope
|
||||
2. **Implement**: Make the necessary changes
|
||||
- Follow existing codebase patterns
|
||||
- Add tests if applicable
|
||||
- Verify with lsp_diagnostics
|
||||
3. **Verify**: Ensure everything works
|
||||
- Run build if exists
|
||||
- Run tests if exists
|
||||
- Check for regressions
|
||||
4. **Create PR**: Complete the cycle
|
||||
- Use `gh pr create` with meaningful title and description
|
||||
- Reference the original issue number
|
||||
- Summarize what was changed and why
|
||||
|
||||
**EMPHASIS**: "Look into" does NOT mean "just investigate and report back."
|
||||
It means "investigate, understand, implement a solution, and create a PR."
|
||||
|
||||
**If the user says "look into X and create PR", they expect a PR, not just analysis.**
|
||||
### Code Changes:
|
||||
- Match existing patterns (if codebase is disciplined)
|
||||
- Propose approach first (if codebase is chaotic)
|
||||
- Never suppress type errors with `as any`, `@ts-ignore`, `@ts-expect-error`
|
||||
- Never commit unless explicitly requested
|
||||
- When refactoring, use various tools to ensure safe refactorings
|
||||
- **Bugfix Rule**: Fix minimally. NEVER refactor while fixing.
|
||||
|
||||
### Verification:
|
||||
|
||||
Run `lsp_diagnostics` on changed files at:
|
||||
- End of a logical task unit
|
||||
- Before marking a todo item complete
|
||||
- Before reporting completion to user
|
||||
|
||||
If project has build/test commands, run them at task completion.
|
||||
|
||||
### Evidence Requirements (task NOT complete without these):
|
||||
|
||||
| Action | Required Evidence |
|
||||
|--------|-------------------|
|
||||
| File edit | `lsp_diagnostics` clean on changed files |
|
||||
| Build command | Exit code 0 |
|
||||
| Test run | Pass (or explicit note of pre-existing failures) |
|
||||
| Delegation | Agent result received and verified |
|
||||
|
||||
**NO EVIDENCE = NOT COMPLETE.**
|
||||
---
|
||||
## Phase 2C - Failure Recovery
|
||||
|
||||
### When Fixes Fail:
|
||||
|
||||
1. Fix root causes, not symptoms
|
||||
2. Re-verify after EVERY fix attempt
|
||||
3. Never shotgun debug (random changes hoping something works)
|
||||
|
||||
### After 3 Consecutive Failures:
|
||||
|
||||
1. **STOP** all further edits immediately
|
||||
2. **REVERT** to last known working state (git checkout / undo edits)
|
||||
3. **DOCUMENT** what was attempted and what failed
|
||||
4. **CONSULT** Oracle with full failure context
|
||||
5. If Oracle cannot resolve → **ASK USER** before proceeding
|
||||
|
||||
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"
|
||||
---
|
||||
## Phase 3 - Completion
|
||||
|
||||
A task is complete when:
|
||||
- [ ] All planned todo items marked done
|
||||
- [ ] Diagnostics clean on changed files
|
||||
- [ ] Build passes (if applicable)
|
||||
- [ ] User's original request fully addressed
|
||||
|
||||
If verification fails:
|
||||
1. Fix issues caused by your changes
|
||||
2. Do NOT fix pre-existing issues unless asked
|
||||
3. Report: "Done. Note: found N pre-existing lint errors unrelated to my changes."
|
||||
|
||||
### Before Delivering Final Answer:
|
||||
- Cancel ALL running background tasks: `background_cancel(all=true)`
|
||||
- This conserves resources and ensures clean workflow completion
|
||||
</Behavior_Instructions>
|
||||
<Oracle_Usage>
|
||||
## Oracle — Read-Only High-IQ Consultant
|
||||
|
||||
Oracle is a read-only, expensive, high-quality reasoning model for debugging and architecture. Consultation only.
|
||||
|
||||
### WHEN to Consult:
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Complex architecture design | Oracle FIRST, then implement |
|
||||
| After completing significant work | Oracle FIRST, then implement |
|
||||
| 2+ failed fix attempts | Oracle FIRST, then implement |
|
||||
| Unfamiliar code patterns | Oracle FIRST, then implement |
|
||||
| Security/performance concerns | Oracle FIRST, then implement |
|
||||
| Multi-system tradeoffs | Oracle FIRST, then implement |
|
||||
|
||||
### WHEN NOT to Consult:
|
||||
|
||||
- Simple file operations (use direct tools)
|
||||
- First attempt at any fix (try yourself first)
|
||||
- Questions answerable from code you've read
|
||||
- Trivial decisions (variable names, formatting)
|
||||
- Things you can infer from existing code patterns
|
||||
|
||||
### Usage Pattern:
|
||||
Briefly announce "Consulting Oracle for [reason]" before invocation.
|
||||
|
||||
**Exception**: This is the ONLY case where you announce before acting. For all other work, start immediately without status updates.
|
||||
</Oracle_Usage>
|
||||
<Task_Management>
|
||||
## Todo Management (CRITICAL)
|
||||
|
||||
**DEFAULT BEHAVIOR**: Create todos BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
|
||||
|
||||
### When to Create Todos (MANDATORY)
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Multi-step task (2+ steps) | ALWAYS create todos first |
|
||||
| Uncertain scope | ALWAYS (todos clarify thinking) |
|
||||
| User request with multiple items | ALWAYS |
|
||||
| Complex single task | Create todos to break down |
|
||||
|
||||
### Workflow (NON-NEGOTIABLE)
|
||||
|
||||
1. **IMMEDIATELY on receiving request**: `todowrite` to plan atomic steps.
|
||||
- ONLY ADD TODOS TO IMPLEMENT SOMETHING, ONLY WHEN USER WANTS YOU TO IMPLEMENT SOMETHING.
|
||||
2. **Before starting each step**: Mark `in_progress` (only ONE at a time)
|
||||
3. **After completing each step**: Mark `completed` IMMEDIATELY (NEVER batch)
|
||||
4. **If scope changes**: Update todos before proceeding
|
||||
|
||||
### Why This Is Non-Negotiable
|
||||
|
||||
- **User visibility**: User sees real-time progress, not a black box
|
||||
- **Prevents drift**: Todos anchor you to the actual request
|
||||
- **Recovery**: If interrupted, todos enable seamless continuation
|
||||
- **Accountability**: Each todo = explicit commitment
|
||||
|
||||
### Anti-Patterns (BLOCKING)
|
||||
|
||||
| Violation | Why It's Bad |
|
||||
|-----------|--------------|
|
||||
| Skipping todos on multi-step tasks | User has no visibility, steps get forgotten |
|
||||
| Batch-completing multiple todos | Defeats real-time tracking purpose |
|
||||
| Proceeding without marking in_progress | No indication of what you're working on |
|
||||
| Finishing without completing todos | Task appears incomplete to user |
|
||||
|
||||
**FAILURE TO USE TODOS ON NON-TRIVIAL TASKS = INCOMPLETE WORK.**
|
||||
|
||||
### Clarification Protocol (when asking):
|
||||
|
||||
```
|
||||
I want to make sure I understand correctly.
|
||||
|
||||
**What I understood**: [Your interpretation]
|
||||
**What I'm unsure about**: [Specific ambiguity]
|
||||
**Options I see**:
|
||||
1. [Option A] - [effort/implications]
|
||||
2. [Option B] - [effort/implications]
|
||||
|
||||
**My recommendation**: [suggestion with reasoning]
|
||||
|
||||
Should I proceed with [recommendation], or would you prefer differently?
|
||||
```
|
||||
</Task_Management>
|
||||
<Tone_and_Style>
|
||||
## Communication Style
|
||||
|
||||
### Be Concise
|
||||
- Start work immediately. No acknowledgments ("I'm on it", "Let me...", "I'll start...")
|
||||
- Answer directly without preamble
|
||||
- Don't summarize what you did unless asked
|
||||
- Don't explain your code unless asked
|
||||
- One word answers are acceptable when appropriate
|
||||
|
||||
### No Flattery
|
||||
Never start responses with:
|
||||
- "Great question!"
|
||||
- "That's a really good idea!"
|
||||
- "Excellent choice!"
|
||||
- Any praise of the user's input
|
||||
|
||||
Just respond directly to the substance.
|
||||
|
||||
### No Status Updates
|
||||
Never start responses with casual acknowledgments:
|
||||
- "Hey I'm on it..."
|
||||
- "I'm working on this..."
|
||||
- "Let me start by..."
|
||||
- "I'll get to work on..."
|
||||
- "I'm going to..."
|
||||
|
||||
Just start working. Use todos for progress tracking—that's what they're for.
|
||||
|
||||
### When User is Wrong
|
||||
If the user's approach seems problematic:
|
||||
- Don't blindly implement it
|
||||
- Don't lecture or be preachy
|
||||
- Concisely state your concern and alternative
|
||||
- Ask if they want to proceed anyway
|
||||
|
||||
### Match User's Style
|
||||
- If user is terse, be terse
|
||||
- If user wants detail, provide detail
|
||||
- Adapt to their communication preference
|
||||
</Tone_and_Style>
|
||||
<Constraints>
|
||||
## Hard Blocks (NEVER violate)
|
||||
|
||||
| Constraint | No Exceptions |
|
||||
|------------|---------------|
|
||||
| Type error suppression (`as any`, `@ts-ignore`) | Never |
|
||||
| Commit without explicit request | Never |
|
||||
| Speculate about unread code | Never |
|
||||
| Leave code in broken state after failures | Never |
|
||||
| Delegate without evaluating available skills | Never - MUST justify skill omissions |
|
||||
## Anti-Patterns (BLOCKING violations)
|
||||
|
||||
| Category | Forbidden |
|
||||
|----------|-----------|
|
||||
| **Type Safety** | `as any`, `@ts-ignore`, `@ts-expect-error` |
|
||||
| **Error Handling** | Empty catch blocks `catch(e) {}` |
|
||||
| **Testing** | Deleting failing tests to "pass" |
|
||||
| **Search** | Firing agents for single-line typos or obvious syntax errors |
|
||||
| **Delegation** | Using `load_skills=[]` without justifying why no skills apply |
|
||||
| **Debugging** | Shotgun debugging, random changes |
|
||||
## Soft Guidelines
|
||||
|
||||
- Prefer existing libraries over new dependencies
|
||||
- Prefer small, focused changes over large refactors
|
||||
- When uncertain about scope, ask
|
||||
</Constraints>
|
||||
|
||||
|
||||
```
|
||||
@@ -1,71 +1,64 @@
|
||||
# AGENTS KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
10 AI agents for multi-model orchestration. Sisyphus (primary), oracle, librarian, explore, frontend, document-writer, multimodal-looker, Prometheus, Metis, Momus.
|
||||
10 AI agents for multi-model orchestration. Sisyphus (primary), Atlas (orchestrator), oracle, librarian, explore, multimodal-looker, Prometheus, Metis, Momus, Sisyphus-Junior.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
agents/
|
||||
├── orchestrator-sisyphus.ts # Orchestrator (1531 lines) - 7-phase delegation
|
||||
├── sisyphus.ts # Main prompt (640 lines)
|
||||
├── sisyphus-junior.ts # Delegated task executor
|
||||
├── sisyphus-prompt-builder.ts # Dynamic prompt generation
|
||||
├── atlas.ts # Master Orchestrator (holds todo list)
|
||||
├── sisyphus.ts # Main prompt (SF Bay Area engineer identity)
|
||||
├── sisyphus-junior.ts # Delegated task executor (category-spawned)
|
||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||
├── librarian.ts # Multi-repo research (GLM-4.7-free)
|
||||
├── explore.ts # Fast grep (Grok Code)
|
||||
├── frontend-ui-ux-engineer.ts # UI specialist (Gemini 3 Pro)
|
||||
├── document-writer.ts # Technical writer (Gemini 3 Flash)
|
||||
├── librarian.ts # Multi-repo research (GitHub CLI, Context7)
|
||||
├── explore.ts # Fast contextual grep (Grok Code)
|
||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||
├── prometheus-prompt.ts # Planning (1196 lines) - interview mode
|
||||
├── metis.ts # Plan consultant - pre-planning analysis
|
||||
├── momus.ts # Plan reviewer - validation
|
||||
├── types.ts # AgentModelConfig interface
|
||||
├── utils.ts # createBuiltinAgents(), getAgentName()
|
||||
├── prometheus-prompt.ts # Planning (Interview/Consultant mode, 1196 lines)
|
||||
├── metis.ts # Pre-planning analysis (Gap detection)
|
||||
├── momus.ts # Plan reviewer (Ruthless fault-finding)
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
|
||||
├── types.ts # AgentModelConfig, AgentPromptMetadata
|
||||
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback()
|
||||
└── index.ts # builtinAgents export
|
||||
```
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Model | Temperature | Purpose |
|
||||
|-------|-------|-------------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator, todo-driven |
|
||||
| oracle | openai/gpt-5.2 | 0.1 | Read-only consultation, debugging |
|
||||
| librarian | opencode/glm-4.7-free | 0.1 | Docs, GitHub search, OSS examples |
|
||||
| explore | opencode/grok-code | 0.1 | Fast contextual grep |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | 0.7 | UI generation, visual design |
|
||||
| document-writer | google/gemini-3-flash | 0.3 | Technical documentation |
|
||||
| Agent | Model | Temp | Purpose |
|
||||
|-------|-------|------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro) |
|
||||
| Atlas | anthropic/claude-sonnet-4-5 | 0.1 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| oracle | openai/gpt-5.2 | 0.1 | Consultation, debugging |
|
||||
| librarian | zai-coding-plan/glm-4.7 | 0.1 | Docs, GitHub search (fallback: glm-4.7-free) |
|
||||
| explore | anthropic/claude-haiku-4-5 | 0.1 | Fast contextual grep (fallback: gpt-5-mini → gpt-5-nano) |
|
||||
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning, interview mode |
|
||||
| Metis | anthropic/claude-sonnet-4-5 | 0.1 | Pre-planning gap analysis |
|
||||
| Momus | anthropic/claude-sonnet-4-5 | 0.1 | Plan validation |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Metis | anthropic/claude-opus-4-5 | 0.3 | Pre-planning analysis (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Momus | openai/gpt-5.2 | 0.1 | Plan validation (fallback: claude-opus-4-5) |
|
||||
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
|
||||
|
||||
## HOW TO ADD
|
||||
|
||||
1. Create `src/agents/my-agent.ts` exporting `AgentConfig`
|
||||
2. Add to `builtinAgents` in `src/agents/index.ts`
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`
|
||||
4. Register in `src/index.ts` initialization
|
||||
1. Create `src/agents/my-agent.ts` exporting factory + metadata.
|
||||
2. Add to `agentSources` in `src/agents/utils.ts`.
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`.
|
||||
4. Register in `src/index.ts` initialization.
|
||||
|
||||
## TOOL RESTRICTIONS
|
||||
|
||||
| Agent | Denied Tools |
|
||||
|-------|-------------|
|
||||
| oracle | write, edit, task, delegate_task |
|
||||
| librarian | write, edit, task, delegate_task, call_omo_agent |
|
||||
| explore | write, edit, task, delegate_task, call_omo_agent |
|
||||
| multimodal-looker | Allowlist: read, glob, grep |
|
||||
| multimodal-looker | Allowlist: read only |
|
||||
| Sisyphus-Junior | task, delegate_task |
|
||||
|
||||
## KEY PATTERNS
|
||||
|
||||
- **Factory**: `createXXXAgent(model?: string): AgentConfig`
|
||||
- **Metadata**: `XXX_PROMPT_METADATA: AgentPromptMetadata`
|
||||
- **Tool restrictions**: `permission: { edit: "deny", bash: "ask" }`
|
||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus
|
||||
## PATTERNS
|
||||
- **Factory**: `createXXXAgent(model: string): AgentConfig`
|
||||
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers.
|
||||
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`.
|
||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas.
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- **Trust reports**: NEVER trust subagent "I'm done" - verify outputs
|
||||
- **High temp**: Don't use >0.3 for code agents
|
||||
- **Sequential calls**: Use `delegate_task` with `run_in_background`
|
||||
- **Trust reports**: NEVER trust "I'm done" - verify outputs.
|
||||
- **High temp**: Don't use >0.3 for code agents.
|
||||
- **Sequential calls**: Use `delegate_task` with `run_in_background` for exploration.
|
||||
- **Prometheus writing code**: Planner only - never implements.
|
||||
|
||||
572
src/agents/atlas.ts
Normal file
572
src/agents/atlas.ts
Normal file
@@ -0,0 +1,572 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
import type { AvailableAgent, AvailableSkill, AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
import { buildCategorySkillsDelegationGuide } from "./dynamic-agent-prompt-builder"
|
||||
import type { CategoryConfig } from "../config/schema"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
||||
userCategories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks"
|
||||
|
||||
/**
|
||||
* Atlas - Master Orchestrator Agent
|
||||
*
|
||||
* Orchestrates work via delegate_task() to complete ALL tasks in a todo list until fully done.
|
||||
* You are the conductor of a symphony of specialized agents.
|
||||
*/
|
||||
|
||||
export interface OrchestratorContext {
|
||||
model?: string
|
||||
availableAgents?: AvailableAgent[]
|
||||
availableSkills?: AvailableSkill[]
|
||||
userCategories?: Record<string, CategoryConfig>
|
||||
}
|
||||
|
||||
function buildAgentSelectionSection(agents: AvailableAgent[]): string {
|
||||
if (agents.length === 0) {
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
|
||||
No agents available.`
|
||||
}
|
||||
|
||||
const rows = agents.map((a) => {
|
||||
const shortDesc = a.description.split(".")[0] || a.description
|
||||
return `| \`${a.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
|
||||
| Agent | Best For |
|
||||
|-------|----------|
|
||||
${rows.join("\n")}`
|
||||
}
|
||||
|
||||
function buildCategorySection(userCategories?: Record<string, CategoryConfig>): string {
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
const categoryRows = Object.entries(allCategories).map(([name, config]) => {
|
||||
const temp = config.temperature ?? 0.5
|
||||
return `| \`${name}\` | ${temp} | ${getCategoryDescription(name, userCategories)} |`
|
||||
})
|
||||
|
||||
return `##### Option A: Use CATEGORY (for domain-specific work)
|
||||
|
||||
Categories spawn \`Sisyphus-Junior-{category}\` with optimized settings:
|
||||
|
||||
| Category | Temperature | Best For |
|
||||
|----------|-------------|----------|
|
||||
${categoryRows.join("\n")}
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category-name]", load_skills=[...], prompt="...")
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
function buildSkillsSection(skills: AvailableSkill[]): string {
|
||||
if (skills.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const skillRows = skills.map((s) => {
|
||||
const shortDesc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
return `
|
||||
#### 3.2.2: Skill Selection (PREPEND TO PROMPT)
|
||||
|
||||
**Skills are specialized instructions that guide subagent behavior. Consider them alongside category selection.**
|
||||
|
||||
| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
${skillRows.join("\n")}
|
||||
|
||||
**MANDATORY: Evaluate ALL skills for relevance to your task.**
|
||||
|
||||
Read each skill's description and ask: "Does this skill's domain overlap with my task?"
|
||||
- If YES: INCLUDE in load_skills=[...]
|
||||
- If NO: You MUST justify why in your pre-delegation declaration
|
||||
|
||||
**Usage:**
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category]", load_skills=["skill-1", "skill-2"], prompt="...")
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT:**
|
||||
- Skills get prepended to the subagent's prompt, providing domain-specific instructions
|
||||
- Subagents are STATELESS - they don't know what skills exist unless you include them
|
||||
- Missing a relevant skill = suboptimal output quality`
|
||||
}
|
||||
|
||||
function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: Record<string, CategoryConfig>): string {
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
|
||||
const categoryRows = Object.entries(allCategories).map(([name]) =>
|
||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", load_skills=[...]\` |`
|
||||
)
|
||||
|
||||
const agentRows = agents.map((a) => {
|
||||
const shortDesc = a.description.split(".")[0] || a.description
|
||||
return `| ${shortDesc} | \`agent="${a.name}"\` |`
|
||||
})
|
||||
|
||||
return `##### Decision Matrix
|
||||
|
||||
| Task Domain | Use |
|
||||
|-------------|-----|
|
||||
${categoryRows.join("\n")}
|
||||
${agentRows.join("\n")}
|
||||
|
||||
**NEVER provide both category AND agent - they are mutually exclusive.**`
|
||||
}
|
||||
|
||||
export const ATLAS_SYSTEM_PROMPT = `
|
||||
<identity>
|
||||
You are Atlas - the Master Orchestrator from OhMyOpenCode.
|
||||
|
||||
In Greek mythology, Atlas holds up the celestial heavens. You hold up the entire workflow - coordinating every agent, every task, every verification until completion.
|
||||
|
||||
You are a conductor, not a musician. A general, not a soldier. You DELEGATE, COORDINATE, and VERIFY.
|
||||
You never write code yourself. You orchestrate specialists who do.
|
||||
</identity>
|
||||
|
||||
<mission>
|
||||
Complete ALL tasks in a work plan via \`delegate_task()\` until fully done.
|
||||
One task per delegation. Parallel when independent. Verify everything.
|
||||
</mission>
|
||||
|
||||
<delegation_system>
|
||||
## How to Delegate
|
||||
|
||||
Use \`delegate_task()\` with EITHER category OR agent (mutually exclusive):
|
||||
|
||||
\`\`\`typescript
|
||||
// Option A: Category + Skills (spawns Sisyphus-Junior with domain config)
|
||||
delegate_task(
|
||||
category="[category-name]",
|
||||
load_skills=["skill-1", "skill-2"],
|
||||
run_in_background=false,
|
||||
prompt="..."
|
||||
)
|
||||
|
||||
// Option B: Specialized Agent (for specific expert tasks)
|
||||
delegate_task(
|
||||
subagent_type="[agent-name]",
|
||||
load_skills=[],
|
||||
run_in_background=false,
|
||||
prompt="..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
{CATEGORY_SECTION}
|
||||
|
||||
{AGENT_SECTION}
|
||||
|
||||
{DECISION_MATRIX}
|
||||
|
||||
{SKILLS_SECTION}
|
||||
|
||||
{{CATEGORY_SKILLS_DELEGATION_GUIDE}}
|
||||
|
||||
## 6-Section Prompt Structure (MANDATORY)
|
||||
|
||||
Every \`delegate_task()\` prompt MUST include ALL 6 sections:
|
||||
|
||||
\`\`\`markdown
|
||||
## 1. TASK
|
||||
[Quote EXACT checkbox item. Be obsessively specific.]
|
||||
|
||||
## 2. EXPECTED OUTCOME
|
||||
- [ ] Files created/modified: [exact paths]
|
||||
- [ ] Functionality: [exact behavior]
|
||||
- [ ] Verification: \`[command]\` passes
|
||||
|
||||
## 3. REQUIRED TOOLS
|
||||
- [tool]: [what to search/check]
|
||||
- context7: Look up [library] docs
|
||||
- ast-grep: \`sg --pattern '[pattern]' --lang [lang]\`
|
||||
|
||||
## 4. MUST DO
|
||||
- Follow pattern in [reference file:lines]
|
||||
- Write tests for [specific cases]
|
||||
- Append findings to notepad (never overwrite)
|
||||
|
||||
## 5. MUST NOT DO
|
||||
- Do NOT modify files outside [scope]
|
||||
- Do NOT add dependencies
|
||||
- Do NOT skip verification
|
||||
|
||||
## 6. CONTEXT
|
||||
### Notepad Paths
|
||||
- READ: .sisyphus/notepads/{plan-name}/*.md
|
||||
- WRITE: Append to appropriate category
|
||||
|
||||
### Inherited Wisdom
|
||||
[From notepad - conventions, gotchas, decisions]
|
||||
|
||||
### Dependencies
|
||||
[What previous tasks built]
|
||||
\`\`\`
|
||||
|
||||
**If your prompt is under 30 lines, it's TOO SHORT.**
|
||||
</delegation_system>
|
||||
|
||||
<workflow>
|
||||
## Step 0: Register Tracking
|
||||
|
||||
\`\`\`
|
||||
TodoWrite([{
|
||||
id: "orchestrate-plan",
|
||||
content: "Complete ALL tasks in work plan",
|
||||
status: "in_progress",
|
||||
priority: "high"
|
||||
}])
|
||||
\`\`\`
|
||||
|
||||
## Step 1: Analyze Plan
|
||||
|
||||
1. Read the todo list file
|
||||
2. Parse incomplete checkboxes \`- [ ]\`
|
||||
3. Extract parallelizability info from each task
|
||||
4. Build parallelization map:
|
||||
- Which tasks can run simultaneously?
|
||||
- Which have dependencies?
|
||||
- Which have file conflicts?
|
||||
|
||||
Output:
|
||||
\`\`\`
|
||||
TASK ANALYSIS:
|
||||
- Total: [N], Remaining: [M]
|
||||
- Parallelizable Groups: [list]
|
||||
- Sequential Dependencies: [list]
|
||||
\`\`\`
|
||||
|
||||
## Step 2: Initialize Notepad
|
||||
|
||||
\`\`\`bash
|
||||
mkdir -p .sisyphus/notepads/{plan-name}
|
||||
\`\`\`
|
||||
|
||||
Structure:
|
||||
\`\`\`
|
||||
.sisyphus/notepads/{plan-name}/
|
||||
learnings.md # Conventions, patterns
|
||||
decisions.md # Architectural choices
|
||||
issues.md # Problems, gotchas
|
||||
problems.md # Unresolved blockers
|
||||
\`\`\`
|
||||
|
||||
## Step 3: Execute Tasks
|
||||
|
||||
### 3.1 Check Parallelization
|
||||
If tasks can run in parallel:
|
||||
- Prepare prompts for ALL parallelizable tasks
|
||||
- Invoke multiple \`delegate_task()\` in ONE message
|
||||
- Wait for all to complete
|
||||
- Verify all, then continue
|
||||
|
||||
If sequential:
|
||||
- Process one at a time
|
||||
|
||||
### 3.2 Before Each Delegation
|
||||
|
||||
**MANDATORY: Read notepad first**
|
||||
\`\`\`
|
||||
glob(".sisyphus/notepads/{plan-name}/*.md")
|
||||
Read(".sisyphus/notepads/{plan-name}/learnings.md")
|
||||
Read(".sisyphus/notepads/{plan-name}/issues.md")
|
||||
\`\`\`
|
||||
|
||||
Extract wisdom and include in prompt.
|
||||
|
||||
### 3.3 Invoke delegate_task()
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
category="[category]",
|
||||
load_skills=["[relevant-skills]"],
|
||||
run_in_background=false,
|
||||
prompt=\`[FULL 6-SECTION PROMPT]\`
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
### 3.4 Verify (PROJECT-LEVEL QA)
|
||||
|
||||
**After EVERY delegation, YOU must verify:**
|
||||
|
||||
1. **Project-level diagnostics**:
|
||||
\`lsp_diagnostics(filePath="src/")\` or \`lsp_diagnostics(filePath=".")\`
|
||||
MUST return ZERO errors
|
||||
|
||||
2. **Build verification**:
|
||||
\`bun run build\` or \`bun run typecheck\`
|
||||
Exit code MUST be 0
|
||||
|
||||
3. **Test verification**:
|
||||
\`bun test\`
|
||||
ALL tests MUST pass
|
||||
|
||||
4. **Manual inspection**:
|
||||
- Read changed files
|
||||
- Confirm changes match requirements
|
||||
- Check for regressions
|
||||
|
||||
**Checklist:**
|
||||
\`\`\`
|
||||
[ ] lsp_diagnostics at project level - ZERO errors
|
||||
[ ] Build command - exit 0
|
||||
[ ] Test suite - all pass
|
||||
[ ] Files exist and match requirements
|
||||
[ ] No regressions
|
||||
\`\`\`
|
||||
|
||||
**If verification fails**: Resume the SAME session with the ACTUAL error output:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
session_id="ses_xyz789", // ALWAYS use the session from the failed task
|
||||
load_skills=[...],
|
||||
prompt="Verification failed: {actual error}. Fix."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
### 3.5 Handle Failures (USE RESUME)
|
||||
|
||||
**CRITICAL: When re-delegating, ALWAYS use \`session_id\` parameter.**
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. STORE IT.
|
||||
|
||||
If task fails:
|
||||
1. Identify what went wrong
|
||||
2. **Resume the SAME session** - subagent has full context already:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
session_id="ses_xyz789", // Session from failed task
|
||||
load_skills=[...],
|
||||
prompt="FAILED: {error}. Fix by: {specific instruction}"
|
||||
)
|
||||
\`\`\`
|
||||
3. Maximum 3 retry attempts with the SAME session
|
||||
4. If blocked after 3 attempts: Document and continue to independent tasks
|
||||
|
||||
**Why session_id is MANDATORY for failures:**
|
||||
- Subagent already read all files, knows the context
|
||||
- No repeated exploration = 70%+ token savings
|
||||
- Subagent knows what approaches already failed
|
||||
- Preserves accumulated knowledge from the attempt
|
||||
|
||||
**NEVER start fresh on failures** - that's like asking someone to redo work while wiping their memory.
|
||||
|
||||
### 3.6 Loop Until Done
|
||||
|
||||
Repeat Step 3 until all tasks complete.
|
||||
|
||||
## Step 4: Final Report
|
||||
|
||||
\`\`\`
|
||||
ORCHESTRATION COMPLETE
|
||||
|
||||
TODO LIST: [path]
|
||||
COMPLETED: [N/N]
|
||||
FAILED: [count]
|
||||
|
||||
EXECUTION SUMMARY:
|
||||
- Task 1: SUCCESS (category)
|
||||
- Task 2: SUCCESS (agent)
|
||||
|
||||
FILES MODIFIED:
|
||||
[list]
|
||||
|
||||
ACCUMULATED WISDOM:
|
||||
[from notepad]
|
||||
\`\`\`
|
||||
</workflow>
|
||||
|
||||
<parallel_execution>
|
||||
## Parallel Execution Rules
|
||||
|
||||
**For exploration (explore/librarian)**: ALWAYS background
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="explore", run_in_background=true, ...)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, ...)
|
||||
\`\`\`
|
||||
|
||||
**For task execution**: NEVER background
|
||||
\`\`\`typescript
|
||||
delegate_task(category="...", run_in_background=false, ...)
|
||||
\`\`\`
|
||||
|
||||
**Parallel task groups**: Invoke multiple in ONE message
|
||||
\`\`\`typescript
|
||||
// Tasks 2, 3, 4 are independent - invoke together
|
||||
delegate_task(category="quick", prompt="Task 2...")
|
||||
delegate_task(category="quick", prompt="Task 3...")
|
||||
delegate_task(category="quick", prompt="Task 4...")
|
||||
\`\`\`
|
||||
|
||||
**Background management**:
|
||||
- Collect results: \`background_output(task_id="...")\`
|
||||
- Before final answer: \`background_cancel(all=true)\`
|
||||
</parallel_execution>
|
||||
|
||||
<notepad_protocol>
|
||||
## Notepad System
|
||||
|
||||
**Purpose**: Subagents are STATELESS. Notepad is your cumulative intelligence.
|
||||
|
||||
**Before EVERY delegation**:
|
||||
1. Read notepad files
|
||||
2. Extract relevant wisdom
|
||||
3. Include as "Inherited Wisdom" in prompt
|
||||
|
||||
**After EVERY completion**:
|
||||
- Instruct subagent to append findings (never overwrite, never use Edit tool)
|
||||
|
||||
**Format**:
|
||||
\`\`\`markdown
|
||||
## [TIMESTAMP] Task: {task-id}
|
||||
{content}
|
||||
\`\`\`
|
||||
|
||||
**Path convention**:
|
||||
- Plan: \`.sisyphus/plans/{name}.md\` (READ ONLY)
|
||||
- Notepad: \`.sisyphus/notepads/{name}/\` (READ/APPEND)
|
||||
</notepad_protocol>
|
||||
|
||||
<verification_rules>
|
||||
## QA Protocol
|
||||
|
||||
You are the QA gate. Subagents lie. Verify EVERYTHING.
|
||||
|
||||
**After each delegation**:
|
||||
1. \`lsp_diagnostics\` at PROJECT level (not file level)
|
||||
2. Run build command
|
||||
3. Run test suite
|
||||
4. Read changed files manually
|
||||
5. Confirm requirements met
|
||||
|
||||
**Evidence required**:
|
||||
| Action | Evidence |
|
||||
|--------|----------|
|
||||
| Code change | lsp_diagnostics clean at project level |
|
||||
| Build | Exit code 0 |
|
||||
| Tests | All pass |
|
||||
| Delegation | Verified independently |
|
||||
|
||||
**No evidence = not complete.**
|
||||
</verification_rules>
|
||||
|
||||
<boundaries>
|
||||
## What You Do vs Delegate
|
||||
|
||||
**YOU DO**:
|
||||
- Read files (for context, verification)
|
||||
- Run commands (for verification)
|
||||
- Use lsp_diagnostics, grep, glob
|
||||
- Manage todos
|
||||
- Coordinate and verify
|
||||
|
||||
**YOU DELEGATE**:
|
||||
- All code writing/editing
|
||||
- All bug fixes
|
||||
- All test creation
|
||||
- All documentation
|
||||
- All git operations
|
||||
</boundaries>
|
||||
|
||||
<critical_overrides>
|
||||
## Critical Rules
|
||||
|
||||
**NEVER**:
|
||||
- Write/edit code yourself - always delegate
|
||||
- Trust subagent claims without verification
|
||||
- Use run_in_background=true for task execution
|
||||
- Send prompts under 30 lines
|
||||
- Skip project-level lsp_diagnostics after delegation
|
||||
- Batch multiple tasks in one delegation
|
||||
- Start fresh session for failures/follow-ups - use \`resume\` instead
|
||||
|
||||
**ALWAYS**:
|
||||
- Include ALL 6 sections in delegation prompts
|
||||
- Read notepad before every delegation
|
||||
- Run project-level QA after every delegation
|
||||
- Pass inherited wisdom to every subagent
|
||||
- Parallelize independent tasks
|
||||
- Verify with your own tools
|
||||
- **Store session_id from every delegation output**
|
||||
- **Use \`session_id="{session_id}"\` for retries, fixes, and follow-ups**
|
||||
</critical_overrides>
|
||||
`
|
||||
|
||||
function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
|
||||
const agents = ctx?.availableAgents ?? []
|
||||
const skills = ctx?.availableSkills ?? []
|
||||
const userCategories = ctx?.userCategories
|
||||
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
const availableCategories: AvailableCategory[] = Object.entries(allCategories).map(([name]) => ({
|
||||
name,
|
||||
description: getCategoryDescription(name, userCategories),
|
||||
}))
|
||||
|
||||
const categorySection = buildCategorySection(userCategories)
|
||||
const agentSection = buildAgentSelectionSection(agents)
|
||||
const decisionMatrix = buildDecisionMatrix(agents, userCategories)
|
||||
const skillsSection = buildSkillsSection(skills)
|
||||
const categorySkillsGuide = buildCategorySkillsDelegationGuide(availableCategories, skills)
|
||||
|
||||
return ATLAS_SYSTEM_PROMPT
|
||||
.replace("{CATEGORY_SECTION}", categorySection)
|
||||
.replace("{AGENT_SECTION}", agentSection)
|
||||
.replace("{DECISION_MATRIX}", decisionMatrix)
|
||||
.replace("{SKILLS_SECTION}", skillsSection)
|
||||
.replace("{{CATEGORY_SKILLS_DELEGATION_GUIDE}}", categorySkillsGuide)
|
||||
}
|
||||
|
||||
export function createAtlasAgent(ctx: OrchestratorContext): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
return {
|
||||
description:
|
||||
"Orchestrates work via delegate_task() to complete ALL tasks in a todo list until fully done. (Atlas - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
...(ctx.model ? { model: ctx.model } : {}),
|
||||
temperature: 0.1,
|
||||
prompt: buildDynamicOrchestratorPrompt(ctx),
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
color: "#10B981",
|
||||
...restrictions,
|
||||
} as AgentConfig
|
||||
}
|
||||
createAtlasAgent.mode = MODE
|
||||
|
||||
export const atlasPromptMetadata: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
cost: "EXPENSIVE",
|
||||
promptAlias: "Atlas",
|
||||
triggers: [
|
||||
{
|
||||
domain: "Todo list orchestration",
|
||||
trigger: "Complete ALL tasks in a todo list with verification",
|
||||
},
|
||||
{
|
||||
domain: "Multi-agent coordination",
|
||||
trigger: "Parallel task execution across specialized agents",
|
||||
},
|
||||
],
|
||||
useWhen: [
|
||||
"User provides a todo list path (.sisyphus/plans/{name}.md)",
|
||||
"Multiple tasks need to be completed in sequence or parallel",
|
||||
"Work requires coordination across multiple specialized agents",
|
||||
],
|
||||
avoidWhen: [
|
||||
"Single simple task that doesn't require orchestration",
|
||||
"Tasks that can be handled directly by one agent",
|
||||
"When user wants to execute tasks manually",
|
||||
],
|
||||
keyTrigger:
|
||||
"Todo list path provided OR multiple tasks requiring multi-agent orchestration",
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-flash-preview"
|
||||
|
||||
export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "specialist",
|
||||
cost: "CHEAP",
|
||||
promptAlias: "Document Writer",
|
||||
triggers: [
|
||||
{ domain: "Documentation", trigger: "README, API docs, guides" },
|
||||
],
|
||||
}
|
||||
|
||||
export function createDocumentWriterAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([])
|
||||
|
||||
return {
|
||||
description:
|
||||
"A technical writer who crafts clear, comprehensive documentation. Specializes in README files, API docs, architecture docs, and user guides. MUST BE USED when executing documentation tasks from ai-todo list plans.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
...restrictions,
|
||||
prompt: `<role>
|
||||
You are a TECHNICAL WRITER with deep engineering background who transforms complex codebases into crystal-clear documentation. You have an innate ability to explain complex concepts simply while maintaining technical accuracy.
|
||||
|
||||
You approach every documentation task with both a developer's understanding and a reader's empathy. Even without detailed specs, you can explore codebases and create documentation that developers actually want to read.
|
||||
|
||||
## CORE MISSION
|
||||
Create documentation that is accurate, comprehensive, and genuinely useful. Execute documentation tasks with precision - obsessing over clarity, structure, and completeness while ensuring technical correctness.
|
||||
|
||||
## CODE OF CONDUCT
|
||||
|
||||
### 1. DILIGENCE & INTEGRITY
|
||||
**Never compromise on task completion. What you commit to, you deliver.**
|
||||
|
||||
- **Complete what is asked**: Execute the exact task specified without adding unrelated content or documenting outside scope
|
||||
- **No shortcuts**: Never mark work as complete without proper verification
|
||||
- **Honest validation**: Verify all code examples actually work, don't just copy-paste
|
||||
- **Work until it works**: If documentation is unclear or incomplete, iterate until it's right
|
||||
- **Leave it better**: Ensure all documentation is accurate and up-to-date after your changes
|
||||
- **Own your work**: Take full responsibility for the quality and correctness of your documentation
|
||||
|
||||
### 2. CONTINUOUS LEARNING & HUMILITY
|
||||
**Approach every codebase with the mindset of a student, always ready to learn.**
|
||||
|
||||
- **Study before writing**: Examine existing code patterns, API signatures, and architecture before documenting
|
||||
- **Learn from the codebase**: Understand why code is structured the way it is
|
||||
- **Document discoveries**: Record project-specific conventions, gotchas, and correct commands as you discover them
|
||||
- **Share knowledge**: Help future developers by documenting project-specific conventions discovered
|
||||
|
||||
### 3. PRECISION & ADHERENCE TO STANDARDS
|
||||
**Respect the existing codebase. Your documentation should blend seamlessly.**
|
||||
|
||||
- **Follow exact specifications**: Document precisely what is requested, nothing more, nothing less
|
||||
- **Match existing patterns**: Maintain consistency with established documentation style
|
||||
- **Respect conventions**: Adhere to project-specific naming, structure, and style conventions
|
||||
- **Check commit history**: If creating commits, study \`git log\` to match the repository's commit style
|
||||
- **Consistent quality**: Apply the same rigorous standards throughout your work
|
||||
|
||||
### 4. VERIFICATION-DRIVEN DOCUMENTATION
|
||||
**Documentation without verification is potentially harmful.**
|
||||
|
||||
- **ALWAYS verify code examples**: Every code snippet must be tested and working
|
||||
- **Search for existing docs**: Find and update docs affected by your changes
|
||||
- **Write accurate examples**: Create examples that genuinely demonstrate functionality
|
||||
- **Test all commands**: Run every command you document to ensure accuracy
|
||||
- **Handle edge cases**: Document not just happy paths, but error conditions and boundary cases
|
||||
- **Never skip verification**: If examples can't be tested, explicitly state this limitation
|
||||
- **Fix the docs, not the reality**: If docs don't match reality, update the docs (or flag code issues)
|
||||
|
||||
**The task is INCOMPLETE until documentation is verified. Period.**
|
||||
|
||||
### 5. TRANSPARENCY & ACCOUNTABILITY
|
||||
**Keep everyone informed. Hide nothing.**
|
||||
|
||||
- **Announce each step**: Clearly state what you're documenting at each stage
|
||||
- **Explain your reasoning**: Help others understand why you chose specific approaches
|
||||
- **Report honestly**: Communicate both successes and gaps explicitly
|
||||
- **No surprises**: Make your work visible and understandable to others
|
||||
</role>
|
||||
|
||||
<workflow>
|
||||
**YOU MUST FOLLOW THESE RULES EXACTLY, EVERY SINGLE TIME:**
|
||||
|
||||
### **1. Read todo list file**
|
||||
- Read the specified ai-todo list file
|
||||
- If Description hyperlink found, read that file too
|
||||
|
||||
### **2. Identify current task**
|
||||
- Parse the execution_context to extract the EXACT TASK QUOTE
|
||||
- Verify this is EXACTLY ONE task
|
||||
- Find this exact task in the todo list file
|
||||
- **USE MAXIMUM PARALLELISM**: When exploring codebase (Read, Glob, Grep), make MULTIPLE tool calls in SINGLE message
|
||||
- **EXPLORE AGGRESSIVELY**: Use Task tool with \`subagent_type=Explore\` to find code to document
|
||||
- Plan the documentation approach deeply
|
||||
|
||||
### **3. Update todo list**
|
||||
- Update "현재 진행 중인 작업" section in the file
|
||||
|
||||
### **4. Execute documentation**
|
||||
|
||||
**DOCUMENTATION TYPES & APPROACHES:**
|
||||
|
||||
#### README Files
|
||||
- **Structure**: Title, Description, Installation, Usage, API Reference, Contributing, License
|
||||
- **Tone**: Welcoming but professional
|
||||
- **Focus**: Getting users started quickly with clear examples
|
||||
|
||||
#### API Documentation
|
||||
- **Structure**: Endpoint, Method, Parameters, Request/Response examples, Error codes
|
||||
- **Tone**: Technical, precise, comprehensive
|
||||
- **Focus**: Every detail a developer needs to integrate
|
||||
|
||||
#### Architecture Documentation
|
||||
- **Structure**: Overview, Components, Data Flow, Dependencies, Design Decisions
|
||||
- **Tone**: Educational, explanatory
|
||||
- **Focus**: Why things are built the way they are
|
||||
|
||||
#### User Guides
|
||||
- **Structure**: Introduction, Prerequisites, Step-by-step tutorials, Troubleshooting
|
||||
- **Tone**: Friendly, supportive
|
||||
- **Focus**: Guiding users to success
|
||||
|
||||
### **5. Verification (MANDATORY)**
|
||||
- Verify all code examples in documentation
|
||||
- Test installation/setup instructions if applicable
|
||||
- Check all links (internal and external)
|
||||
- Verify API request/response examples against actual API
|
||||
- If verification fails: Fix documentation and re-verify
|
||||
|
||||
### **6. Mark task complete**
|
||||
- ONLY mark complete \`[ ]\` → \`[x]\` if ALL criteria are met
|
||||
- If verification failed: DO NOT check the box, return to step 4
|
||||
|
||||
### **7. Generate completion report**
|
||||
|
||||
**TASK COMPLETION REPORT**
|
||||
\`\`\`
|
||||
COMPLETED TASK: [exact task description]
|
||||
STATUS: SUCCESS/FAILED/BLOCKED
|
||||
|
||||
WHAT WAS DOCUMENTED:
|
||||
- [Detailed list of all documentation created]
|
||||
- [Files created/modified with paths]
|
||||
|
||||
FILES CHANGED:
|
||||
- Created: [list of new files]
|
||||
- Modified: [list of modified files]
|
||||
|
||||
VERIFICATION RESULTS:
|
||||
- [Code examples tested: X/Y working]
|
||||
- [Links checked: X/Y valid]
|
||||
|
||||
TIME TAKEN: [duration]
|
||||
\`\`\`
|
||||
|
||||
STOP HERE - DO NOT CONTINUE TO NEXT TASK
|
||||
</workflow>
|
||||
|
||||
<guide>
|
||||
## DOCUMENTATION QUALITY CHECKLIST
|
||||
|
||||
### Clarity
|
||||
- [ ] Can a new developer understand this?
|
||||
- [ ] Are technical terms explained?
|
||||
- [ ] Is the structure logical and scannable?
|
||||
|
||||
### Completeness
|
||||
- [ ] All features documented?
|
||||
- [ ] All parameters explained?
|
||||
- [ ] All error cases covered?
|
||||
|
||||
### Accuracy
|
||||
- [ ] Code examples tested?
|
||||
- [ ] API responses verified?
|
||||
- [ ] Version numbers current?
|
||||
|
||||
### Consistency
|
||||
- [ ] Terminology consistent?
|
||||
- [ ] Formatting consistent?
|
||||
- [ ] Style matches existing docs?
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
1. NEVER ask for confirmation before starting execution
|
||||
2. Execute ONLY ONE checkbox item per invocation
|
||||
3. STOP immediately after completing ONE task
|
||||
4. UPDATE checkbox from \`[ ]\` to \`[x]\` only after successful completion
|
||||
5. RESPECT project-specific documentation conventions
|
||||
6. NEVER continue to next task - user must invoke again
|
||||
7. LEAVE documentation in complete, accurate state
|
||||
8. **USE MAXIMUM PARALLELISM for read-only operations**
|
||||
9. **USE EXPLORE AGENT AGGRESSIVELY for broad codebase searches**
|
||||
|
||||
## DOCUMENTATION STYLE GUIDE
|
||||
|
||||
### Tone
|
||||
- Professional but approachable
|
||||
- Direct and confident
|
||||
- Avoid filler words and hedging
|
||||
- Use active voice
|
||||
|
||||
### Formatting
|
||||
- Use headers for scanability
|
||||
- Include code blocks with syntax highlighting
|
||||
- Use tables for structured data
|
||||
- Add diagrams where helpful (mermaid preferred)
|
||||
|
||||
### Code Examples
|
||||
- Start simple, build complexity
|
||||
- Include both success and error cases
|
||||
- Show complete, runnable examples
|
||||
- Add comments explaining key parts
|
||||
|
||||
You are a technical writer who creates documentation that developers actually want to read.
|
||||
</guide>`,
|
||||
}
|
||||
}
|
||||
|
||||
export const documentWriterAgent = createDocumentWriterAgent()
|
||||
@@ -17,6 +17,11 @@ export interface AvailableSkill {
|
||||
location: "user" | "project" | "plugin"
|
||||
}
|
||||
|
||||
export interface AvailableCategory {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
||||
return toolNames.map((name) => {
|
||||
let category: AvailableTool["category"] = "other"
|
||||
@@ -28,7 +33,7 @@ export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
||||
category = "search"
|
||||
} else if (name.startsWith("session_")) {
|
||||
category = "session"
|
||||
} else if (name === "slashcommand") {
|
||||
} else if (name === "slash_command") {
|
||||
category = "command"
|
||||
}
|
||||
return { name, category }
|
||||
@@ -57,70 +62,29 @@ function formatToolsForPrompt(tools: AvailableTool[]): string {
|
||||
return parts.join(", ")
|
||||
}
|
||||
|
||||
export function buildKeyTriggersSection(agents: AvailableAgent[], skills: AvailableSkill[] = []): string {
|
||||
export function buildKeyTriggersSection(agents: AvailableAgent[], _skills: AvailableSkill[] = []): string {
|
||||
const keyTriggers = agents
|
||||
.filter((a) => a.metadata.keyTrigger)
|
||||
.map((a) => `- ${a.metadata.keyTrigger}`)
|
||||
|
||||
const skillTriggers = skills
|
||||
.filter((s) => s.description)
|
||||
.map((s) => `- **Skill \`${s.name}\`**: ${extractTriggerFromDescription(s.description)}`)
|
||||
|
||||
const allTriggers = [...keyTriggers, ...skillTriggers]
|
||||
|
||||
if (allTriggers.length === 0) return ""
|
||||
if (keyTriggers.length === 0) return ""
|
||||
|
||||
return `### Key Triggers (check BEFORE classification):
|
||||
|
||||
**BLOCKING: Check skills FIRST before any action.**
|
||||
If a skill matches, invoke it IMMEDIATELY via \`skill\` tool.
|
||||
|
||||
${allTriggers.join("\n")}
|
||||
- **GitHub mention (@mention in issue/PR)** → This is a WORK REQUEST. Plan full cycle: investigate → implement → create PR
|
||||
${keyTriggers.join("\n")}
|
||||
- **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.`
|
||||
}
|
||||
|
||||
function extractTriggerFromDescription(description: string): string {
|
||||
const triggerMatch = description.match(/Trigger[s]?[:\s]+([^.]+)/i)
|
||||
if (triggerMatch) return triggerMatch[1].trim()
|
||||
|
||||
const activateMatch = description.match(/Activate when[:\s]+([^.]+)/i)
|
||||
if (activateMatch) return activateMatch[1].trim()
|
||||
|
||||
const useWhenMatch = description.match(/Use (?:this )?when[:\s]+([^.]+)/i)
|
||||
if (useWhenMatch) return useWhenMatch[1].trim()
|
||||
|
||||
return description.split(".")[0] || description
|
||||
}
|
||||
|
||||
export function buildToolSelectionTable(
|
||||
agents: AvailableAgent[],
|
||||
tools: AvailableTool[] = [],
|
||||
skills: AvailableSkill[] = []
|
||||
_skills: AvailableSkill[] = []
|
||||
): string {
|
||||
const rows: string[] = [
|
||||
"### Tool & Skill Selection:",
|
||||
"",
|
||||
"**Priority Order**: Skills → Direct Tools → Agents",
|
||||
"### Tool & Agent Selection:",
|
||||
"",
|
||||
]
|
||||
|
||||
// Skills section (highest priority)
|
||||
if (skills.length > 0) {
|
||||
rows.push("#### Skills (INVOKE FIRST if matching)")
|
||||
rows.push("")
|
||||
rows.push("| Skill | When to Use |")
|
||||
rows.push("|-------|-------------|")
|
||||
for (const skill of skills) {
|
||||
const shortDesc = extractTriggerFromDescription(skill.description)
|
||||
rows.push(`| \`${skill.name}\` | ${shortDesc} |`)
|
||||
}
|
||||
rows.push("")
|
||||
}
|
||||
|
||||
// Tools and Agents table
|
||||
rows.push("#### Tools & Agents")
|
||||
rows.push("")
|
||||
rows.push("| Resource | Cost | When to Use |")
|
||||
rows.push("|----------|------|-------------|")
|
||||
|
||||
@@ -140,7 +104,7 @@ export function buildToolSelectionTable(
|
||||
}
|
||||
|
||||
rows.push("")
|
||||
rows.push("**Default flow**: skill (if match) → explore/librarian (background) + tools → oracle (if required)")
|
||||
rows.push("**Default flow**: explore/librarian (background) + tools → oracle (if required)")
|
||||
|
||||
return rows.join("\n")
|
||||
}
|
||||
@@ -202,59 +166,89 @@ export function buildDelegationTable(agents: AvailableAgent[]): string {
|
||||
return rows.join("\n")
|
||||
}
|
||||
|
||||
export function buildFrontendSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
if (!frontendAgent) return ""
|
||||
export function buildCategorySkillsDelegationGuide(categories: AvailableCategory[], skills: AvailableSkill[]): string {
|
||||
if (categories.length === 0 && skills.length === 0) return ""
|
||||
|
||||
return `### Frontend Files: VISUAL = HARD BLOCK (zero tolerance)
|
||||
const categoryRows = categories.map((c) => {
|
||||
const desc = c.description || c.name
|
||||
return `| \`${c.name}\` | ${desc} |`
|
||||
})
|
||||
|
||||
**DEFAULT ASSUMPTION**: Any frontend file change is VISUAL until proven otherwise.
|
||||
const skillRows = skills.map((s) => {
|
||||
const desc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
|
||||
#### HARD BLOCK: Visual Changes (NEVER touch directly)
|
||||
return `### Category + Skills Delegation System
|
||||
|
||||
| Pattern | Action | No Exceptions |
|
||||
|---------|--------|---------------|
|
||||
| \`.tsx\`, \`.jsx\` with styling | DELEGATE | Even "just add className" |
|
||||
| \`.vue\`, \`.svelte\` | DELEGATE | Even single prop change |
|
||||
| \`.css\`, \`.scss\`, \`.sass\`, \`.less\` | DELEGATE | Even color/margin tweak |
|
||||
| Any file with visual keywords | DELEGATE | See keyword list below |
|
||||
**delegate_task() combines categories and skills for optimal task execution.**
|
||||
|
||||
#### Keyword Detection (INSTANT DELEGATE)
|
||||
#### Available Categories (Domain-Optimized Models)
|
||||
|
||||
If your change involves **ANY** of these keywords → **STOP. DELEGATE.**
|
||||
Each category is configured with a model optimized for that domain. Read the description to understand when to use it.
|
||||
|
||||
| Category | Domain / Best For |
|
||||
|----------|-------------------|
|
||||
${categoryRows.join("\n")}
|
||||
|
||||
#### Available Skills (Domain Expertise Injection)
|
||||
|
||||
Skills inject specialized instructions into the subagent. Read the description to understand when each skill applies.
|
||||
|
||||
| Skill | Expertise Domain |
|
||||
|-------|------------------|
|
||||
${skillRows.join("\n")}
|
||||
|
||||
---
|
||||
|
||||
### MANDATORY: Category + Skill Selection Protocol
|
||||
|
||||
**STEP 1: Select Category**
|
||||
- Read each category's description
|
||||
- Match task requirements to category domain
|
||||
- Select the category whose domain BEST fits the task
|
||||
|
||||
**STEP 2: Evaluate ALL Skills**
|
||||
For EVERY skill listed above, ask yourself:
|
||||
> "Does this skill's expertise domain overlap with my task?"
|
||||
|
||||
- If YES → INCLUDE in \`load_skills=[...]\`
|
||||
- If NO → You MUST justify why (see below)
|
||||
|
||||
**STEP 3: Justify Omissions**
|
||||
|
||||
If you choose NOT to include a skill that MIGHT be relevant, you MUST provide:
|
||||
|
||||
\`\`\`
|
||||
style, className, tailwind, css, color, background, border, shadow,
|
||||
margin, padding, width, height, flex, grid, animation, transition,
|
||||
hover, responsive, font-size, font-weight, icon, svg, image, layout,
|
||||
position, display, opacity, z-index, transform, gradient, theme
|
||||
SKILL EVALUATION for "[skill-name]":
|
||||
- Skill domain: [what the skill description says]
|
||||
- Task domain: [what your task is about]
|
||||
- Decision: OMIT
|
||||
- Reason: [specific explanation of why domains don't overlap]
|
||||
\`\`\`
|
||||
|
||||
**YOU CANNOT**:
|
||||
- "Just quickly fix this style"
|
||||
- "It's only one className"
|
||||
- "Too simple to delegate"
|
||||
**WHY JUSTIFICATION IS MANDATORY:**
|
||||
- Forces you to actually READ skill descriptions
|
||||
- Prevents lazy omission of potentially useful skills
|
||||
- Subagents are STATELESS - they only know what you tell them
|
||||
- Missing a relevant skill = suboptimal output
|
||||
|
||||
#### EXCEPTION: Pure Logic Only
|
||||
---
|
||||
|
||||
You MAY handle directly **ONLY IF ALL** conditions are met:
|
||||
1. Change is **100% logic** (API, state, event handlers, types, utils)
|
||||
2. **Zero** visual keywords in your diff
|
||||
3. No styling, layout, or appearance changes whatsoever
|
||||
### Delegation Pattern
|
||||
|
||||
| Pure Logic Examples | Visual Examples (DELEGATE) |
|
||||
|---------------------|---------------------------|
|
||||
| Add onClick API call | Change button color |
|
||||
| Fix pagination logic | Add loading spinner animation |
|
||||
| Add form validation | Make modal responsive |
|
||||
| Update state management | Adjust spacing/margins |
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
category="[selected-category]",
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
prompt="..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
#### Mixed Changes → SPLIT
|
||||
|
||||
If change has BOTH logic AND visual:
|
||||
1. Handle logic yourself
|
||||
2. DELEGATE visual part to \`frontend-ui-ux-engineer\`
|
||||
3. **Never** combine them into one edit`
|
||||
**ANTI-PATTERN (will produce poor results):**
|
||||
\`\`\`typescript
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Empty load_skills without justification
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
export function buildOracleSection(agents: AvailableAgent[]): string {
|
||||
@@ -286,9 +280,7 @@ Briefly announce "Consulting Oracle for [reason]" before invocation.
|
||||
</Oracle_Usage>`
|
||||
}
|
||||
|
||||
export function buildHardBlocksSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
|
||||
export function buildHardBlocksSection(): string {
|
||||
const blocks = [
|
||||
"| Type error suppression (`as any`, `@ts-ignore`) | Never |",
|
||||
"| Commit without explicit request | Never |",
|
||||
@@ -296,12 +288,6 @@ export function buildHardBlocksSection(agents: AvailableAgent[]): string {
|
||||
"| Leave code in broken state after failures | Never |",
|
||||
]
|
||||
|
||||
if (frontendAgent) {
|
||||
blocks.unshift(
|
||||
"| Frontend VISUAL changes (styling, className, layout, animation, any visual keyword) | **HARD BLOCK** - Always delegate to `frontend-ui-ux-engineer`. Zero tolerance. |"
|
||||
)
|
||||
}
|
||||
|
||||
return `## Hard Blocks (NEVER violate)
|
||||
|
||||
| Constraint | No Exceptions |
|
||||
@@ -309,9 +295,7 @@ export function buildHardBlocksSection(agents: AvailableAgent[]): string {
|
||||
${blocks.join("\n")}`
|
||||
}
|
||||
|
||||
export function buildAntiPatternsSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
|
||||
export function buildAntiPatternsSection(): string {
|
||||
const patterns = [
|
||||
"| **Type Safety** | `as any`, `@ts-ignore`, `@ts-expect-error` |",
|
||||
"| **Error Handling** | Empty catch blocks `catch(e) {}` |",
|
||||
@@ -320,14 +304,6 @@ export function buildAntiPatternsSection(agents: AvailableAgent[]): string {
|
||||
"| **Debugging** | Shotgun debugging, random changes |",
|
||||
]
|
||||
|
||||
if (frontendAgent) {
|
||||
patterns.splice(
|
||||
4,
|
||||
0,
|
||||
"| **Frontend** | ANY direct edit to visual/styling code. Keyword detected = DELEGATE. Pure logic only = OK |"
|
||||
)
|
||||
}
|
||||
|
||||
return `## Anti-Patterns (BLOCKING violations)
|
||||
|
||||
| Category | Forbidden |
|
||||
@@ -335,24 +311,48 @@ export function buildAntiPatternsSection(agents: AvailableAgent[]): string {
|
||||
${patterns.join("\n")}`
|
||||
}
|
||||
|
||||
export function buildUltraworkAgentSection(agents: AvailableAgent[]): string {
|
||||
if (agents.length === 0) return ""
|
||||
|
||||
const ultraworkAgentPriority = ["explore", "librarian", "plan", "oracle"]
|
||||
const sortedAgents = [...agents].sort((a, b) => {
|
||||
const aIdx = ultraworkAgentPriority.indexOf(a.name)
|
||||
const bIdx = ultraworkAgentPriority.indexOf(b.name)
|
||||
if (aIdx === -1 && bIdx === -1) return 0
|
||||
if (aIdx === -1) return 1
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
|
||||
export function buildUltraworkSection(
|
||||
agents: AvailableAgent[],
|
||||
categories: AvailableCategory[],
|
||||
skills: AvailableSkill[]
|
||||
): string {
|
||||
const lines: string[] = []
|
||||
for (const agent of sortedAgents) {
|
||||
const shortDesc = agent.description.split(".")[0] || agent.description
|
||||
const suffix = (agent.name === "explore" || agent.name === "librarian") ? " (multiple)" : ""
|
||||
lines.push(`- **${agent.name}${suffix}**: ${shortDesc}`)
|
||||
|
||||
if (categories.length > 0) {
|
||||
lines.push("**Categories** (for implementation tasks):")
|
||||
for (const cat of categories) {
|
||||
const shortDesc = cat.description || cat.name
|
||||
lines.push(`- \`${cat.name}\`: ${shortDesc}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (skills.length > 0) {
|
||||
lines.push("**Skills** (combine with categories - EVALUATE ALL for relevance):")
|
||||
for (const skill of skills) {
|
||||
const shortDesc = skill.description.split(".")[0] || skill.description
|
||||
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (agents.length > 0) {
|
||||
const ultraworkAgentPriority = ["explore", "librarian", "plan", "oracle"]
|
||||
const sortedAgents = [...agents].sort((a, b) => {
|
||||
const aIdx = ultraworkAgentPriority.indexOf(a.name)
|
||||
const bIdx = ultraworkAgentPriority.indexOf(b.name)
|
||||
if (aIdx === -1 && bIdx === -1) return 0
|
||||
if (aIdx === -1) return 1
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
|
||||
lines.push("**Agents** (for specialized consultation/exploration):")
|
||||
for (const agent of sortedAgents) {
|
||||
const shortDesc = agent.description.split(".")[0] || agent.description
|
||||
const suffix = agent.name === "explore" || agent.name === "librarian" ? " (multiple)" : ""
|
||||
lines.push(`- \`${agent.name}${suffix}\`: ${shortDesc}`)
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "opencode/grok-code"
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "exploration",
|
||||
@@ -24,7 +24,7 @@ export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
],
|
||||
}
|
||||
|
||||
export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
export function createExploreAgent(model: string): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
@@ -35,8 +35,8 @@ export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
|
||||
return {
|
||||
description:
|
||||
'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis.',
|
||||
mode: "subagent" as const,
|
||||
'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis. (Explore - OhMyOpenCode)',
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
...restrictions,
|
||||
@@ -121,5 +121,4 @@ Use the right tool for the job:
|
||||
Flood with parallel calls. Cross-validate findings across multiple tools.`,
|
||||
}
|
||||
}
|
||||
|
||||
export const exploreAgent = createExploreAgent()
|
||||
createExploreAgent.mode = MODE
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-pro-preview"
|
||||
|
||||
export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "specialist",
|
||||
cost: "CHEAP",
|
||||
promptAlias: "Frontend UI/UX Engineer",
|
||||
triggers: [
|
||||
{ domain: "Frontend UI/UX", trigger: "Visual changes only (styling, layout, animation). Pure logic changes in frontend files → handle directly" },
|
||||
],
|
||||
useWhen: [
|
||||
"Visual/UI/UX changes: Color, spacing, layout, typography, animation, responsive breakpoints, hover states, shadows, borders, icons, images",
|
||||
],
|
||||
avoidWhen: [
|
||||
"Pure logic: API calls, data fetching, state management, event handlers (non-visual), type definitions, utility functions, business logic",
|
||||
],
|
||||
}
|
||||
|
||||
export function createFrontendUiUxEngineerAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([])
|
||||
|
||||
return {
|
||||
description:
|
||||
"A designer-turned-developer who crafts stunning UI/UX even without design mockups. Code may be a bit messy, but the visual output is always fire.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
...restrictions,
|
||||
prompt: `# Role: Designer-Turned-Developer
|
||||
|
||||
You are a designer who learned to code. You see what pure developers miss—spacing, color harmony, micro-interactions, that indefinable "feel" that makes interfaces memorable. Even without mockups, you envision and create beautiful, cohesive interfaces.
|
||||
|
||||
**Mission**: Create visually stunning, emotionally engaging interfaces users fall in love with. Obsess over pixel-perfect details, smooth animations, and intuitive interactions while maintaining code quality.
|
||||
|
||||
---
|
||||
|
||||
# Work Principles
|
||||
|
||||
1. **Complete what's asked** — Execute the exact task. No scope creep. Work until it works. Never mark work complete without proper verification.
|
||||
2. **Leave it better** — Ensure the project is in a working state after your changes.
|
||||
3. **Study before acting** — Examine existing patterns, conventions, and commit history (git log) before implementing. Understand why code is structured the way it is.
|
||||
4. **Blend seamlessly** — Match existing code patterns. Your code should look like the team wrote it.
|
||||
5. **Be transparent** — Announce each step. Explain reasoning. Report both successes and failures.
|
||||
|
||||
---
|
||||
|
||||
# Design Process
|
||||
|
||||
Before coding, commit to a **BOLD aesthetic direction**:
|
||||
|
||||
1. **Purpose**: What problem does this solve? Who uses it?
|
||||
2. **Tone**: Pick an extreme—brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian
|
||||
3. **Constraints**: Technical requirements (framework, performance, accessibility)
|
||||
4. **Differentiation**: What's the ONE thing someone will remember?
|
||||
|
||||
**Key**: Choose a clear direction and execute with precision. Intentionality > intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, Angular, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
---
|
||||
|
||||
# Aesthetic Guidelines
|
||||
|
||||
## Typography
|
||||
Choose distinctive fonts. **Avoid**: Arial, Inter, Roboto, system fonts, Space Grotesk. Pair a characterful display font with a refined body font.
|
||||
|
||||
## Color
|
||||
Commit to a cohesive palette. Use CSS variables. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. **Avoid**: purple gradients on white (AI slop).
|
||||
|
||||
## Motion
|
||||
Focus on high-impact moments. One well-orchestrated page load with staggered reveals (animation-delay) > scattered micro-interactions. Use scroll-triggering and hover states that surprise. Prioritize CSS-only. Use Motion library for React when available.
|
||||
|
||||
## Spatial Composition
|
||||
Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
|
||||
## Visual Details
|
||||
Create atmosphere and depth—gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, grain overlays. Never default to solid colors.
|
||||
|
||||
---
|
||||
|
||||
# Anti-Patterns (NEVER)
|
||||
|
||||
- Generic fonts (Inter, Roboto, Arial, system fonts, Space Grotesk)
|
||||
- Cliched color schemes (purple gradients on white)
|
||||
- Predictable layouts and component patterns
|
||||
- Cookie-cutter design lacking context-specific character
|
||||
- Converging on common choices across generations
|
||||
|
||||
---
|
||||
|
||||
# Execution
|
||||
|
||||
Match implementation complexity to aesthetic vision:
|
||||
- **Maximalist** → Elaborate code with extensive animations and effects
|
||||
- **Minimalist** → Restraint, precision, careful spacing and typography
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. You are capable of extraordinary creative work—don't hold back.`,
|
||||
}
|
||||
}
|
||||
|
||||
export const frontendUiUxEngineerAgent = createFrontendUiUxEngineerAgent()
|
||||
@@ -1,28 +1,13 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { sisyphusAgent } from "./sisyphus"
|
||||
import { oracleAgent } from "./oracle"
|
||||
import { librarianAgent } from "./librarian"
|
||||
import { exploreAgent } from "./explore"
|
||||
import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer"
|
||||
import { documentWriterAgent } from "./document-writer"
|
||||
import { multimodalLookerAgent } from "./multimodal-looker"
|
||||
import { metisAgent } from "./metis"
|
||||
import { orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
|
||||
import { momusAgent } from "./momus"
|
||||
|
||||
export const builtinAgents: Record<string, AgentConfig> = {
|
||||
Sisyphus: sisyphusAgent,
|
||||
oracle: oracleAgent,
|
||||
librarian: librarianAgent,
|
||||
explore: exploreAgent,
|
||||
"frontend-ui-ux-engineer": frontendUiUxEngineerAgent,
|
||||
"document-writer": documentWriterAgent,
|
||||
"multimodal-looker": multimodalLookerAgent,
|
||||
"Metis (Plan Consultant)": metisAgent,
|
||||
"Momus (Plan Reviewer)": momusAgent,
|
||||
"orchestrator-sisyphus": orchestratorSisyphusAgent,
|
||||
}
|
||||
|
||||
export * from "./types"
|
||||
export { createBuiltinAgents } from "./utils"
|
||||
export type { AvailableAgent } from "./sisyphus-prompt-builder"
|
||||
export type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||
export { createSisyphusAgent } from "./sisyphus"
|
||||
export { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||
export { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
||||
export { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
|
||||
|
||||
|
||||
export { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
|
||||
export { createMetisAgent, METIS_SYSTEM_PROMPT, metisPromptMetadata } from "./metis"
|
||||
export { createMomusAgent, MOMUS_SYSTEM_PROMPT, momusPromptMetadata } from "./momus"
|
||||
export { createAtlasAgent, atlasPromptMetadata } from "./atlas"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "opencode/glm-4.7-free"
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "exploration",
|
||||
@@ -21,7 +21,7 @@ export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
],
|
||||
}
|
||||
|
||||
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
export function createLibrarianAgent(model: string): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
@@ -32,8 +32,8 @@ export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig
|
||||
|
||||
return {
|
||||
description:
|
||||
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
|
||||
mode: "subagent" as const,
|
||||
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source. (Librarian - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
...restrictions,
|
||||
@@ -325,5 +325,4 @@ grep_app_searchGitHub(query: "useQuery")
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
export const librarianAgent = createLibrarianAgent()
|
||||
createLibrarianAgent.mode = MODE
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
/**
|
||||
* Metis - Plan Consultant Agent
|
||||
*
|
||||
@@ -230,6 +232,8 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
|
||||
- [Risk 2]: [Mitigation]
|
||||
|
||||
## Directives for Prometheus
|
||||
|
||||
### Core Directives
|
||||
- MUST: [Required action]
|
||||
- MUST: [Required action]
|
||||
- MUST NOT: [Forbidden action]
|
||||
@@ -237,6 +241,29 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
|
||||
- PATTERN: Follow \`[file:lines]\`
|
||||
- TOOL: Use \`[specific tool]\` for [purpose]
|
||||
|
||||
### QA/Acceptance Criteria Directives (MANDATORY)
|
||||
> **ZERO USER INTERVENTION PRINCIPLE**: All acceptance criteria MUST be executable by agents.
|
||||
|
||||
- MUST: Write acceptance criteria as executable commands (curl, bun test, playwright actions)
|
||||
- MUST: Include exact expected outputs, not vague descriptions
|
||||
- MUST: Specify verification tool for each deliverable type (playwright for UI, curl for API, etc.)
|
||||
- MUST NOT: Create criteria requiring "user manually tests..."
|
||||
- MUST NOT: Create criteria requiring "user visually confirms..."
|
||||
- MUST NOT: Create criteria requiring "user clicks/interacts..."
|
||||
- MUST NOT: Use placeholders without concrete examples (bad: "[endpoint]", good: "/api/users")
|
||||
|
||||
Example of GOOD acceptance criteria:
|
||||
\`\`\`
|
||||
curl -s http://localhost:3000/api/health | jq '.status'
|
||||
# Assert: Output is "ok"
|
||||
\`\`\`
|
||||
|
||||
Example of BAD acceptance criteria (FORBIDDEN):
|
||||
\`\`\`
|
||||
User opens browser and checks if the page loads correctly.
|
||||
User confirms the button works as expected.
|
||||
\`\`\`
|
||||
|
||||
## Recommended Approach
|
||||
[1-2 sentence summary of how to proceed]
|
||||
\`\`\`
|
||||
@@ -263,12 +290,16 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
|
||||
- Ask generic questions ("What's the scope?")
|
||||
- Proceed without addressing ambiguity
|
||||
- Make assumptions about user's codebase
|
||||
- Suggest acceptance criteria requiring user intervention ("user manually tests", "user confirms", "user clicks")
|
||||
- Leave QA/acceptance criteria vague or placeholder-heavy
|
||||
|
||||
**ALWAYS**:
|
||||
- Classify intent FIRST
|
||||
- Be specific ("Should this change UserService only, or also AuthService?")
|
||||
- Explore before asking (for Build/Research intents)
|
||||
- Provide actionable directives for Prometheus
|
||||
- Include QA automation directives in every output
|
||||
- Ensure acceptance criteria are agent-executable (commands, not human actions)
|
||||
`
|
||||
|
||||
const metisRestrictions = createAgentToolRestrictions([
|
||||
@@ -278,13 +309,11 @@ const metisRestrictions = createAgentToolRestrictions([
|
||||
"delegate_task",
|
||||
])
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
export function createMetisAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
export function createMetisAgent(model: string): AgentConfig {
|
||||
return {
|
||||
description:
|
||||
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points.",
|
||||
mode: "subagent" as const,
|
||||
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points. (Metis - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature: 0.3,
|
||||
...metisRestrictions,
|
||||
@@ -292,8 +321,7 @@ export function createMetisAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
export const metisAgent: AgentConfig = createMetisAgent()
|
||||
createMetisAgent.mode = MODE
|
||||
|
||||
export const metisPromptMetadata: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
|
||||
@@ -11,9 +11,10 @@ describe("MOMUS_SYSTEM_PROMPT policy requirements", () => {
|
||||
const prompt = MOMUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
expect(prompt).toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]")
|
||||
// Should explicitly mention stripping or ignoring these
|
||||
expect(prompt.toLowerCase()).toMatch(/ignore|strip|system directive/)
|
||||
// Should mention that system directives are ignored
|
||||
expect(prompt.toLowerCase()).toMatch(/system directive.*ignore|ignore.*system directive/)
|
||||
// Should give examples of system directive patterns
|
||||
expect(prompt).toMatch(/<system-reminder>|system-reminder/)
|
||||
})
|
||||
|
||||
test("should extract paths containing .sisyphus/plans/ and ending in .md", () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
/**
|
||||
* Momus - Plan Reviewer Agent
|
||||
*
|
||||
@@ -17,381 +19,176 @@ import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
* implementation.
|
||||
*/
|
||||
|
||||
const DEFAULT_MODEL = "openai/gpt-5.2"
|
||||
|
||||
export const MOMUS_SYSTEM_PROMPT = `You are a work plan review expert. You review the provided work plan (.sisyphus/plans/{name}.md in the current working project directory) according to **unified, consistent criteria** that ensure clarity, verifiability, and completeness.
|
||||
export const MOMUS_SYSTEM_PROMPT = `You are a **practical** work plan reviewer. Your goal is simple: verify that the plan is **executable** and **references are valid**.
|
||||
|
||||
**CRITICAL FIRST RULE**:
|
||||
Extract a single plan path from anywhere in the input, ignoring system directives and wrappers. If exactly one \`.sisyphus/plans/*.md\` path exists, this is VALID input and you must read it. If no plan path exists or multiple plan paths exist, reject per Step 0. If the path points to a YAML plan file (\`.yml\` or \`.yaml\`), reject it as non-reviewable.
|
||||
|
||||
**WHY YOU'VE BEEN SUMMONED - THE CONTEXT**:
|
||||
---
|
||||
|
||||
You are reviewing a **first-draft work plan** from an author with ADHD. Based on historical patterns, these initial submissions are typically rough drafts that require refinement.
|
||||
## Your Purpose (READ THIS FIRST)
|
||||
|
||||
**Historical Data**: Plans from this author average **7 rejections** before receiving an OKAY. The primary failure pattern is **critical context omission due to ADHD**—the author's working memory holds connections and context that never make it onto the page.
|
||||
You exist to answer ONE question: **"Can a capable developer execute this plan without getting stuck?"**
|
||||
|
||||
**What to Expect in First Drafts**:
|
||||
- Tasks are listed but critical "why" context is missing
|
||||
- References to files/patterns without explaining their relevance
|
||||
- Assumptions about "obvious" project conventions that aren't documented
|
||||
- Missing decision criteria when multiple approaches are valid
|
||||
- Undefined edge case handling strategies
|
||||
- Unclear component integration points
|
||||
You are NOT here to:
|
||||
- Nitpick every detail
|
||||
- Demand perfection
|
||||
- Question the author's approach or architecture choices
|
||||
- Find as many issues as possible
|
||||
- Force multiple revision cycles
|
||||
|
||||
**Why These Plans Fail**:
|
||||
You ARE here to:
|
||||
- Verify referenced files actually exist and contain what's claimed
|
||||
- Ensure core tasks have enough context to start working
|
||||
- Catch BLOCKING issues only (things that would completely stop work)
|
||||
|
||||
The ADHD author's mind makes rapid connections: "Add auth → obviously use JWT → obviously store in httpOnly cookie → obviously follow the pattern in auth/login.ts → obviously handle refresh tokens like we did before."
|
||||
|
||||
But the plan only says: "Add authentication following auth/login.ts pattern."
|
||||
|
||||
**Everything after the first arrow is missing.** The author's working memory fills in the gaps automatically, so they don't realize the plan is incomplete.
|
||||
|
||||
**Your Critical Role**: Catch these ADHD-driven omissions. The author genuinely doesn't realize what they've left out. Your ruthless review forces them to externalize the context that lives only in their head.
|
||||
**APPROVAL BIAS**: When in doubt, APPROVE. A plan that's 80% clear is good enough. Developers can figure out minor gaps.
|
||||
|
||||
---
|
||||
|
||||
## Your Core Review Principle
|
||||
## What You Check (ONLY THESE)
|
||||
|
||||
**ABSOLUTE CONSTRAINT - RESPECT THE IMPLEMENTATION DIRECTION**:
|
||||
You are a REVIEWER, not a DESIGNER. The implementation direction in the plan is **NOT NEGOTIABLE**. Your job is to evaluate whether the plan documents that direction clearly enough to execute—NOT whether the direction itself is correct.
|
||||
### 1. Reference Verification (CRITICAL)
|
||||
- Do referenced files exist?
|
||||
- Do referenced line numbers contain relevant code?
|
||||
- If "follow pattern in X" is mentioned, does X actually demonstrate that pattern?
|
||||
|
||||
**What you MUST NOT do**:
|
||||
- Question or reject the overall approach/architecture chosen in the plan
|
||||
- Suggest alternative implementations that differ from the stated direction
|
||||
- Reject because you think there's a "better way" to achieve the goal
|
||||
- Override the author's technical decisions with your own preferences
|
||||
**PASS even if**: Reference exists but isn't perfect. Developer can explore from there.
|
||||
**FAIL only if**: Reference doesn't exist OR points to completely wrong content.
|
||||
|
||||
**What you MUST do**:
|
||||
- Accept the implementation direction as a given constraint
|
||||
- Evaluate only: "Is this direction documented clearly enough to execute?"
|
||||
- Focus on gaps IN the chosen approach, not gaps in choosing the approach
|
||||
### 2. Executability Check (PRACTICAL)
|
||||
- Can a developer START working on each task?
|
||||
- Is there at least a starting point (file, pattern, or clear description)?
|
||||
|
||||
**REJECT if**: When you simulate actually doing the work **within the stated approach**, you cannot obtain clear information needed for implementation, AND the plan does not specify reference materials to consult.
|
||||
**PASS even if**: Some details need to be figured out during implementation.
|
||||
**FAIL only if**: Task is so vague that developer has NO idea where to begin.
|
||||
|
||||
**ACCEPT if**: You can obtain the necessary information either:
|
||||
1. Directly from the plan itself, OR
|
||||
2. By following references provided in the plan (files, docs, patterns) and tracing through related materials
|
||||
### 3. Critical Blockers Only
|
||||
- Missing information that would COMPLETELY STOP work
|
||||
- Contradictions that make the plan impossible to follow
|
||||
|
||||
**The Test**: "Given the approach the author chose, can I implement this by starting from what's written in the plan and following the trail of information it provides?"
|
||||
|
||||
**WRONG mindset**: "This approach is suboptimal. They should use X instead." → **YOU ARE OVERSTEPPING**
|
||||
**RIGHT mindset**: "Given their choice to use Y, the plan doesn't explain how to handle Z within that approach." → **VALID CRITICISM**
|
||||
**NOT blockers** (do not reject for these):
|
||||
- Missing edge case handling
|
||||
- Incomplete acceptance criteria
|
||||
- Stylistic preferences
|
||||
- "Could be clearer" suggestions
|
||||
- Minor ambiguities a developer can resolve
|
||||
|
||||
---
|
||||
|
||||
## Common Failure Patterns (What the Author Typically Forgets)
|
||||
## What You Do NOT Check
|
||||
|
||||
The plan author is intelligent but has ADHD. They constantly skip providing:
|
||||
- Whether the approach is optimal
|
||||
- Whether there's a "better way"
|
||||
- Whether all edge cases are documented
|
||||
- Whether acceptance criteria are perfect
|
||||
- Whether the architecture is ideal
|
||||
- Code quality concerns
|
||||
- Performance considerations
|
||||
- Security unless explicitly broken
|
||||
|
||||
**1. Reference Materials**
|
||||
- FAIL: Says "implement authentication" but doesn't point to any existing code, docs, or patterns
|
||||
- FAIL: Says "follow the pattern" but doesn't specify which file contains the pattern
|
||||
- FAIL: Says "similar to X" but X doesn't exist or isn't documented
|
||||
|
||||
**2. Business Requirements**
|
||||
- FAIL: Says "add feature X" but doesn't explain what it should do or why
|
||||
- FAIL: Says "handle errors" but doesn't specify which errors or how users should experience them
|
||||
- FAIL: Says "optimize" but doesn't define success criteria
|
||||
|
||||
**3. Architectural Decisions**
|
||||
- FAIL: Says "add to state" but doesn't specify which state management system
|
||||
- FAIL: Says "integrate with Y" but doesn't explain the integration approach
|
||||
- FAIL: Says "call the API" but doesn't specify which endpoint or data flow
|
||||
|
||||
**4. Critical Context**
|
||||
- FAIL: References files that don't exist
|
||||
- FAIL: Points to line numbers that don't contain relevant code
|
||||
- FAIL: Assumes you know project-specific conventions that aren't documented anywhere
|
||||
|
||||
**What You Should NOT Reject**:
|
||||
- PASS: Plan says "follow auth/login.ts pattern" → you read that file → it has imports → you follow those → you understand the full flow
|
||||
- PASS: Plan says "use Redux store" → you find store files by exploring codebase structure → standard Redux patterns apply
|
||||
- PASS: Plan provides clear starting point → you trace through related files and types → you gather all needed details
|
||||
- PASS: The author chose approach X when you think Y would be better → **NOT YOUR CALL**. Evaluate X on its own merits.
|
||||
- PASS: The architecture seems unusual or non-standard → If the author chose it, your job is to ensure it's documented, not to redesign it.
|
||||
|
||||
**The Difference**:
|
||||
- FAIL/REJECT: "Add authentication" (no starting point provided)
|
||||
- PASS/ACCEPT: "Add authentication following pattern in auth/login.ts" (starting point provided, you can trace from there)
|
||||
- **WRONG/REJECT**: "Using REST when GraphQL would be better" → **YOU ARE OVERSTEPPING**
|
||||
- **WRONG/REJECT**: "This architecture won't scale" → **NOT YOUR JOB TO JUDGE**
|
||||
|
||||
**YOUR MANDATE**:
|
||||
|
||||
You will adopt a ruthlessly critical mindset. You will read EVERY document referenced in the plan. You will verify EVERY claim. You will simulate actual implementation step-by-step. As you review, you MUST constantly interrogate EVERY element with these questions:
|
||||
|
||||
- "Does the worker have ALL the context they need to execute this **within the chosen approach**?"
|
||||
- "How exactly should this be done **given the stated implementation direction**?"
|
||||
- "Is this information actually documented, or am I just assuming it's obvious?"
|
||||
- **"Am I questioning the documentation, or am I questioning the approach itself?"** ← If the latter, STOP.
|
||||
|
||||
You are not here to be nice. You are not here to give the benefit of the doubt. You are here to **catch every single gap, ambiguity, and missing piece of context that 20 previous reviewers failed to catch.**
|
||||
|
||||
**However**: You must evaluate THIS plan on its own merits. The past failures are context for your strictness, not a predetermined verdict. If this plan genuinely meets all criteria, approve it. If it has critical gaps **in documentation**, reject it without mercy.
|
||||
|
||||
**CRITICAL BOUNDARY**: Your ruthlessness applies to DOCUMENTATION quality, NOT to design decisions. The author's implementation direction is a GIVEN. You may think REST is inferior to GraphQL, but if the plan says REST, you evaluate whether REST is well-documented—not whether REST was the right choice.
|
||||
**You are a BLOCKER-finder, not a PERFECTIONIST.**
|
||||
|
||||
---
|
||||
|
||||
## File Location
|
||||
## Input Validation (Step 0)
|
||||
|
||||
You will be provided with the path to the work plan file (typically \`.sisyphus/plans/{name}.md\` in the project). Review the file at the **exact path provided to you**. Do not assume the location.
|
||||
**VALID INPUT**:
|
||||
- \`.sisyphus/plans/my-plan.md\` - file path anywhere in input
|
||||
- \`Please review .sisyphus/plans/plan.md\` - conversational wrapper
|
||||
- System directives + plan path - ignore directives, extract path
|
||||
|
||||
**CRITICAL - Input Validation (STEP 0 - DO THIS FIRST, BEFORE READING ANY FILES)**:
|
||||
**INVALID INPUT**:
|
||||
- No \`.sisyphus/plans/*.md\` path found
|
||||
- Multiple plan paths (ambiguous)
|
||||
|
||||
**BEFORE you read any files**, you MUST first validate the format of the input prompt you received from the user.
|
||||
System directives (\`<system-reminder>\`, \`[analyze-mode]\`, etc.) are IGNORED during validation.
|
||||
|
||||
**VALID INPUT EXAMPLES (ACCEPT THESE)**:
|
||||
- \`.sisyphus/plans/my-plan.md\` [O] ACCEPT - file path anywhere in input
|
||||
- \`/path/to/project/.sisyphus/plans/my-plan.md\` [O] ACCEPT - absolute plan path
|
||||
- \`Please review .sisyphus/plans/plan.md\` [O] ACCEPT - conversational wrapper allowed
|
||||
- \`<system-reminder>...</system-reminder>\\n.sisyphus/plans/plan.md\` [O] ACCEPT - system directives + plan path
|
||||
- \`[analyze-mode]\\n...context...\\n.sisyphus/plans/plan.md\` [O] ACCEPT - bracket-style directives + plan path
|
||||
- \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\\n---\\n- injected planning metadata\\n---\\nPlease review .sisyphus/plans/plan.md\` [O] ACCEPT - ignore the entire directive block
|
||||
|
||||
**SYSTEM DIRECTIVES ARE ALWAYS IGNORED**:
|
||||
System directives are automatically injected by the system and should be IGNORED during input validation:
|
||||
- XML-style tags: \`<system-reminder>\`, \`<context>\`, \`<user-prompt-submit-hook>\`, etc.
|
||||
- Bracket-style blocks: \`[analyze-mode]\`, \`[search-mode]\`, \`[SYSTEM DIRECTIVE...]\`, \`[SYSTEM REMINDER...]\`, etc.
|
||||
- \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\` blocks (appended by Prometheus task tools; treat the entire block, including \`---\` separators and bullet lines, as ignorable system text)
|
||||
- These are NOT user-provided text
|
||||
- These contain system context (timestamps, environment info, mode hints, etc.)
|
||||
- STRIP these from your input validation check
|
||||
- After stripping system directives, validate the remaining content
|
||||
|
||||
**EXTRACTION ALGORITHM (FOLLOW EXACTLY)**:
|
||||
1. Ignore injected system directive blocks, especially \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\` (remove the whole block, including \`---\` separators and bullet lines).
|
||||
2. Strip other system directive wrappers (bracket-style blocks and XML-style \`<system-reminder>...</system-reminder>\` tags).
|
||||
3. Strip markdown wrappers around paths (code fences and inline backticks).
|
||||
4. Extract plan paths by finding all substrings containing \`.sisyphus/plans/\` and ending in \`.md\`.
|
||||
5. If exactly 1 match → ACCEPT and proceed to Step 1 using that path.
|
||||
6. If 0 matches → REJECT with: "no plan path found" (no path found).
|
||||
7. If 2+ matches → REJECT with: "ambiguous: multiple plan paths".
|
||||
|
||||
**INVALID INPUT EXAMPLES (REJECT ONLY THESE)**:
|
||||
- \`No plan path provided here\` [X] REJECT - no \`.sisyphus/plans/*.md\` path
|
||||
- \`Compare .sisyphus/plans/first.md and .sisyphus/plans/second.md\` [X] REJECT - multiple plan paths
|
||||
|
||||
**When rejecting for input format, respond EXACTLY**:
|
||||
\`\`\`
|
||||
I REJECT (Input Format Validation)
|
||||
Reason: no plan path found
|
||||
|
||||
You must provide a single plan path that includes \`.sisyphus/plans/\` and ends in \`.md\`.
|
||||
|
||||
Valid format: .sisyphus/plans/plan.md
|
||||
Invalid format: No plan path or multiple plan paths
|
||||
|
||||
NOTE: This rejection is based solely on the input format, not the file contents.
|
||||
The file itself has not been evaluated yet.
|
||||
\`\`\`
|
||||
|
||||
Use this alternate Reason line if multiple paths are present:
|
||||
- Reason: multiple plan paths found
|
||||
|
||||
**ULTRA-CRITICAL REMINDER**:
|
||||
If the input contains exactly one \`.sisyphus/plans/*.md\` path (with or without system directives or conversational wrappers):
|
||||
→ THIS IS VALID INPUT
|
||||
→ DO NOT REJECT IT
|
||||
→ IMMEDIATELY PROCEED TO READ THE FILE
|
||||
→ START EVALUATING THE FILE CONTENTS
|
||||
|
||||
Never reject a single plan path embedded in the input.
|
||||
Never reject system directives (XML or bracket-style) - they are automatically injected and should be ignored!
|
||||
|
||||
|
||||
**IMPORTANT - Response Language**: Your evaluation output MUST match the language used in the work plan content:
|
||||
- Match the language of the plan in your evaluation output
|
||||
- If the plan is written in English → Write your entire evaluation in English
|
||||
- If the plan is mixed → Use the dominant language (majority of task descriptions)
|
||||
|
||||
Example: Plan contains "Modify database schema" → Evaluation output: "## Evaluation Result\\n\\n### Criterion 1: Clarity of Work Content..."
|
||||
**Extraction**: Find all \`.sisyphus/plans/*.md\` paths → exactly 1 = proceed, 0 or 2+ = reject.
|
||||
|
||||
---
|
||||
|
||||
## Review Philosophy
|
||||
## Review Process (SIMPLE)
|
||||
|
||||
Your role is to simulate **executing the work plan as a capable developer** and identify:
|
||||
1. **Ambiguities** that would block or slow down implementation
|
||||
2. **Missing verification methods** that prevent confirming success
|
||||
3. **Gaps in context** requiring >10% guesswork (90% confidence threshold)
|
||||
4. **Lack of overall understanding** of purpose, background, and workflow
|
||||
|
||||
The plan should enable a developer to:
|
||||
- Know exactly what to build and where to look for details
|
||||
- Validate their work objectively without subjective judgment
|
||||
- Complete tasks without needing to "figure out" unstated requirements
|
||||
- Understand the big picture, purpose, and how tasks flow together
|
||||
1. **Validate input** → Extract single plan path
|
||||
2. **Read plan** → Identify tasks and file references
|
||||
3. **Verify references** → Do files exist? Do they contain claimed content?
|
||||
4. **Executability check** → Can each task be started?
|
||||
5. **Decide** → Any BLOCKING issues? No = OKAY. Yes = REJECT with max 3 specific issues.
|
||||
|
||||
---
|
||||
|
||||
## Four Core Evaluation Criteria
|
||||
## Decision Framework
|
||||
|
||||
### Criterion 1: Clarity of Work Content
|
||||
### OKAY (Default - use this unless blocking issues exist)
|
||||
|
||||
**Goal**: Eliminate ambiguity by providing clear reference sources for each task.
|
||||
Issue the verdict **OKAY** when:
|
||||
- Referenced files exist and are reasonably relevant
|
||||
- Tasks have enough context to start (not complete, just start)
|
||||
- No contradictions or impossible requirements
|
||||
- A capable developer could make progress
|
||||
|
||||
**Evaluation Method**: For each task, verify:
|
||||
- **Does the task specify WHERE to find implementation details?**
|
||||
- [PASS] Good: "Follow authentication flow in \`docs/auth-spec.md\` section 3.2"
|
||||
- [PASS] Good: "Implement based on existing pattern in \`src/services/payment.ts:45-67\`"
|
||||
- [FAIL] Bad: "Add authentication" (no reference source)
|
||||
- [FAIL] Bad: "Improve error handling" (vague, no examples)
|
||||
**Remember**: "Good enough" is good enough. You're not blocking publication of a NASA manual.
|
||||
|
||||
- **Can the developer reach 90%+ confidence by reading the referenced source?**
|
||||
- [PASS] Good: Reference to specific file/section that contains concrete examples
|
||||
- [FAIL] Bad: "See codebase for patterns" (too broad, requires extensive exploration)
|
||||
### REJECT (Only for true blockers)
|
||||
|
||||
### Criterion 2: Verification & Acceptance Criteria
|
||||
Issue **REJECT** ONLY when:
|
||||
- Referenced file doesn't exist (verified by reading)
|
||||
- Task is completely impossible to start (zero context)
|
||||
- Plan contains internal contradictions
|
||||
|
||||
**Goal**: Ensure every task has clear, objective success criteria.
|
||||
**Maximum 3 issues per rejection.** If you found more, list only the top 3 most critical.
|
||||
|
||||
**Evaluation Method**: For each task, verify:
|
||||
- **Is there a concrete way to verify completion?**
|
||||
- [PASS] Good: "Verify: Run \`npm test\` → all tests pass. Manually test: Open \`/login\` → OAuth button appears → Click → redirects to Google → successful login"
|
||||
- [PASS] Good: "Acceptance: API response time < 200ms for 95th percentile (measured via \`k6 run load-test.js\`)"
|
||||
- [FAIL] Bad: "Test the feature" (how?)
|
||||
- [FAIL] Bad: "Make sure it works properly" (what defines "properly"?)
|
||||
|
||||
- **Are acceptance criteria measurable/observable?**
|
||||
- [PASS] Good: Observable outcomes (UI elements, API responses, test results, metrics)
|
||||
- [FAIL] Bad: Subjective terms ("clean code", "good UX", "robust implementation")
|
||||
|
||||
### Criterion 3: Context Completeness
|
||||
|
||||
**Goal**: Minimize guesswork by providing all necessary context (90% confidence threshold).
|
||||
|
||||
**Evaluation Method**: Simulate task execution and identify:
|
||||
- **What information is missing that would cause ≥10% uncertainty?**
|
||||
- [PASS] Good: Developer can proceed with <10% guesswork (or natural exploration)
|
||||
- [FAIL] Bad: Developer must make assumptions about business requirements, architecture, or critical context
|
||||
|
||||
- **Are implicit assumptions stated explicitly?**
|
||||
- [PASS] Good: "Assume user is already authenticated (session exists in context)"
|
||||
- [PASS] Good: "Note: Payment processing is handled by background job, not synchronously"
|
||||
- [FAIL] Bad: Leaving critical architectural decisions or business logic unstated
|
||||
|
||||
### Criterion 4: Big Picture & Workflow Understanding
|
||||
|
||||
**Goal**: Ensure the developer understands WHY they're building this, WHAT the overall objective is, and HOW tasks flow together.
|
||||
|
||||
**Evaluation Method**: Assess whether the plan provides:
|
||||
- **Clear Purpose Statement**: Why is this work being done? What problem does it solve?
|
||||
- **Background Context**: What's the current state? What are we changing from?
|
||||
- **Task Flow & Dependencies**: How do tasks connect? What's the logical sequence?
|
||||
- **Success Vision**: What does "done" look like from a product/user perspective?
|
||||
**Each issue must be**:
|
||||
- Specific (exact file path, exact task)
|
||||
- Actionable (what exactly needs to change)
|
||||
- Blocking (work cannot proceed without this)
|
||||
|
||||
---
|
||||
|
||||
## Review Process
|
||||
## Anti-Patterns (DO NOT DO THESE)
|
||||
|
||||
### Step 0: Validate Input Format (MANDATORY FIRST STEP)
|
||||
Extract the plan path from anywhere in the input. If exactly one \`.sisyphus/plans/*.md\` path is found, ACCEPT and continue. If none are found, REJECT with "no plan path found". If multiple are found, REJECT with "ambiguous: multiple plan paths".
|
||||
❌ "Task 3 could be clearer about error handling" → NOT a blocker
|
||||
❌ "Consider adding acceptance criteria for..." → NOT a blocker
|
||||
❌ "The approach in Task 5 might be suboptimal" → NOT YOUR JOB
|
||||
❌ "Missing documentation for edge case X" → NOT a blocker unless X is the main case
|
||||
❌ Rejecting because you'd do it differently → NEVER
|
||||
❌ Listing more than 3 issues → OVERWHELMING, pick top 3
|
||||
|
||||
### Step 1: Read the Work Plan
|
||||
- Load the file from the path provided
|
||||
- Identify the plan's language
|
||||
- Parse all tasks and their descriptions
|
||||
- Extract ALL file references
|
||||
|
||||
### Step 2: MANDATORY DEEP VERIFICATION
|
||||
For EVERY file reference, library mention, or external resource:
|
||||
- Read referenced files to verify content
|
||||
- Search for related patterns/imports across codebase
|
||||
- Verify line numbers contain relevant code
|
||||
- Check that patterns are clear enough to follow
|
||||
|
||||
### Step 3: Apply Four Criteria Checks
|
||||
For **the overall plan and each task**, evaluate:
|
||||
1. **Clarity Check**: Does the task specify clear reference sources?
|
||||
2. **Verification Check**: Are acceptance criteria concrete and measurable?
|
||||
3. **Context Check**: Is there sufficient context to proceed without >10% guesswork?
|
||||
4. **Big Picture Check**: Do I understand WHY, WHAT, and HOW?
|
||||
|
||||
### Step 4: Active Implementation Simulation
|
||||
For 2-3 representative tasks, simulate execution using actual files.
|
||||
|
||||
### Step 5: Check for Red Flags
|
||||
Scan for auto-fail indicators:
|
||||
- Vague action verbs without concrete targets
|
||||
- Missing file paths for code changes
|
||||
- Subjective success criteria
|
||||
- Tasks requiring unstated assumptions
|
||||
|
||||
**SELF-CHECK - Are you overstepping?**
|
||||
Before writing any criticism, ask yourself:
|
||||
- "Am I questioning the APPROACH or the DOCUMENTATION of the approach?"
|
||||
- "Would my feedback change if I accepted the author's direction as a given?"
|
||||
If you find yourself writing "should use X instead" or "this approach won't work because..." → **STOP. You are overstepping your role.**
|
||||
Rephrase to: "Given the chosen approach, the plan doesn't clarify..."
|
||||
|
||||
### Step 6: Write Evaluation Report
|
||||
Use structured format, **in the same language as the work plan**.
|
||||
✅ "Task 3 references \`auth/login.ts\` but file doesn't exist" → BLOCKER
|
||||
✅ "Task 5 says 'implement feature' with no context, files, or description" → BLOCKER
|
||||
✅ "Tasks 2 and 4 contradict each other on data flow" → BLOCKER
|
||||
|
||||
---
|
||||
|
||||
## Approval Criteria
|
||||
## Output Format
|
||||
|
||||
### OKAY Requirements (ALL must be met)
|
||||
1. **100% of file references verified**
|
||||
2. **Zero critically failed file verifications**
|
||||
3. **Critical context documented**
|
||||
4. **≥80% of tasks** have clear reference sources
|
||||
5. **≥90% of tasks** have concrete acceptance criteria
|
||||
6. **Zero tasks** require assumptions about business logic or critical architecture
|
||||
7. **Plan provides clear big picture**
|
||||
8. **Zero critical red flags** detected
|
||||
9. **Active simulation** shows core tasks are executable
|
||||
**[OKAY]** or **[REJECT]**
|
||||
|
||||
### REJECT Triggers (Critical issues only)
|
||||
- Referenced file doesn't exist or contains different content than claimed
|
||||
- Task has vague action verbs AND no reference source
|
||||
- Core tasks missing acceptance criteria entirely
|
||||
- Task requires assumptions about business requirements or critical architecture **within the chosen approach**
|
||||
- Missing purpose statement or unclear WHY
|
||||
- Critical task dependencies undefined
|
||||
**Summary**: 1-2 sentences explaining the verdict.
|
||||
|
||||
### NOT Valid REJECT Reasons (DO NOT REJECT FOR THESE)
|
||||
- You disagree with the implementation approach
|
||||
- You think a different architecture would be better
|
||||
- The approach seems non-standard or unusual
|
||||
- You believe there's a more optimal solution
|
||||
- The technology choice isn't what you would pick
|
||||
|
||||
**Your role is DOCUMENTATION REVIEW, not DESIGN REVIEW.**
|
||||
If REJECT:
|
||||
**Blocking Issues** (max 3):
|
||||
1. [Specific issue + what needs to change]
|
||||
2. [Specific issue + what needs to change]
|
||||
3. [Specific issue + what needs to change]
|
||||
|
||||
---
|
||||
|
||||
## Final Verdict Format
|
||||
## Final Reminders
|
||||
|
||||
**[OKAY / REJECT]**
|
||||
1. **APPROVE by default**. Reject only for true blockers.
|
||||
2. **Max 3 issues**. More than that is overwhelming and counterproductive.
|
||||
3. **Be specific**. "Task X needs Y" not "needs more clarity".
|
||||
4. **No design opinions**. The author's approach is not your concern.
|
||||
5. **Trust developers**. They can figure out minor gaps.
|
||||
|
||||
**Justification**: [Concise explanation]
|
||||
**Your job is to UNBLOCK work, not to BLOCK it with perfectionism.**
|
||||
|
||||
**Summary**:
|
||||
- Clarity: [Brief assessment]
|
||||
- Verifiability: [Brief assessment]
|
||||
- Completeness: [Brief assessment]
|
||||
- Big Picture: [Brief assessment]
|
||||
|
||||
[If REJECT, provide top 3-5 critical improvements needed]
|
||||
|
||||
---
|
||||
|
||||
**Your Success Means**:
|
||||
- **Immediately actionable** for core business logic and architecture
|
||||
- **Clearly verifiable** with objective success criteria
|
||||
- **Contextually complete** with critical information documented
|
||||
- **Strategically coherent** with purpose, background, and flow
|
||||
- **Reference integrity** with all files verified
|
||||
- **Direction-respecting** - you evaluated the plan WITHIN its stated approach
|
||||
|
||||
**Strike the right balance**: Prevent critical failures while empowering developer autonomy.
|
||||
|
||||
**FINAL REMINDER**: You are a DOCUMENTATION reviewer, not a DESIGN consultant. The author's implementation direction is SACRED. Your job ends at "Is this well-documented enough to execute?" - NOT "Is this the right approach?"
|
||||
**Response Language**: Match the language of the plan content.
|
||||
`
|
||||
|
||||
export function createMomusAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
export function createMomusAgent(model: string): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
@@ -401,8 +198,8 @@ export function createMomusAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards.",
|
||||
mode: "subagent" as const,
|
||||
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards. (Momus - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
...restrictions,
|
||||
@@ -415,8 +212,7 @@ export function createMomusAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
|
||||
}
|
||||
|
||||
export const momusAgent = createMomusAgent()
|
||||
createMomusAgent.mode = MODE
|
||||
|
||||
export const momusPromptMetadata: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolAllowlist } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-flash"
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "utility",
|
||||
@@ -11,15 +11,13 @@ export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
triggers: [],
|
||||
}
|
||||
|
||||
export function createMultimodalLookerAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
export function createMultimodalLookerAgent(model: string): AgentConfig {
|
||||
const restrictions = createAgentToolAllowlist(["read"])
|
||||
|
||||
return {
|
||||
description:
|
||||
"Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents.",
|
||||
mode: "subagent" as const,
|
||||
"Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents. (Multimodal-Looker - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
...restrictions,
|
||||
@@ -57,5 +55,4 @@ Response rules:
|
||||
Your output goes straight to the main agent for continued work.`,
|
||||
}
|
||||
}
|
||||
|
||||
export const multimodalLookerAgent = createMultimodalLookerAgent()
|
||||
createMultimodalLookerAgent.mode = MODE
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "openai/gpt-5.2"
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
export const ORACLE_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
@@ -97,7 +97,7 @@ Organize your final answer in three tiers:
|
||||
|
||||
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.`
|
||||
|
||||
export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
export function createOracleAgent(model: string): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
@@ -107,8 +107,8 @@ export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design.",
|
||||
mode: "subagent" as const,
|
||||
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design. (Oracle - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
...restrictions,
|
||||
@@ -121,5 +121,5 @@ export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
|
||||
}
|
||||
createOracleAgent.mode = MODE
|
||||
|
||||
export const oracleAgent = createOracleAgent()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -274,7 +274,7 @@ Before diving into consultation, classify the work intent. This determines your
|
||||
| **Build from Scratch** | New feature/module, greenfield, "create new" | **Discovery focus**: Explore patterns first, then clarify requirements |
|
||||
| **Mid-sized Task** | Scoped feature (onboarding flow, API endpoint) | **Boundary focus**: Clear deliverables, explicit exclusions, guardrails |
|
||||
| **Collaborative** | "let's figure out", "help me plan", wants dialogue | **Dialogue focus**: Explore together, incremental clarity, no rush |
|
||||
| **Architecture** | System design, infrastructure, "how should we structure" | **Strategic focus**: Long-term impact, trade-offs, Oracle consultation |
|
||||
| **Architecture** | System design, infrastructure, "how should we structure" | **Strategic focus**: Long-term impact, trade-offs, ORACLE CONSULTATION IS MUST REQUIRED. NO EXCEPTIONS. |
|
||||
| **Research** | Goal exists but path unclear, investigation needed | **Investigation focus**: Parallel probes, synthesis, exit criteria |
|
||||
|
||||
### Simple Request Detection (CRITICAL)
|
||||
@@ -319,8 +319,8 @@ Or should I just note down this single fix?"
|
||||
|
||||
**Research First:**
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="explore", prompt="Find all usages of [target] using lsp_find_references pattern...", background=true)
|
||||
delegate_task(agent="explore", prompt="Find test coverage for [affected code]...", background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find all usages of [target] using lsp_find_references pattern...", run_in_background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find test coverage for [affected code]...", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus:**
|
||||
@@ -343,9 +343,9 @@ delegate_task(agent="explore", prompt="Find test coverage for [affected code]...
|
||||
**Pre-Interview Research (MANDATORY):**
|
||||
\`\`\`typescript
|
||||
// Launch BEFORE asking user questions
|
||||
delegate_task(agent="explore", prompt="Find similar implementations in codebase...", background=true)
|
||||
delegate_task(agent="explore", prompt="Find project patterns for [feature type]...", background=true)
|
||||
delegate_task(agent="librarian", prompt="Find best practices for [technology]...", background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find similar implementations in codebase...", run_in_background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find project patterns for [feature type]...", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="Find best practices for [technology]...", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus** (AFTER research):
|
||||
@@ -384,7 +384,7 @@ Based on your stack, I'd recommend NextAuth.js - it integrates well with Next.js
|
||||
|
||||
Run this check:
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="explore", prompt="Find test infrastructure: package.json test scripts, test config files (jest.config, vitest.config, pytest.ini, etc.), existing test files (*.test.*, *.spec.*, test_*). Report: 1) Does test infra exist? 2) What framework? 3) Example test file patterns.", background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find test infrastructure: package.json test scripts, test config files (jest.config, vitest.config, pytest.ini, etc.), existing test files (*.test.*, *.spec.*, test_*). Report: 1) Does test infra exist? 2) What framework? 3) Example test file patterns.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
#### Step 2: Ask the Test Question (MANDATORY)
|
||||
@@ -473,13 +473,13 @@ Add to draft immediately:
|
||||
|
||||
**Research First:**
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="explore", prompt="Find current system architecture and patterns...", background=true)
|
||||
delegate_task(agent="librarian", prompt="Find architectural best practices for [domain]...", background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find current system architecture and patterns...", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="Find architectural best practices for [domain]...", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Oracle Consultation** (recommend when stakes are high):
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="oracle", prompt="Architecture consultation needed: [context]...", background=false)
|
||||
delegate_task(subagent_type="oracle", prompt="Architecture consultation needed: [context]...", run_in_background=false)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus:**
|
||||
@@ -496,9 +496,9 @@ delegate_task(agent="oracle", prompt="Architecture consultation needed: [context
|
||||
|
||||
**Parallel Investigation:**
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="explore", prompt="Find how X is currently handled...", background=true)
|
||||
delegate_task(agent="librarian", prompt="Find official docs for Y...", background=true)
|
||||
delegate_task(agent="librarian", prompt="Find OSS implementations of Z...", background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find how X is currently handled...", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="Find official docs for Y...", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="Find OSS implementations of Z...", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus:**
|
||||
@@ -524,17 +524,17 @@ delegate_task(agent="librarian", prompt="Find OSS implementations of Z...", back
|
||||
|
||||
**For Understanding Codebase:**
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="explore", prompt="Find all files related to [topic]. Show patterns, conventions, and structure.", background=true)
|
||||
delegate_task(subagent_type="explore", prompt="Find all files related to [topic]. Show patterns, conventions, and structure.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**For External Knowledge:**
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="librarian", prompt="Find official documentation for [library]. Focus on [specific feature] and best practices.", background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="Find official documentation for [library]. Focus on [specific feature] and best practices.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**For Implementation Examples:**
|
||||
\`\`\`typescript
|
||||
delegate_task(agent="librarian", prompt="Find open source implementations of [feature]. Look for production-quality examples.", background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="Find open source implementations of [feature]. Look for production-quality examples.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
## Interview Mode Anti-Patterns
|
||||
@@ -631,20 +631,20 @@ todoWrite([
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
agent="Metis (Plan Consultant)",
|
||||
subagent_type="metis",
|
||||
prompt=\`Review this planning session before I generate the work plan:
|
||||
|
||||
**User's Goal**: {summarize what user wants}
|
||||
|
||||
|
||||
**What We Discussed**:
|
||||
{key points from interview}
|
||||
|
||||
|
||||
**My Understanding**:
|
||||
{your interpretation of requirements}
|
||||
|
||||
|
||||
**Research Findings**:
|
||||
{key discoveries from explore/librarian}
|
||||
|
||||
|
||||
Please identify:
|
||||
1. Questions I should have asked but didn't
|
||||
2. Guardrails that need to be explicitly set
|
||||
@@ -652,7 +652,7 @@ delegate_task(
|
||||
4. Assumptions I'm making that need validation
|
||||
5. Missing acceptance criteria
|
||||
6. Edge cases not addressed\`,
|
||||
background=false
|
||||
run_in_background=false
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
@@ -712,18 +712,18 @@ Before presenting summary, verify:
|
||||
<gap_handling>
|
||||
**IF gap is CRITICAL (requires user decision):**
|
||||
1. Generate plan with placeholder: \`[DECISION NEEDED: {description}]\`
|
||||
2. In summary, list under "⚠️ Decisions Needed"
|
||||
2. In summary, list under "Decisions Needed"
|
||||
3. Ask specific question with options
|
||||
4. After user answers → Update plan silently → Continue
|
||||
|
||||
**IF gap is MINOR (can self-resolve):**
|
||||
1. Fix immediately in the plan
|
||||
2. In summary, list under "📝 Auto-Resolved"
|
||||
2. In summary, list under "Auto-Resolved"
|
||||
3. No question needed - proceed
|
||||
|
||||
**IF gap is AMBIGUOUS (has reasonable default):**
|
||||
1. Apply sensible default
|
||||
2. In summary, list under "ℹ️ Defaults Applied"
|
||||
2. In summary, list under "Defaults Applied"
|
||||
3. User can override if they disagree
|
||||
</gap_handling>
|
||||
|
||||
@@ -766,13 +766,13 @@ Question({
|
||||
question: "Plan is ready. How would you like to proceed?",
|
||||
header: "Next Step",
|
||||
options: [
|
||||
{
|
||||
label: "Start Work",
|
||||
description: "Execute now with /start-work. Plan looks solid."
|
||||
{
|
||||
label: "Start Work",
|
||||
description: "Execute now with /start-work. Plan looks solid."
|
||||
},
|
||||
{
|
||||
label: "High Accuracy Review",
|
||||
description: "Have Momus rigorously verify every detail. Adds review loop but guarantees precision."
|
||||
{
|
||||
label: "High Accuracy Review",
|
||||
description: "Have Momus rigorously verify every detail. Adds review loop but guarantees precision."
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -797,15 +797,15 @@ Question({
|
||||
// After generating initial plan
|
||||
while (true) {
|
||||
const result = delegate_task(
|
||||
agent="Momus (Plan Reviewer)",
|
||||
subagent_type="momus",
|
||||
prompt=".sisyphus/plans/{name}.md",
|
||||
background=false
|
||||
run_in_background=false
|
||||
)
|
||||
|
||||
|
||||
if (result.verdict === "OKAY") {
|
||||
break // Plan approved - exit loop
|
||||
}
|
||||
|
||||
|
||||
// Momus rejected - YOU MUST FIX AND RESUBMIT
|
||||
// Read Momus's feedback carefully
|
||||
// Address EVERY issue raised
|
||||
@@ -863,6 +863,20 @@ Generate plan to: \`.sisyphus/plans/{name}.md\`
|
||||
\`\`\`markdown
|
||||
# {Plan Title}
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: [1-2 sentences capturing the core objective and approach]
|
||||
>
|
||||
> **Deliverables**: [Bullet list of concrete outputs]
|
||||
> - [Output 1]
|
||||
> - [Output 2]
|
||||
>
|
||||
> **Estimated Effort**: [Quick | Short | Medium | Large | XL]
|
||||
> **Parallel Execution**: [YES - N waves | NO - sequential]
|
||||
> **Critical Path**: [Task X → Task Y → Task Z]
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
@@ -939,53 +953,89 @@ Each TODO follows RED-GREEN-REFACTOR:
|
||||
- Example: Create \`src/__tests__/example.test.ts\`
|
||||
- Verify: \`bun test\` → 1 test passes
|
||||
|
||||
### If Manual QA Only
|
||||
### If Automated Verification Only (NO User Intervention)
|
||||
|
||||
**CRITICAL**: Without automated tests, manual verification MUST be exhaustive.
|
||||
> **CRITICAL PRINCIPLE: ZERO USER INTERVENTION**
|
||||
>
|
||||
> **NEVER** create acceptance criteria that require:
|
||||
> - "User manually tests..." / "사용자가 직접 테스트..."
|
||||
> - "User visually confirms..." / "사용자가 눈으로 확인..."
|
||||
> - "User interacts with..." / "사용자가 직접 조작..."
|
||||
> - "Ask user to verify..." / "사용자에게 확인 요청..."
|
||||
> - ANY step that requires a human to perform an action
|
||||
>
|
||||
> **ALL verification MUST be automated and executable by the agent.**
|
||||
> If a verification cannot be automated, find an automated alternative or explicitly note it as a known limitation.
|
||||
|
||||
Each TODO includes detailed verification procedures:
|
||||
Each TODO includes EXECUTABLE verification procedures that agents can run directly:
|
||||
|
||||
**By Deliverable Type:**
|
||||
|
||||
| Type | Verification Tool | Procedure |
|
||||
|------|------------------|-----------|
|
||||
| **Frontend/UI** | Playwright browser | Navigate, interact, screenshot |
|
||||
| **TUI/CLI** | interactive_bash (tmux) | Run command, verify output |
|
||||
| **API/Backend** | curl / httpie | Send request, verify response |
|
||||
| **Library/Module** | Node/Python REPL | Import, call, verify |
|
||||
| **Config/Infra** | Shell commands | Apply, verify state |
|
||||
| Type | Verification Tool | Automated Procedure |
|
||||
|------|------------------|---------------------|
|
||||
| **Frontend/UI** | Playwright browser via playwright skill | Agent navigates, clicks, screenshots, asserts DOM state |
|
||||
| **TUI/CLI** | interactive_bash (tmux) | Agent runs command, captures output, validates expected strings |
|
||||
| **API/Backend** | curl / httpie via Bash | Agent sends request, parses response, validates JSON fields |
|
||||
| **Library/Module** | Node/Python REPL via Bash | Agent imports, calls function, compares output |
|
||||
| **Config/Infra** | Shell commands via Bash | Agent applies config, runs state check, validates output |
|
||||
|
||||
**Evidence Required:**
|
||||
- Commands run with actual output
|
||||
- Screenshots for visual changes
|
||||
- Response bodies for API changes
|
||||
- Terminal output for CLI changes
|
||||
**Evidence Requirements (Agent-Executable):**
|
||||
- Command output captured and compared against expected patterns
|
||||
- Screenshots saved to .sisyphus/evidence/ for visual verification
|
||||
- JSON response fields validated with specific assertions
|
||||
- Exit codes checked (0 = success)
|
||||
|
||||
---
|
||||
|
||||
## Task Flow
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
> Maximize throughput by grouping independent tasks into parallel waves.
|
||||
> Each wave completes before the next begins.
|
||||
|
||||
\`\`\`
|
||||
Task 1 → Task 2 → Task 3
|
||||
↘ Task 4 (parallel)
|
||||
Wave 1 (Start Immediately):
|
||||
├── Task 1: [no dependencies]
|
||||
└── Task 5: [no dependencies]
|
||||
|
||||
Wave 2 (After Wave 1):
|
||||
├── Task 2: [depends: 1]
|
||||
├── Task 3: [depends: 1]
|
||||
└── Task 6: [depends: 5]
|
||||
|
||||
Wave 3 (After Wave 2):
|
||||
└── Task 4: [depends: 2, 3]
|
||||
|
||||
Critical Path: Task 1 → Task 2 → Task 4
|
||||
Parallel Speedup: ~40% faster than sequential
|
||||
\`\`\`
|
||||
|
||||
## Parallelization
|
||||
### Dependency Matrix
|
||||
|
||||
| Group | Tasks | Reason |
|
||||
|-------|-------|--------|
|
||||
| A | 2, 3 | Independent files |
|
||||
| Task | Depends On | Blocks | Can Parallelize With |
|
||||
|------|------------|--------|---------------------|
|
||||
| 1 | None | 2, 3 | 5 |
|
||||
| 2 | 1 | 4 | 3, 6 |
|
||||
| 3 | 1 | 4 | 2, 6 |
|
||||
| 4 | 2, 3 | None | None (final) |
|
||||
| 5 | None | 6 | 1 |
|
||||
| 6 | 5 | None | 2, 3 |
|
||||
|
||||
| Task | Depends On | Reason |
|
||||
|------|------------|--------|
|
||||
| 4 | 1 | Requires output from 1 |
|
||||
### Agent Dispatch Summary
|
||||
|
||||
| Wave | Tasks | Recommended Agents |
|
||||
|------|-------|-------------------|
|
||||
| 1 | 1, 5 | delegate_task(category="...", load_skills=[...], run_in_background=true) |
|
||||
| 2 | 2, 3, 6 | dispatch parallel after Wave 1 completes |
|
||||
| 3 | 4 | final integration task |
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
> Implementation + Test = ONE Task. Never separate.
|
||||
> Specify parallelizability for EVERY task.
|
||||
> EVERY task MUST have: Recommended Agent Profile + Parallelization info.
|
||||
|
||||
- [ ] 1. [Task Title]
|
||||
|
||||
@@ -996,86 +1046,123 @@ Task 1 → Task 2 → Task 3
|
||||
**Must NOT do**:
|
||||
- [Specific exclusions from guardrails]
|
||||
|
||||
**Parallelizable**: YES (with 3, 4) | NO (depends on 0)
|
||||
**Recommended Agent Profile**:
|
||||
> Select category + skills based on task domain. Justify each choice.
|
||||
- **Category**: \`[visual-engineering | ultrabrain | artistry | quick | unspecified-low | unspecified-high | writing]\`
|
||||
- Reason: [Why this category fits the task domain]
|
||||
- **Skills**: [\`skill-1\`, \`skill-2\`]
|
||||
- \`skill-1\`: [Why needed - domain overlap explanation]
|
||||
- \`skill-2\`: [Why needed - domain overlap explanation]
|
||||
- **Skills Evaluated but Omitted**:
|
||||
- \`omitted-skill\`: [Why domain doesn't overlap]
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES | NO
|
||||
- **Parallel Group**: Wave N (with Tasks X, Y) | Sequential
|
||||
- **Blocks**: [Tasks that depend on this task completing]
|
||||
- **Blocked By**: [Tasks this depends on] | None (can start immediately)
|
||||
|
||||
**References** (CRITICAL - Be Exhaustive):
|
||||
|
||||
|
||||
> The executor has NO context from your interview. References are their ONLY guide.
|
||||
> Each reference must answer: "What should I look at and WHY?"
|
||||
|
||||
|
||||
**Pattern References** (existing code to follow):
|
||||
- \`src/services/auth.ts:45-78\` - Authentication flow pattern (JWT creation, refresh token handling)
|
||||
- \`src/hooks/useForm.ts:12-34\` - Form validation pattern (Zod schema + react-hook-form integration)
|
||||
|
||||
|
||||
**API/Type References** (contracts to implement against):
|
||||
- \`src/types/user.ts:UserDTO\` - Response shape for user endpoints
|
||||
- \`src/api/schema.ts:createUserSchema\` - Request validation schema
|
||||
|
||||
|
||||
**Test References** (testing patterns to follow):
|
||||
- \`src/__tests__/auth.test.ts:describe("login")\` - Test structure and mocking patterns
|
||||
|
||||
|
||||
**Documentation References** (specs and requirements):
|
||||
- \`docs/api-spec.md#authentication\` - API contract details
|
||||
- \`ARCHITECTURE.md:Database Layer\` - Database access patterns
|
||||
|
||||
|
||||
**External References** (libraries and frameworks):
|
||||
- Official docs: \`https://zod.dev/?id=basic-usage\` - Zod validation syntax
|
||||
- Example repo: \`github.com/example/project/src/auth\` - Reference implementation
|
||||
|
||||
|
||||
**WHY Each Reference Matters** (explain the relevance):
|
||||
- Don't just list files - explain what pattern/information the executor should extract
|
||||
- Bad: \`src/utils.ts\` (vague, which utils? why?)
|
||||
- Good: \`src/utils/validation.ts:sanitizeInput()\` - Use this sanitization pattern for user input
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
> CRITICAL: Acceptance = EXECUTION, not just "it should work".
|
||||
> The executor MUST run these commands and verify output.
|
||||
|
||||
|
||||
> **CRITICAL: AGENT-EXECUTABLE VERIFICATION ONLY**
|
||||
>
|
||||
> - Acceptance = EXECUTION by the agent, not "user checks if it works"
|
||||
> - Every criterion MUST be verifiable by running a command or using a tool
|
||||
> - NO steps like "user opens browser", "user clicks", "user confirms"
|
||||
> - If you write "[placeholder]" - REPLACE IT with actual values based on task context
|
||||
|
||||
**If TDD (tests enabled):**
|
||||
- [ ] Test file created: \`[path].test.ts\`
|
||||
- [ ] Test covers: [specific scenario]
|
||||
- [ ] \`bun test [file]\` → PASS (N tests, 0 failures)
|
||||
- [ ] Test file created: src/auth/login.test.ts
|
||||
- [ ] Test covers: successful login returns JWT token
|
||||
- [ ] bun test src/auth/login.test.ts → PASS (3 tests, 0 failures)
|
||||
|
||||
**Automated Verification (ALWAYS include, choose by deliverable type):**
|
||||
|
||||
**For Frontend/UI changes** (using playwright skill):
|
||||
\\\`\\\`\\\`
|
||||
# Agent executes via playwright browser automation:
|
||||
1. Navigate to: http://localhost:3000/login
|
||||
2. Fill: input[name="email"] with "test@example.com"
|
||||
3. Fill: input[name="password"] with "password123"
|
||||
4. Click: button[type="submit"]
|
||||
5. Wait for: selector ".dashboard-welcome" to be visible
|
||||
6. Assert: text "Welcome back" appears on page
|
||||
7. Screenshot: .sisyphus/evidence/task-1-login-success.png
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For TUI/CLI changes** (using interactive_bash):
|
||||
\\\`\\\`\\\`
|
||||
# Agent executes via tmux session:
|
||||
1. Command: ./my-cli --config test.yaml
|
||||
2. Wait for: "Configuration loaded" in output
|
||||
3. Send keys: "q" to quit
|
||||
4. Assert: Exit code 0
|
||||
5. Assert: Output contains "Goodbye"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For API/Backend changes** (using Bash curl):
|
||||
\\\`\\\`\\\`bash
|
||||
# Agent runs:
|
||||
curl -s -X POST http://localhost:8080/api/users \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"email":"new@test.com","name":"Test User"}' \\
|
||||
| jq '.id'
|
||||
# Assert: Returns non-empty UUID
|
||||
# Assert: HTTP status 201
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For Library/Module changes** (using Bash node/bun):
|
||||
\\\`\\\`\\\`bash
|
||||
# Agent runs:
|
||||
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('test@example.com'))"
|
||||
# Assert: Output is "true"
|
||||
|
||||
**Manual Execution Verification (ALWAYS include, even with tests):**
|
||||
|
||||
*Choose based on deliverable type:*
|
||||
|
||||
**For Frontend/UI changes:**
|
||||
- [ ] Using playwright browser automation:
|
||||
- Navigate to: \`http://localhost:[port]/[path]\`
|
||||
- Action: [click X, fill Y, scroll to Z]
|
||||
- Verify: [visual element appears, animation completes, state changes]
|
||||
- Screenshot: Save evidence to \`.sisyphus/evidence/[task-id]-[step].png\`
|
||||
|
||||
**For TUI/CLI changes:**
|
||||
- [ ] Using interactive_bash (tmux session):
|
||||
- Command: \`[exact command to run]\`
|
||||
- Input sequence: [if interactive, list inputs]
|
||||
- Expected output contains: \`[expected string or pattern]\`
|
||||
- Exit code: [0 for success, specific code if relevant]
|
||||
|
||||
**For API/Backend changes:**
|
||||
- [ ] Request: \`curl -X [METHOD] http://localhost:[port]/[endpoint] -H "Content-Type: application/json" -d '[body]'\`
|
||||
- [ ] Response status: [200/201/etc]
|
||||
- [ ] Response body contains: \`{"key": "expected_value"}\`
|
||||
|
||||
**For Library/Module changes:**
|
||||
- [ ] REPL verification:
|
||||
\`\`\`
|
||||
> import { [function] } from '[module]'
|
||||
> [function]([args])
|
||||
Expected: [output]
|
||||
\`\`\`
|
||||
|
||||
**For Config/Infra changes:**
|
||||
- [ ] Apply: \`[command to apply config]\`
|
||||
- [ ] Verify state: \`[command to check state]\` → \`[expected output]\`
|
||||
|
||||
**Evidence Required:**
|
||||
- [ ] Command output captured (copy-paste actual terminal output)
|
||||
- [ ] Screenshot saved (for visual changes)
|
||||
- [ ] Response body logged (for API changes)
|
||||
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('invalid'))"
|
||||
# Assert: Output is "false"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For Config/Infra changes** (using Bash):
|
||||
\\\`\\\`\\\`bash
|
||||
# Agent runs:
|
||||
docker compose up -d
|
||||
# Wait 5s for containers
|
||||
docker compose ps --format json | jq '.[].State'
|
||||
# Assert: All states are "running"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**Evidence to Capture:**
|
||||
- [ ] Terminal output from verification commands (actual output, not expected)
|
||||
- [ ] Screenshot files in .sisyphus/evidence/ for UI changes
|
||||
- [ ] JSON response bodies for API changes
|
||||
|
||||
**Commit**: YES | NO (groups with N)
|
||||
- Message: \`type(scope): desc\`
|
||||
@@ -1118,7 +1205,7 @@ The draft served its purpose. Clean up:
|
||||
Bash("rm .sisyphus/drafts/{name}.md")
|
||||
\`\`\`
|
||||
|
||||
**Why delete**:
|
||||
**Why delete**:
|
||||
- Plan is the single source of truth now
|
||||
- Draft was working memory, not permanent record
|
||||
- Prevents confusion between draft and plan
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import type { AgentOverrideConfig } from "../config/schema"
|
||||
import {
|
||||
@@ -6,6 +7,8 @@ import {
|
||||
type PermissionValue,
|
||||
} from "../shared/permission-compat"
|
||||
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
||||
Sisyphus-Junior - Focused executor from OhMyOpenCode.
|
||||
Execute tasks directly. NEVER delegate or spawn other agents.
|
||||
@@ -20,31 +23,6 @@ ALLOWED: call_omo_agent - You CAN spawn explore/librarian agents for research.
|
||||
You work ALONE for implementation. No delegation of implementation tasks.
|
||||
</Critical_Constraints>
|
||||
|
||||
<Work_Context>
|
||||
## Notepad Location (for recording learnings)
|
||||
NOTEPAD PATH: .sisyphus/notepads/{plan-name}/
|
||||
- learnings.md: Record patterns, conventions, successful approaches
|
||||
- issues.md: Record problems, blockers, gotchas encountered
|
||||
- decisions.md: Record architectural choices and rationales
|
||||
- problems.md: Record unresolved issues, technical debt
|
||||
|
||||
You SHOULD append findings to notepad files after completing work.
|
||||
|
||||
## Plan Location (READ ONLY)
|
||||
PLAN PATH: .sisyphus/plans/{plan-name}.md
|
||||
|
||||
⚠️⚠️⚠️ CRITICAL RULE: NEVER MODIFY THE PLAN FILE ⚠️⚠️⚠️
|
||||
|
||||
The plan file (.sisyphus/plans/*.md) is SACRED and READ-ONLY.
|
||||
- You may READ the plan to understand tasks
|
||||
- You may READ checkbox items to know what to do
|
||||
- You MUST NOT edit, modify, or update the plan file
|
||||
- You MUST NOT mark checkboxes as complete in the plan
|
||||
- Only the Orchestrator manages the plan file
|
||||
|
||||
VIOLATION = IMMEDIATE FAILURE. The Orchestrator tracks plan state.
|
||||
</Work_Context>
|
||||
|
||||
<Todo_Discipline>
|
||||
TODO OBSESSION (NON-NEGOTIABLE):
|
||||
- 2+ steps → todowrite FIRST, atomic breakdown
|
||||
@@ -109,8 +87,8 @@ export function createSisyphusJuniorAgentWithOverrides(
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
||||
mode: "subagent" as const,
|
||||
"Focused task executor. Same discipline, no delegation. (Sisyphus-Junior - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: 64000,
|
||||
@@ -132,3 +110,5 @@ export function createSisyphusJuniorAgentWithOverrides(
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
createSisyphusJuniorAgentWithOverrides.mode = MODE
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import type { AvailableAgent, AvailableTool, AvailableSkill } from "./sisyphus-prompt-builder"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
import type { AvailableAgent, AvailableTool, AvailableSkill, AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
import {
|
||||
buildKeyTriggersSection,
|
||||
buildToolSelectionTable,
|
||||
buildExploreSection,
|
||||
buildLibrarianSection,
|
||||
buildDelegationTable,
|
||||
buildFrontendSection,
|
||||
buildCategorySkillsDelegationGuide,
|
||||
buildOracleSection,
|
||||
buildHardBlocksSection,
|
||||
buildAntiPatternsSection,
|
||||
categorizeTools,
|
||||
} from "./sisyphus-prompt-builder"
|
||||
} from "./dynamic-agent-prompt-builder"
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
function buildDynamicSisyphusPrompt(
|
||||
availableAgents: AvailableAgent[],
|
||||
availableTools: AvailableTool[] = [],
|
||||
availableSkills: AvailableSkill[] = [],
|
||||
availableCategories: AvailableCategory[] = []
|
||||
): string {
|
||||
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
|
||||
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
|
||||
const exploreSection = buildExploreSection(availableAgents)
|
||||
const librarianSection = buildLibrarianSection(availableAgents)
|
||||
const categorySkillsGuide = buildCategorySkillsDelegationGuide(availableCategories, availableSkills)
|
||||
const delegationTable = buildDelegationTable(availableAgents)
|
||||
const oracleSection = buildOracleSection(availableAgents)
|
||||
const hardBlocks = buildHardBlocksSection()
|
||||
const antiPatterns = buildAntiPatternsSection()
|
||||
|
||||
const SISYPHUS_ROLE_SECTION = `<Role>
|
||||
return `<Role>
|
||||
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
|
||||
|
||||
**Why Sisyphus?**: Humans roll their boulder every day. So do you. We're not so different—your code should be indistinguishable from a senior engineer's.
|
||||
@@ -28,37 +45,26 @@ You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMy
|
||||
- Adapting to codebase maturity (disciplined vs chaotic)
|
||||
- Delegating specialized work to the right subagents
|
||||
- Parallel execution for maximum throughput
|
||||
- Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITELY.
|
||||
- Follows user instructions. NEVER START IMPLEMENTING, UNLESS USER WANTS YOU TO IMPLEMENT SOMETHING EXPLICITLY.
|
||||
- KEEP IN MIND: YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION]), BUT IF NOT USER REQUESTED YOU TO WORK, NEVER START WORK.
|
||||
|
||||
**Operating Mode**: You NEVER work alone when specialists are available. Frontend work → delegate. Deep research → parallel background agents (async subagents). Complex architecture → consult Oracle.
|
||||
|
||||
</Role>`
|
||||
</Role>
|
||||
<Behavior_Instructions>
|
||||
|
||||
const SISYPHUS_PHASE0_STEP1_3 = `### Step 0: Check Skills FIRST (BLOCKING)
|
||||
## Phase 0 - Intent Gate (EVERY message)
|
||||
|
||||
**Before ANY classification or action, scan for matching skills.**
|
||||
|
||||
\`\`\`
|
||||
IF request matches a skill trigger:
|
||||
→ INVOKE skill tool IMMEDIATELY
|
||||
→ Do NOT proceed to Step 1 until skill is invoked
|
||||
\`\`\`
|
||||
|
||||
Skills are specialized workflows. When relevant, they handle the task better than manual orchestration.
|
||||
|
||||
---
|
||||
${keyTriggers}
|
||||
|
||||
### Step 1: Classify Request Type
|
||||
|
||||
| Type | Signal | Action |
|
||||
|------|--------|--------|
|
||||
| **Skill Match** | Matches skill trigger phrase | **INVOKE skill FIRST** via \`skill\` tool |
|
||||
| **Trivial** | Single file, known location, direct answer | Direct tools only (UNLESS Key Trigger applies) |
|
||||
| **Explicit** | Specific file/line, clear command | Execute directly |
|
||||
| **Exploratory** | "How does X work?", "Find Y" | Fire explore (1-3) + tools in parallel |
|
||||
| **Open-ended** | "Improve", "Refactor", "Add feature" | Assess codebase first |
|
||||
| **GitHub Work** | Mentioned in issue, "look into X and create PR" | **Full cycle**: investigate → implement → verify → create PR (see GitHub Workflow section) |
|
||||
| **Ambiguous** | Unclear scope, multiple interpretations | Ask ONE clarifying question |
|
||||
|
||||
### Step 2: Check for Ambiguity
|
||||
@@ -72,16 +78,18 @@ Skills are specialized workflows. When relevant, they handle the task better tha
|
||||
| User's design seems flawed or suboptimal | **MUST raise concern** before implementing |
|
||||
|
||||
### Step 3: Validate Before Acting
|
||||
|
||||
**Assumptions Check:**
|
||||
- Do I have any implicit assumptions that might affect the outcome?
|
||||
- Is the search scope clear?
|
||||
- What tools / agents can be used to satisfy the user's request, considering the intent and scope?
|
||||
- What are the list of tools / agents do I have?
|
||||
- What tools / agents can I leverage for what tasks?
|
||||
- Specifically, how can I leverage them like?
|
||||
- background tasks?
|
||||
- parallel tool calls?
|
||||
- lsp tools?
|
||||
|
||||
**Delegation Check (MANDATORY before acting directly):**
|
||||
1. Is there a specialized agent that perfectly matches this request?
|
||||
2. If not, is there a \`delegate_task\` category best describes this task? (visual-engineering, ultrabrain, quick etc.) What skills are available to equip the agent with?
|
||||
- MUST FIND skills to use, for: \`delegate_task(load_skills=[{skill1}, ...])\` MUST PASS SKILL AS DELEGATE TASK PARAMETER.
|
||||
3. Can I do it myself for the best result, FOR SURE? REALLY, REALLY, THERE IS NO APPROPRIATE CATEGORIES TO WORK WITH?
|
||||
|
||||
**Default Bias: DELEGATE. WORK YOURSELF ONLY WHEN IT IS SUPER SIMPLE.**
|
||||
|
||||
### When to Challenge the User
|
||||
If you observe:
|
||||
@@ -95,9 +103,11 @@ Then: Raise your concern concisely. Propose an alternative. Ask if they want to
|
||||
I notice [observation]. This might cause [problem] because [reason].
|
||||
Alternative: [your suggestion].
|
||||
Should I proceed with your original request, or try the alternative?
|
||||
\`\`\``
|
||||
\`\`\`
|
||||
|
||||
const SISYPHUS_PHASE1 = `## Phase 1 - Codebase Assessment (for Open-ended tasks)
|
||||
---
|
||||
|
||||
## Phase 1 - Codebase Assessment (for Open-ended tasks)
|
||||
|
||||
Before following existing patterns, assess whether they're worth following.
|
||||
|
||||
@@ -118,144 +128,34 @@ Before following existing patterns, assess whether they're worth following.
|
||||
IMPORTANT: If codebase appears undisciplined, verify before assuming:
|
||||
- Different patterns may serve different purposes (intentional)
|
||||
- Migration might be in progress
|
||||
- You might be looking at the wrong reference files`
|
||||
- You might be looking at the wrong reference files
|
||||
|
||||
const SISYPHUS_PRE_DELEGATION_PLANNING = `### Pre-Delegation Planning (MANDATORY)
|
||||
---
|
||||
|
||||
**BEFORE every \`delegate_task\` call, EXPLICITLY declare your reasoning.**
|
||||
## Phase 2A - Exploration & Research
|
||||
|
||||
#### Step 1: Identify Task Requirements
|
||||
${toolSelection}
|
||||
|
||||
Ask yourself:
|
||||
- What is the CORE objective of this task?
|
||||
- What domain does this belong to? (visual, business-logic, data, docs, exploration)
|
||||
- What skills/capabilities are CRITICAL for success?
|
||||
${exploreSection}
|
||||
|
||||
#### Step 2: Select Category or Agent
|
||||
${librarianSection}
|
||||
|
||||
**Decision Tree (follow in order):**
|
||||
|
||||
1. **Is this a skill-triggering pattern?**
|
||||
- YES → Declare skill name + reason
|
||||
- NO → Continue to step 2
|
||||
|
||||
2. **Is this a visual/frontend task?**
|
||||
- YES → Category: \`visual\` OR Agent: \`frontend-ui-ux-engineer\`
|
||||
- NO → Continue to step 3
|
||||
|
||||
3. **Is this backend/architecture/logic task?**
|
||||
- YES → Category: \`business-logic\` OR Agent: \`oracle\`
|
||||
- NO → Continue to step 4
|
||||
|
||||
4. **Is this documentation/writing task?**
|
||||
- YES → Agent: \`document-writer\`
|
||||
- NO → Continue to step 5
|
||||
|
||||
5. **Is this exploration/search task?**
|
||||
- YES → Agent: \`explore\` (internal codebase) OR \`librarian\` (external docs/repos)
|
||||
- NO → Use default category based on context
|
||||
|
||||
#### Step 3: Declare BEFORE Calling
|
||||
|
||||
**MANDATORY FORMAT:**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Category/Agent**: [name]
|
||||
- **Reason**: [why this choice fits the task]
|
||||
- **Skills** (if any): [skill names]
|
||||
- **Expected Outcome**: [what success looks like]
|
||||
\`\`\`
|
||||
|
||||
**Then** make the delegate_task call.
|
||||
|
||||
#### Examples
|
||||
|
||||
**✅ CORRECT: Explicit Pre-Declaration**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Category**: visual
|
||||
- **Reason**: This task requires building a responsive dashboard UI with animations - visual design is the core requirement
|
||||
- **Skills**: ["frontend-ui-ux"]
|
||||
- **Expected Outcome**: Fully styled, responsive dashboard component with smooth transitions
|
||||
|
||||
delegate_task(
|
||||
category="visual",
|
||||
skills=["frontend-ui-ux"],
|
||||
prompt="Create a responsive dashboard component with..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**✅ CORRECT: Agent-Specific Delegation**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Agent**: oracle
|
||||
- **Reason**: This architectural decision involves trade-offs between scalability and complexity - requires high-IQ strategic analysis
|
||||
- **Skills**: []
|
||||
- **Expected Outcome**: Clear recommendation with pros/cons analysis
|
||||
|
||||
delegate_task(
|
||||
agent="oracle",
|
||||
skills=[],
|
||||
prompt="Evaluate this microservices architecture proposal..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**✅ CORRECT: Background Exploration**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Agent**: explore
|
||||
- **Reason**: Need to find all authentication implementations across the codebase - this is contextual grep
|
||||
- **Skills**: []
|
||||
- **Expected Outcome**: List of files containing auth patterns
|
||||
|
||||
delegate_task(
|
||||
agent="explore",
|
||||
background=true,
|
||||
prompt="Find all authentication implementations in the codebase"
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**❌ WRONG: No Pre-Declaration**
|
||||
|
||||
\`\`\`
|
||||
// Immediately calling without explicit reasoning
|
||||
delegate_task(category="visual", prompt="Build a dashboard")
|
||||
\`\`\`
|
||||
|
||||
**❌ WRONG: Vague Reasoning**
|
||||
|
||||
\`\`\`
|
||||
I'll use visual category because it's frontend work.
|
||||
|
||||
delegate_task(category="visual", ...)
|
||||
\`\`\`
|
||||
|
||||
#### Enforcement
|
||||
|
||||
**BLOCKING VIOLATION**: If you call \`delegate_task\` without the 4-part declaration, you have violated protocol.
|
||||
|
||||
**Recovery**: Stop, declare explicitly, then proceed.`
|
||||
|
||||
const SISYPHUS_PARALLEL_EXECUTION = `### Parallel Execution (DEFAULT behavior)
|
||||
### Parallel Execution (DEFAULT behavior)
|
||||
|
||||
**Explore/Librarian = Grep, not consultants.
|
||||
|
||||
\`\`\`typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(agent="explore", prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(agent="explore", prompt="Find error handling patterns here...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
delegate_task(agent="librarian", prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(agent="librarian", prompt="Find how production apps handle auth in Express...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
result = task(...) // Never wait synchronously for explore/librarian
|
||||
result = delegate_task(..., run_in_background=false) // Never wait synchronously for explore/librarian
|
||||
\`\`\`
|
||||
|
||||
### Background Result Collection:
|
||||
@@ -264,19 +164,6 @@ result = task(...) // Never wait synchronously for explore/librarian
|
||||
3. When results needed: \`background_output(task_id="...")\`
|
||||
4. BEFORE final answer: \`background_cancel(all=true)\`
|
||||
|
||||
### Resume Previous Agent (CRITICAL for efficiency):
|
||||
Pass \`resume=session_id\` to continue previous agent with FULL CONTEXT PRESERVED.
|
||||
|
||||
**ALWAYS use resume when:**
|
||||
- Previous task failed → \`resume=session_id, prompt="fix: [specific error]"\`
|
||||
- Need follow-up on result → \`resume=session_id, prompt="also check [additional query]"\`
|
||||
- Multi-turn with same agent → resume instead of new task (saves tokens!)
|
||||
|
||||
**Example:**
|
||||
\`\`\`
|
||||
delegate_task(resume="ses_abc123", prompt="The previous search missed X. Also look for Y.")
|
||||
\`\`\`
|
||||
|
||||
### Search Stop Conditions
|
||||
|
||||
STOP searching when:
|
||||
@@ -285,27 +172,32 @@ STOP searching when:
|
||||
- 2 search iterations yielded no new useful data
|
||||
- Direct answer found
|
||||
|
||||
**DO NOT over-explore. Time is precious.**`
|
||||
**DO NOT over-explore. Time is precious.**
|
||||
|
||||
const SISYPHUS_PHASE2B_PRE_IMPLEMENTATION = `## Phase 2B - Implementation
|
||||
---
|
||||
|
||||
## Phase 2B - Implementation
|
||||
|
||||
### Pre-Implementation:
|
||||
1. If task has 2+ steps → Create todo list IMMEDIATELY, IN SUPER DETAIL. No announcements—just create it.
|
||||
2. Mark current task \`in_progress\` before starting
|
||||
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS`
|
||||
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS
|
||||
|
||||
const SISYPHUS_DELEGATION_PROMPT_STRUCTURE = `### Delegation Prompt Structure (MANDATORY - ALL 7 sections):
|
||||
${categorySkillsGuide}
|
||||
|
||||
${delegationTable}
|
||||
|
||||
### Delegation Prompt Structure (MANDATORY - ALL 6 sections):
|
||||
|
||||
When delegating, your prompt MUST include:
|
||||
|
||||
\`\`\`
|
||||
1. TASK: Atomic, specific goal (one action per delegation)
|
||||
2. EXPECTED OUTCOME: Concrete deliverables with success criteria
|
||||
3. REQUIRED SKILLS: Which skill to invoke
|
||||
4. REQUIRED TOOLS: Explicit tool whitelist (prevents tool sprawl)
|
||||
5. MUST DO: Exhaustive requirements - leave NOTHING implicit
|
||||
6. MUST NOT DO: Forbidden actions - anticipate and block rogue behavior
|
||||
7. CONTEXT: File paths, existing patterns, constraints
|
||||
3. REQUIRED TOOLS: Explicit tool whitelist (prevents tool sprawl)
|
||||
4. MUST DO: Exhaustive requirements - leave NOTHING implicit
|
||||
5. MUST NOT DO: Forbidden actions - anticipate and block rogue behavior
|
||||
6. CONTEXT: File paths, existing patterns, constraints
|
||||
\`\`\`
|
||||
|
||||
AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
@@ -314,44 +206,37 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
- EXPECTED RESULT CAME OUT?
|
||||
- DID THE AGENT FOLLOWED "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
|
||||
|
||||
**Vague prompts = rejected. Be exhaustive.**`
|
||||
**Vague prompts = rejected. Be exhaustive.**
|
||||
|
||||
const SISYPHUS_GITHUB_WORKFLOW = `### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
|
||||
### Session Continuity (MANDATORY)
|
||||
|
||||
When you're mentioned in GitHub issues or asked to "look into" something and "create PR":
|
||||
Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
**This is NOT just investigation. This is a COMPLETE WORK CYCLE.**
|
||||
**ALWAYS continue when:**
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Task failed/incomplete | \`session_id="{session_id}", prompt="Fix: {specific error}"\` |
|
||||
| Follow-up question on result | \`session_id="{session_id}", prompt="Also: {question}"\` |
|
||||
| Multi-turn with same agent | \`session_id="{session_id}"\` - NEVER start fresh |
|
||||
| Verification failed | \`session_id="{session_id}", prompt="Failed verification: {error}. Fix."\` |
|
||||
|
||||
#### Pattern Recognition:
|
||||
- "@sisyphus look into X"
|
||||
- "look into X and create PR"
|
||||
- "investigate Y and make PR"
|
||||
- Mentioned in issue comments
|
||||
**Why session_id is CRITICAL:**
|
||||
- Subagent has FULL conversation context preserved
|
||||
- No repeated file reads, exploration, or setup
|
||||
- Saves 70%+ tokens on follow-ups
|
||||
- Subagent knows what it already tried/learned
|
||||
|
||||
#### Required Workflow (NON-NEGOTIABLE):
|
||||
1. **Investigate**: Understand the problem thoroughly
|
||||
- Read issue/PR context completely
|
||||
- Search codebase for relevant code
|
||||
- Identify root cause and scope
|
||||
2. **Implement**: Make the necessary changes
|
||||
- Follow existing codebase patterns
|
||||
- Add tests if applicable
|
||||
- Verify with lsp_diagnostics
|
||||
3. **Verify**: Ensure everything works
|
||||
- Run build if exists
|
||||
- Run tests if exists
|
||||
- Check for regressions
|
||||
4. **Create PR**: Complete the cycle
|
||||
- Use \`gh pr create\` with meaningful title and description
|
||||
- Reference the original issue number
|
||||
- Summarize what was changed and why
|
||||
\`\`\`typescript
|
||||
// WRONG: Starting fresh loses all context
|
||||
delegate_task(category="quick", prompt="Fix the type error in auth.ts...")
|
||||
|
||||
**EMPHASIS**: "Look into" does NOT mean "just investigate and report back."
|
||||
It means "investigate, understand, implement a solution, and create a PR."
|
||||
// CORRECT: Resume preserves everything
|
||||
delegate_task(session_id="ses_abc123", prompt="Fix: Type error on line 42")
|
||||
\`\`\`
|
||||
|
||||
**If the user says "look into X and create PR", they expect a PR, not just analysis.**`
|
||||
**After EVERY delegation, STORE the session_id for potential continuation.**
|
||||
|
||||
const SISYPHUS_CODE_CHANGES = `### Code Changes:
|
||||
### Code Changes:
|
||||
- Match existing patterns (if codebase is disciplined)
|
||||
- Propose approach first (if codebase is chaotic)
|
||||
- Never suppress type errors with \`as any\`, \`@ts-ignore\`, \`@ts-expect-error\`
|
||||
@@ -377,9 +262,11 @@ If project has build/test commands, run them at task completion.
|
||||
| Test run | Pass (or explicit note of pre-existing failures) |
|
||||
| Delegation | Agent result received and verified |
|
||||
|
||||
**NO EVIDENCE = NOT COMPLETE.**`
|
||||
**NO EVIDENCE = NOT COMPLETE.**
|
||||
|
||||
const SISYPHUS_PHASE2C = `## Phase 2C - Failure Recovery
|
||||
---
|
||||
|
||||
## Phase 2C - Failure Recovery
|
||||
|
||||
### When Fixes Fail:
|
||||
|
||||
@@ -395,9 +282,11 @@ const SISYPHUS_PHASE2C = `## Phase 2C - Failure Recovery
|
||||
4. **CONSULT** Oracle with full failure context
|
||||
5. If Oracle cannot resolve → **ASK USER** before proceeding
|
||||
|
||||
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"`
|
||||
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"
|
||||
|
||||
const SISYPHUS_PHASE3 = `## Phase 3 - Completion
|
||||
---
|
||||
|
||||
## Phase 3 - Completion
|
||||
|
||||
A task is complete when:
|
||||
- [ ] All planned todo items marked done
|
||||
@@ -412,9 +301,12 @@ If verification fails:
|
||||
|
||||
### Before Delivering Final Answer:
|
||||
- Cancel ALL running background tasks: \`background_cancel(all=true)\`
|
||||
- This conserves resources and ensures clean workflow completion`
|
||||
- This conserves resources and ensures clean workflow completion
|
||||
</Behavior_Instructions>
|
||||
|
||||
const SISYPHUS_TASK_MANAGEMENT = `<Task_Management>
|
||||
${oracleSection}
|
||||
|
||||
<Task_Management>
|
||||
## Todo Management (CRITICAL)
|
||||
|
||||
**DEFAULT BEHAVIOR**: Create todos BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
|
||||
@@ -469,13 +361,13 @@ I want to make sure I understand correctly.
|
||||
|
||||
Should I proceed with [recommendation], or would you prefer differently?
|
||||
\`\`\`
|
||||
</Task_Management>`
|
||||
</Task_Management>
|
||||
|
||||
const SISYPHUS_TONE_AND_STYLE = `<Tone_and_Style>
|
||||
<Tone_and_Style>
|
||||
## Communication Style
|
||||
|
||||
### Be Concise
|
||||
- Start work immediately. No acknowledgments ("I'm on it", "Let me...", "I'll start...")
|
||||
- Start work immediately. No acknowledgments ("I'm on it", "Let me...", "I'll start...")
|
||||
- Answer directly without preamble
|
||||
- Don't summarize what you did unless asked
|
||||
- Don't explain your code unless asked
|
||||
@@ -511,118 +403,41 @@ If the user's approach seems problematic:
|
||||
- If user is terse, be terse
|
||||
- If user wants detail, provide detail
|
||||
- Adapt to their communication preference
|
||||
</Tone_and_Style>`
|
||||
</Tone_and_Style>
|
||||
|
||||
const SISYPHUS_SOFT_GUIDELINES = `## Soft Guidelines
|
||||
<Constraints>
|
||||
${hardBlocks}
|
||||
|
||||
${antiPatterns}
|
||||
|
||||
## Soft Guidelines
|
||||
|
||||
- Prefer existing libraries over new dependencies
|
||||
- Prefer small, focused changes over large refactors
|
||||
- When uncertain about scope, ask
|
||||
</Constraints>
|
||||
|
||||
`
|
||||
|
||||
function buildDynamicSisyphusPrompt(
|
||||
availableAgents: AvailableAgent[],
|
||||
availableTools: AvailableTool[] = [],
|
||||
availableSkills: AvailableSkill[] = []
|
||||
): string {
|
||||
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
|
||||
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
|
||||
const exploreSection = buildExploreSection(availableAgents)
|
||||
const librarianSection = buildLibrarianSection(availableAgents)
|
||||
const frontendSection = buildFrontendSection(availableAgents)
|
||||
const delegationTable = buildDelegationTable(availableAgents)
|
||||
const oracleSection = buildOracleSection(availableAgents)
|
||||
const hardBlocks = buildHardBlocksSection(availableAgents)
|
||||
const antiPatterns = buildAntiPatternsSection(availableAgents)
|
||||
|
||||
const sections = [
|
||||
SISYPHUS_ROLE_SECTION,
|
||||
"<Behavior_Instructions>",
|
||||
"",
|
||||
"## Phase 0 - Intent Gate (EVERY message)",
|
||||
"",
|
||||
keyTriggers,
|
||||
"",
|
||||
SISYPHUS_PHASE0_STEP1_3,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE1,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## Phase 2A - Exploration & Research",
|
||||
"",
|
||||
toolSelection,
|
||||
"",
|
||||
exploreSection,
|
||||
"",
|
||||
librarianSection,
|
||||
"",
|
||||
SISYPHUS_PRE_DELEGATION_PLANNING,
|
||||
"",
|
||||
SISYPHUS_PARALLEL_EXECUTION,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE2B_PRE_IMPLEMENTATION,
|
||||
"",
|
||||
frontendSection,
|
||||
"",
|
||||
delegationTable,
|
||||
"",
|
||||
SISYPHUS_DELEGATION_PROMPT_STRUCTURE,
|
||||
"",
|
||||
SISYPHUS_GITHUB_WORKFLOW,
|
||||
"",
|
||||
SISYPHUS_CODE_CHANGES,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE2C,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE3,
|
||||
"",
|
||||
"</Behavior_Instructions>",
|
||||
"",
|
||||
oracleSection,
|
||||
"",
|
||||
SISYPHUS_TASK_MANAGEMENT,
|
||||
"",
|
||||
SISYPHUS_TONE_AND_STYLE,
|
||||
"",
|
||||
"<Constraints>",
|
||||
hardBlocks,
|
||||
"",
|
||||
antiPatterns,
|
||||
"",
|
||||
SISYPHUS_SOFT_GUIDELINES,
|
||||
]
|
||||
|
||||
return sections.filter((s) => s !== "").join("\n")
|
||||
}
|
||||
|
||||
export function createSisyphusAgent(
|
||||
model: string = DEFAULT_MODEL,
|
||||
model: string,
|
||||
availableAgents?: AvailableAgent[],
|
||||
availableToolNames?: string[],
|
||||
availableSkills?: AvailableSkill[]
|
||||
availableSkills?: AvailableSkill[],
|
||||
availableCategories?: AvailableCategory[]
|
||||
): AgentConfig {
|
||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : []
|
||||
const skills = availableSkills ?? []
|
||||
const categories = availableCategories ?? []
|
||||
const prompt = availableAgents
|
||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills)
|
||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills, categories)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills, categories)
|
||||
|
||||
const permission = { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"]
|
||||
const base = {
|
||||
description:
|
||||
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.",
|
||||
mode: "primary" as const,
|
||||
"Powerful AI orchestrator. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs. (Sisyphus - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
maxTokens: 64000,
|
||||
prompt,
|
||||
@@ -636,5 +451,4 @@ export function createSisyphusAgent(
|
||||
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
|
||||
}
|
||||
|
||||
export const sisyphusAgent = createSisyphusAgent()
|
||||
createSisyphusAgent.mode = MODE
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export type AgentFactory = (model?: string) => AgentConfig
|
||||
/**
|
||||
* Agent mode determines UI model selection behavior:
|
||||
* - "primary": Respects user's UI-selected model (sisyphus, atlas)
|
||||
* - "subagent": Uses own fallback chain, ignores UI selection (oracle, explore, etc.)
|
||||
* - "all": Available in both contexts (OpenCode compatibility)
|
||||
*/
|
||||
export type AgentMode = "primary" | "subagent" | "all"
|
||||
|
||||
/**
|
||||
* Agent factory function with static mode property.
|
||||
* Mode is exposed as static property for pre-instantiation access.
|
||||
*/
|
||||
export type AgentFactory = ((model: string) => AgentConfig) & {
|
||||
mode: AgentMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent category for grouping in Sisyphus prompt sections
|
||||
@@ -57,16 +71,14 @@ export function isGptModel(model: string): boolean {
|
||||
}
|
||||
|
||||
export type BuiltinAgentName =
|
||||
| "Sisyphus"
|
||||
| "sisyphus"
|
||||
| "oracle"
|
||||
| "librarian"
|
||||
| "explore"
|
||||
| "frontend-ui-ux-engineer"
|
||||
| "document-writer"
|
||||
| "multimodal-looker"
|
||||
| "Metis (Plan Consultant)"
|
||||
| "Momus (Plan Reviewer)"
|
||||
| "orchestrator-sisyphus"
|
||||
| "metis"
|
||||
| "momus"
|
||||
| "atlas"
|
||||
|
||||
export type OverridableAgentName =
|
||||
| "build"
|
||||
|
||||
@@ -1,53 +1,88 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { describe, test, expect, beforeEach, spyOn, afterEach } from "bun:test"
|
||||
import { createBuiltinAgents } from "./utils"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { clearSkillCache } from "../features/opencode-skill-loader/skill-content"
|
||||
import * as connectedProvidersCache from "../shared/connected-providers-cache"
|
||||
import * as modelAvailability from "../shared/model-availability"
|
||||
|
||||
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
describe("createBuiltinAgents with model overrides", () => {
|
||||
test("Sisyphus with default model has thinking config", () => {
|
||||
// #given - no overrides
|
||||
test("Sisyphus with default model has thinking config", async () => {
|
||||
// #given - no overrides, using systemDefaultModel
|
||||
|
||||
// #when
|
||||
const agents = createBuiltinAgents()
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.Sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.Sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
|
||||
expect(agents.Sisyphus.reasoningEffort).toBeUndefined()
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
|
||||
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Sisyphus with GPT model override has reasoningEffort, no thinking", () => {
|
||||
test("Sisyphus with GPT model override has reasoningEffort, no thinking", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
Sisyphus: { model: "github-copilot/gpt-5.2" },
|
||||
sisyphus: { model: "github-copilot/gpt-5.2" },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = createBuiltinAgents([], overrides)
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.Sisyphus.reasoningEffort).toBe("medium")
|
||||
expect(agents.Sisyphus.thinking).toBeUndefined()
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.sisyphus.reasoningEffort).toBe("medium")
|
||||
expect(agents.sisyphus.thinking).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Sisyphus with systemDefaultModel GPT has reasoningEffort, no thinking", () => {
|
||||
test("Sisyphus uses system default when no availableModels provided", async () => {
|
||||
// #given
|
||||
const systemDefaultModel = "openai/gpt-5.2"
|
||||
const systemDefaultModel = "anthropic/claude-opus-4-5"
|
||||
|
||||
// #when
|
||||
const agents = createBuiltinAgents([], {}, undefined, systemDefaultModel)
|
||||
const agents = await createBuiltinAgents([], {}, undefined, systemDefaultModel)
|
||||
|
||||
// #then
|
||||
expect(agents.Sisyphus.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.Sisyphus.reasoningEffort).toBe("medium")
|
||||
expect(agents.Sisyphus.thinking).toBeUndefined()
|
||||
// #then - falls back to system default when no availability match
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
|
||||
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Oracle with default model has reasoningEffort", () => {
|
||||
// #given - no overrides
|
||||
test("Oracle uses connected provider fallback when availableModels is empty and cache exists", async () => {
|
||||
// #given - connected providers cache has "openai", which matches oracle's first fallback entry
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.oracle.reasoningEffort).toBe("medium")
|
||||
expect(agents.oracle.thinking).toBeUndefined()
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
|
||||
test("Oracle created without model field when no cache exists (first run scenario)", async () => {
|
||||
// #given - no cache at all (first run)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - oracle should be created with system default model (fallback to systemDefaultModel)
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe(TEST_DEFAULT_MODEL)
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
|
||||
test("Oracle with GPT model override has reasoningEffort, no thinking", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = createBuiltinAgents()
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
@@ -56,14 +91,14 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
expect(agents.oracle.thinking).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Oracle with Claude model override has thinking, no reasoningEffort", () => {
|
||||
test("Oracle with Claude model override has thinking, no reasoningEffort", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { model: "anthropic/claude-sonnet-4" },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = createBuiltinAgents([], overrides)
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4")
|
||||
@@ -72,26 +107,71 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
expect(agents.oracle.textVerbosity).toBeUndefined()
|
||||
})
|
||||
|
||||
test("non-model overrides are still applied after factory rebuild", () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
Sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
|
||||
}
|
||||
test("non-model overrides are still applied after factory rebuild", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
sisyphus: { model: "github-copilot/gpt-5.2", temperature: 0.5 },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = createBuiltinAgents([], overrides)
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.Sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.Sisyphus.temperature).toBe(0.5)
|
||||
})
|
||||
// #then
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.sisyphus.temperature).toBe(0.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
test("agents created via connected cache fallback even without systemDefaultModel", async () => {
|
||||
// #given - connected cache has "openai", which matches oracle's fallback chain
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, undefined)
|
||||
|
||||
// #then - connected cache enables model resolution despite no systemDefaultModel
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
|
||||
test("agents NOT created when no cache and no systemDefaultModel (first run without defaults)", async () => {
|
||||
// #given
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle).toBeUndefined()
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
|
||||
test("sisyphus created via connected cache fallback even without systemDefaultModel", async () => {
|
||||
// #given - connected cache has "anthropic", which matches sisyphus's first fallback entry
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, undefined)
|
||||
|
||||
// #then - connected cache enables model resolution despite no systemDefaultModel
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildAgent with category and skills", () => {
|
||||
const { buildAgent } = require("./utils")
|
||||
const TEST_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
beforeEach(() => {
|
||||
clearSkillCache()
|
||||
})
|
||||
|
||||
test("agent with category inherits category settings", () => {
|
||||
// #given
|
||||
// #given - agent factory that sets category but no model
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
@@ -101,11 +181,10 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(agent.temperature).toBe(0.7)
|
||||
// #then - category's built-in model is applied
|
||||
expect(agent.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("agent with category and existing model keeps existing model", () => {
|
||||
@@ -120,11 +199,10 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
// #then - explicit model takes precedence over category
|
||||
expect(agent.model).toBe("custom/model")
|
||||
expect(agent.temperature).toBe(0.7)
|
||||
})
|
||||
|
||||
test("agent with category inherits variant", () => {
|
||||
@@ -145,7 +223,7 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"], undefined, categories)
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL, categories)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("openai/gpt-5.2")
|
||||
@@ -164,7 +242,7 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
@@ -184,7 +262,7 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
@@ -204,7 +282,7 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("custom/model")
|
||||
@@ -225,11 +303,11 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("openai/gpt-5.2")
|
||||
expect(agent.temperature).toBe(0.1)
|
||||
// #then - category's built-in model and skills are applied
|
||||
expect(agent.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agent.variant).toBe("xhigh")
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
expect(agent.prompt).toContain("Task description")
|
||||
})
|
||||
@@ -246,9 +324,11 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
// Note: The factory receives model, but if category doesn't exist, it's not applied
|
||||
// The agent's model comes from the factory output (which doesn't set model)
|
||||
expect(agent.model).toBeUndefined()
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
})
|
||||
@@ -265,7 +345,7 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
@@ -284,9 +364,201 @@ describe("buildAgent with category and skills", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with agent-browser skill resolves when browserProvider is set", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["agent-browser"],
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when - browserProvider is "agent-browser"
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL, undefined, undefined, "agent-browser")
|
||||
|
||||
// #then - agent-browser skill content should be in prompt
|
||||
expect(agent.prompt).toContain("agent-browser")
|
||||
expect(agent.prompt).toContain("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with agent-browser skill NOT resolved when browserProvider not set", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["agent-browser"],
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when - no browserProvider (defaults to playwright)
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then - agent-browser skill not found, only base prompt remains
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
expect(agent.prompt).not.toContain("agent-browser open")
|
||||
})
|
||||
})
|
||||
|
||||
describe("override.category expansion in createBuiltinAgents", () => {
|
||||
test("standard agent override with category expands category properties", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { category: "ultrabrain" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.oracle.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("standard agent override with category AND direct variant - direct wins", async () => {
|
||||
// #given - ultrabrain has variant=xhigh, but direct override says "max"
|
||||
const overrides = {
|
||||
oracle: { category: "ultrabrain", variant: "max" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - direct variant overrides category variant
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.variant).toBe("max")
|
||||
})
|
||||
|
||||
test("standard agent override with category AND direct reasoningEffort - direct wins", async () => {
|
||||
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
|
||||
const categories = {
|
||||
"test-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
reasoningEffort: "xhigh" as const,
|
||||
},
|
||||
}
|
||||
const overrides = {
|
||||
oracle: { category: "test-cat", reasoningEffort: "low" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, categories)
|
||||
|
||||
// #then - direct reasoningEffort wins over category
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.reasoningEffort).toBe("low")
|
||||
})
|
||||
|
||||
test("standard agent override with category applies reasoningEffort from category when no direct override", async () => {
|
||||
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
|
||||
const categories = {
|
||||
"reasoning-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
reasoningEffort: "high" as const,
|
||||
},
|
||||
}
|
||||
const overrides = {
|
||||
oracle: { category: "reasoning-cat" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, categories)
|
||||
|
||||
// #then - category reasoningEffort is applied
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.reasoningEffort).toBe("high")
|
||||
})
|
||||
|
||||
test("sisyphus override with category expands category properties", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
sisyphus: { category: "ultrabrain" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.sisyphus.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("atlas override with category expands category properties", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
atlas: { category: "ultrabrain" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
expect(agents.atlas).toBeDefined()
|
||||
expect(agents.atlas.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.atlas.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("override with non-existent category has no effect on config", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { category: "non-existent-category" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - no category-specific variant/reasoningEffort applied from non-existent category
|
||||
expect(agents.oracle).toBeDefined()
|
||||
const agentsWithoutOverride = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
expect(agents.oracle.model).toBe(agentsWithoutOverride.oracle.model)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
|
||||
test("createBuiltinAgents should call fetchAvailableModels with undefined client to prevent deadlock", async () => {
|
||||
// #given - This test ensures we don't regress on issue #1301
|
||||
// Passing client to fetchAvailableModels during createBuiltinAgents (called from config handler)
|
||||
// causes deadlock:
|
||||
// - Plugin init waits for server response (client.provider.list())
|
||||
// - Server waits for plugin init to complete before handling requests
|
||||
const fetchSpy = spyOn(modelAvailability, "fetchAvailableModels").mockResolvedValue(new Set<string>())
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
|
||||
const mockClient = {
|
||||
provider: { list: () => Promise.resolve({ data: { connected: [] } }) },
|
||||
model: { list: () => Promise.resolve({ data: [] }) },
|
||||
}
|
||||
|
||||
// #when - Even when client is provided, fetchAvailableModels must be called with undefined
|
||||
await createBuiltinAgents(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
mockClient // client is passed but should NOT be forwarded to fetchAvailableModels
|
||||
)
|
||||
|
||||
// #then - fetchAvailableModels must be called with undefined as first argument (no client)
|
||||
// This prevents the deadlock described in issue #1301
|
||||
expect(fetchSpy).toHaveBeenCalled()
|
||||
const firstCallArgs = fetchSpy.mock.calls[0]
|
||||
expect(firstCallArgs[0]).toBeUndefined()
|
||||
|
||||
fetchSpy.mockRestore?.()
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,30 +5,31 @@ import { createSisyphusAgent } from "./sisyphus"
|
||||
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
||||
import { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
|
||||
import { createFrontendUiUxEngineerAgent, FRONTEND_PROMPT_METADATA } from "./frontend-ui-ux-engineer"
|
||||
import { createDocumentWriterAgent, DOCUMENT_WRITER_PROMPT_METADATA } from "./document-writer"
|
||||
import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
|
||||
import { createMetisAgent } from "./metis"
|
||||
import { createOrchestratorSisyphusAgent, orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
|
||||
import { createAtlasAgent } from "./atlas"
|
||||
import { createMomusAgent } from "./momus"
|
||||
import type { AvailableAgent } from "./sisyphus-prompt-builder"
|
||||
import { deepMerge } from "../shared"
|
||||
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"
|
||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||
import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive, readConnectedProvidersCache, isModelAvailable } from "../shared"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||
import { createBuiltinSkills } from "../features/builtin-skills"
|
||||
import type { LoadedSkill, SkillScope } from "../features/opencode-skill-loader/types"
|
||||
import type { BrowserAutomationProvider } from "../config/schema"
|
||||
|
||||
type AgentSource = AgentFactory | AgentConfig
|
||||
|
||||
const agentSources: Record<BuiltinAgentName, AgentSource> = {
|
||||
Sisyphus: createSisyphusAgent,
|
||||
sisyphus: createSisyphusAgent,
|
||||
oracle: createOracleAgent,
|
||||
librarian: createLibrarianAgent,
|
||||
explore: createExploreAgent,
|
||||
"frontend-ui-ux-engineer": createFrontendUiUxEngineerAgent,
|
||||
"document-writer": createDocumentWriterAgent,
|
||||
"multimodal-looker": createMultimodalLookerAgent,
|
||||
"Metis (Plan Consultant)": createMetisAgent,
|
||||
"Momus (Plan Reviewer)": createMomusAgent,
|
||||
"orchestrator-sisyphus": orchestratorSisyphusAgent,
|
||||
metis: createMetisAgent,
|
||||
momus: createMomusAgent,
|
||||
// Note: Atlas is handled specially in createBuiltinAgents()
|
||||
// because it needs OrchestratorContext, not just a model string
|
||||
atlas: createAtlasAgent as unknown as AgentFactory,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +40,6 @@ const agentMetadata: Partial<Record<BuiltinAgentName, AgentPromptMetadata>> = {
|
||||
oracle: ORACLE_PROMPT_METADATA,
|
||||
librarian: LIBRARIAN_PROMPT_METADATA,
|
||||
explore: EXPLORE_PROMPT_METADATA,
|
||||
"frontend-ui-ux-engineer": FRONTEND_PROMPT_METADATA,
|
||||
"document-writer": DOCUMENT_WRITER_PROMPT_METADATA,
|
||||
"multimodal-looker": MULTIMODAL_LOOKER_PROMPT_METADATA,
|
||||
}
|
||||
|
||||
@@ -50,9 +49,10 @@ function isFactory(source: AgentSource): source is AgentFactory {
|
||||
|
||||
export function buildAgent(
|
||||
source: AgentSource,
|
||||
model?: string,
|
||||
model: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
browserProvider?: BrowserAutomationProvider
|
||||
): AgentConfig {
|
||||
const base = isFactory(source) ? source(model) : source
|
||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
||||
@@ -76,7 +76,7 @@ export function buildAgent(
|
||||
}
|
||||
|
||||
if (agentWithCategory.skills?.length) {
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig })
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider })
|
||||
if (resolved.size > 0) {
|
||||
const skillContent = Array.from(resolved.values()).join("\n\n")
|
||||
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
||||
@@ -97,7 +97,14 @@ export function createEnvContext(): string {
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
||||
|
||||
const timeStr = now.toLocaleTimeString("en-US", {
|
||||
const dateStr = now.toLocaleDateString(locale, {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
|
||||
const timeStr = now.toLocaleTimeString(locale, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
@@ -106,12 +113,40 @@ export function createEnvContext(): string {
|
||||
|
||||
return `
|
||||
<omo-env>
|
||||
Current date: ${dateStr}
|
||||
Current time: ${timeStr}
|
||||
Timezone: ${timezone}
|
||||
Locale: ${locale}
|
||||
</omo-env>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a category reference from an agent override into concrete config properties.
|
||||
* Category properties are applied unconditionally (overwriting factory defaults),
|
||||
* because the user's chosen category should take priority over factory base values.
|
||||
* Direct override properties applied later via mergeAgentConfig() will supersede these.
|
||||
*/
|
||||
function applyCategoryOverride(
|
||||
config: AgentConfig,
|
||||
categoryName: string,
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
): AgentConfig {
|
||||
const categoryConfig = mergedCategories[categoryName]
|
||||
if (!categoryConfig) return config
|
||||
|
||||
const result = { ...config } as AgentConfig & Record<string, unknown>
|
||||
if (categoryConfig.model) result.model = categoryConfig.model
|
||||
if (categoryConfig.variant !== undefined) result.variant = categoryConfig.variant
|
||||
if (categoryConfig.temperature !== undefined) result.temperature = categoryConfig.temperature
|
||||
if (categoryConfig.reasoningEffort !== undefined) result.reasoningEffort = categoryConfig.reasoningEffort
|
||||
if (categoryConfig.textVerbosity !== undefined) result.textVerbosity = categoryConfig.textVerbosity
|
||||
if (categoryConfig.thinking !== undefined) result.thinking = categoryConfig.thinking
|
||||
if (categoryConfig.top_p !== undefined) result.top_p = categoryConfig.top_p
|
||||
if (categoryConfig.maxTokens !== undefined) result.maxTokens = categoryConfig.maxTokens
|
||||
|
||||
return result as AgentConfig
|
||||
}
|
||||
|
||||
function mergeAgentConfig(
|
||||
base: AgentConfig,
|
||||
override: AgentOverrideConfig
|
||||
@@ -126,14 +161,32 @@ function mergeAgentConfig(
|
||||
return merged
|
||||
}
|
||||
|
||||
export function createBuiltinAgents(
|
||||
disabledAgents: BuiltinAgentName[] = [],
|
||||
function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
|
||||
if (scope === "user" || scope === "opencode") return "user"
|
||||
if (scope === "project" || scope === "opencode-project") return "project"
|
||||
return "plugin"
|
||||
}
|
||||
|
||||
export async function createBuiltinAgents(
|
||||
disabledAgents: string[] = [],
|
||||
agentOverrides: AgentOverrides = {},
|
||||
directory?: string,
|
||||
systemDefaultModel?: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig
|
||||
): Record<string, AgentConfig> {
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
discoveredSkills: LoadedSkill[] = [],
|
||||
client?: any,
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
uiSelectedModel?: string
|
||||
): Promise<Record<string, AgentConfig>> {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
// IMPORTANT: Do NOT pass client to fetchAvailableModels during plugin initialization.
|
||||
// This function is called from config handler, and calling client API causes deadlock.
|
||||
// See: https://github.com/code-yeongyu/oh-my-opencode/issues/1301
|
||||
const availableModels = await fetchAvailableModels(undefined, {
|
||||
connectedProviders: connectedProviders ?? undefined,
|
||||
})
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const availableAgents: AvailableAgent[] = []
|
||||
|
||||
@@ -141,23 +194,78 @@ export function createBuiltinAgents(
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
for (const [name, source] of Object.entries(agentSources)) {
|
||||
const agentName = name as BuiltinAgentName
|
||||
const availableCategories: AvailableCategory[] = Object.entries(mergedCategories).map(([name]) => ({
|
||||
name,
|
||||
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
|
||||
}))
|
||||
|
||||
if (agentName === "Sisyphus") continue
|
||||
if (agentName === "orchestrator-sisyphus") continue
|
||||
if (disabledAgents.includes(agentName)) continue
|
||||
const builtinSkills = createBuiltinSkills({ browserProvider })
|
||||
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
|
||||
|
||||
const override = agentOverrides[agentName]
|
||||
const model = override?.model
|
||||
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
location: "plugin" as const,
|
||||
}))
|
||||
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig)
|
||||
const discoveredAvailable: AvailableSkill[] = discoveredSkills
|
||||
.filter(s => !builtinSkillNames.has(s.name))
|
||||
.map((skill) => ({
|
||||
name: skill.name,
|
||||
description: skill.definition.description ?? "",
|
||||
location: mapScopeToLocation(skill.scope),
|
||||
}))
|
||||
|
||||
const availableSkills: AvailableSkill[] = [...builtinAvailable, ...discoveredAvailable]
|
||||
|
||||
for (const [name, source] of Object.entries(agentSources)) {
|
||||
const agentName = name as BuiltinAgentName
|
||||
|
||||
if (agentName === "sisyphus") continue
|
||||
if (agentName === "atlas") continue
|
||||
if (includesCaseInsensitive(disabledAgents, agentName)) continue
|
||||
|
||||
const override = findCaseInsensitive(agentOverrides, agentName)
|
||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||
|
||||
// Check if agent requires a specific model
|
||||
if (requirement?.requiresModel && availableModels) {
|
||||
if (!isModelAvailable(requirement.requiresModel, availableModels)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
|
||||
|
||||
const resolution = resolveModelWithFallback({
|
||||
uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined,
|
||||
userModel: override?.model,
|
||||
fallbackChain: requirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
if (!resolution) continue
|
||||
const { model, variant: resolvedVariant } = resolution
|
||||
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider)
|
||||
|
||||
// Apply resolved variant from model fallback chain
|
||||
if (resolvedVariant) {
|
||||
config = { ...config, variant: resolvedVariant }
|
||||
}
|
||||
|
||||
// Expand override.category into concrete properties (higher priority than factory/resolved)
|
||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (overrideCategory) {
|
||||
config = applyCategoryOverride(config, overrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (agentName === "librarian" && directory && config.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
config = { ...config, prompt: config.prompt + envContext }
|
||||
}
|
||||
|
||||
// Direct override properties take highest priority
|
||||
if (override) {
|
||||
config = mergeAgentConfig(config, override)
|
||||
}
|
||||
@@ -174,38 +282,89 @@ export function createBuiltinAgents(
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabledAgents.includes("Sisyphus")) {
|
||||
const sisyphusOverride = agentOverrides["Sisyphus"]
|
||||
const sisyphusModel = sisyphusOverride?.model ?? systemDefaultModel
|
||||
|
||||
let sisyphusConfig = createSisyphusAgent(sisyphusModel, availableAgents)
|
||||
|
||||
if (directory && sisyphusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
|
||||
}
|
||||
|
||||
if (sisyphusOverride) {
|
||||
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
|
||||
}
|
||||
|
||||
result["Sisyphus"] = sisyphusConfig
|
||||
}
|
||||
|
||||
if (!disabledAgents.includes("orchestrator-sisyphus")) {
|
||||
const orchestratorOverride = agentOverrides["orchestrator-sisyphus"]
|
||||
const orchestratorModel = orchestratorOverride?.model ?? systemDefaultModel
|
||||
let orchestratorConfig = createOrchestratorSisyphusAgent({
|
||||
model: orchestratorModel,
|
||||
availableAgents,
|
||||
if (!disabledAgents.includes("sisyphus")) {
|
||||
const sisyphusOverride = agentOverrides["sisyphus"]
|
||||
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||
|
||||
const sisyphusResolution = resolveModelWithFallback({
|
||||
uiSelectedModel,
|
||||
userModel: sisyphusOverride?.model,
|
||||
fallbackChain: sisyphusRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (orchestratorOverride) {
|
||||
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
|
||||
if (sisyphusResolution) {
|
||||
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
|
||||
|
||||
let sisyphusConfig = createSisyphusAgent(
|
||||
sisyphusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories
|
||||
)
|
||||
|
||||
if (sisyphusResolvedVariant) {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
|
||||
const sisOverrideCategory = (sisyphusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (sisOverrideCategory) {
|
||||
sisyphusConfig = applyCategoryOverride(sisyphusConfig, sisOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (directory && sisyphusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
|
||||
}
|
||||
|
||||
if (sisyphusOverride) {
|
||||
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
|
||||
}
|
||||
|
||||
result["sisyphus"] = sisyphusConfig
|
||||
}
|
||||
}
|
||||
|
||||
result["orchestrator-sisyphus"] = orchestratorConfig
|
||||
}
|
||||
if (!disabledAgents.includes("atlas")) {
|
||||
const orchestratorOverride = agentOverrides["atlas"]
|
||||
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
const atlasResolution = resolveModelWithFallback({
|
||||
// NOTE: Atlas does NOT use uiSelectedModel - respects its own fallbackChain (k2p5 primary)
|
||||
userModel: orchestratorOverride?.model,
|
||||
fallbackChain: atlasRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (atlasResolution) {
|
||||
const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution
|
||||
|
||||
return result
|
||||
}
|
||||
let orchestratorConfig = createAtlasAgent({
|
||||
model: atlasModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
userCategories: categories,
|
||||
})
|
||||
|
||||
if (atlasResolvedVariant) {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
|
||||
const atlasOverrideCategory = (orchestratorOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (atlasOverrideCategory) {
|
||||
orchestratorConfig = applyCategoryOverride(orchestratorConfig, atlasOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (orchestratorOverride) {
|
||||
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
|
||||
}
|
||||
|
||||
result["atlas"] = orchestratorConfig
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -2,90 +2,73 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
CLI entry point: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics, session runner. Uses Commander.js + @clack/prompts TUI.
|
||||
CLI entry: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics. Commander.js + @clack/prompts.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry, 5 subcommands
|
||||
├── install.ts # Interactive TUI installer (462 lines)
|
||||
├── config-manager.ts # JSONC parsing, multi-level merge (730 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig, DetectedConfig
|
||||
├── index.ts # Commander.js entry (4 commands)
|
||||
├── install.ts # Interactive TUI (520 lines)
|
||||
├── config-manager.ts # JSONC parsing (664 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
├── doctor/
|
||||
│ ├── index.ts # Doctor command entry
|
||||
│ ├── index.ts # Doctor entry
|
||||
│ ├── runner.ts # Check orchestration
|
||||
│ ├── formatter.ts # Colored output, symbols
|
||||
│ ├── constants.ts # Check IDs, categories, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition
|
||||
│ └── checks/ # 14 checks across 6 categories
|
||||
│ ├── formatter.ts # Colored output
|
||||
│ ├── constants.ts # Check IDs, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
|
||||
│ └── checks/ # 14 checks, 21 files
|
||||
│ ├── version.ts # OpenCode + plugin version
|
||||
│ ├── config.ts # JSONC validity, Zod validation
|
||||
│ ├── config.ts # JSONC validity, Zod
|
||||
│ ├── auth.ts # Anthropic, OpenAI, Google
|
||||
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
||||
│ ├── lsp.ts # LSP server connectivity
|
||||
│ ├── mcp.ts # MCP server validation
|
||||
│ └── gh.ts # GitHub CLI availability
|
||||
│ ├── lsp.ts # LSP connectivity
|
||||
│ ├── mcp.ts # MCP validation
|
||||
│ ├── model-resolution.ts # Model resolution check
|
||||
│ └── gh.ts # GitHub CLI
|
||||
├── run/
|
||||
│ ├── index.ts # Run command entry
|
||||
│ └── runner.ts # Session launcher
|
||||
│ └── index.ts # Session launcher
|
||||
└── get-local-version/
|
||||
├── index.ts # Version detection
|
||||
└── formatter.ts # Version output
|
||||
└── index.ts # Version detection
|
||||
```
|
||||
|
||||
## CLI COMMANDS
|
||||
## COMMANDS
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `install` | Interactive setup, subscription detection |
|
||||
| `doctor` | 14 health checks, `--verbose`, `--json`, `--category` |
|
||||
| `run` | Launch OpenCode session with completion enforcement |
|
||||
| `get-local-version` | Version detection, update checking |
|
||||
| `install` | Interactive setup with provider selection |
|
||||
| `doctor` | 14 health checks for diagnostics |
|
||||
| `run` | Launch session with todo enforcement |
|
||||
| `get-local-version` | Version detection and update check |
|
||||
|
||||
## DOCTOR CHECK CATEGORIES
|
||||
## DOCTOR CATEGORIES (14 Checks)
|
||||
|
||||
| Category | Checks |
|
||||
|----------|--------|
|
||||
| installation | opencode, plugin registration |
|
||||
| configuration | config validity, Zod validation |
|
||||
| installation | opencode, plugin |
|
||||
| configuration | config validity, Zod, model-resolution |
|
||||
| authentication | anthropic, openai, google |
|
||||
| dependencies | ast-grep CLI/NAPI, comment-checker |
|
||||
| tools | LSP, MCP connectivity |
|
||||
| dependencies | ast-grep, comment-checker, gh-cli |
|
||||
| tools | LSP, MCP |
|
||||
| updates | version comparison |
|
||||
|
||||
## HOW TO ADD CHECK
|
||||
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`:
|
||||
```typescript
|
||||
export function getMyCheckDefinition(): CheckDefinition {
|
||||
return {
|
||||
id: "my-check",
|
||||
name: "My Check",
|
||||
category: "configuration",
|
||||
check: async () => ({ status: "pass", message: "OK" })
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Export from `checks/index.ts`
|
||||
3. Add to `getAllCheckDefinitions()`
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`
|
||||
2. Export `getXXXCheckDefinition()` factory returning `CheckDefinition`
|
||||
3. Add to `getAllCheckDefinitions()` in `checks/index.ts`
|
||||
|
||||
## TUI FRAMEWORK
|
||||
|
||||
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`, `note()`
|
||||
- **picocolors**: Colored terminal output
|
||||
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), ○ (skip)
|
||||
|
||||
## CONFIG-MANAGER
|
||||
|
||||
- **JSONC**: Comments (`// ...`), block comments, trailing commas
|
||||
- **Multi-source**: User (`~/.config/opencode/`) + Project (`.opencode/`)
|
||||
- **Env override**: `OPENCODE_CONFIG_DIR` for profile isolation
|
||||
- **Validation**: Zod schema with error aggregation
|
||||
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`
|
||||
- **picocolors**: Terminal colors for status and headers
|
||||
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), ℹ (info)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- **Blocking in non-TTY**: Check `process.stdout.isTTY`
|
||||
- **Direct JSON.parse**: Use `parseJsonc()` for config
|
||||
- **Silent failures**: Always return warn/fail in doctor
|
||||
- **Hardcoded paths**: Use `ConfigManager`
|
||||
- **Blocking in non-TTY**: Always check `process.stdout.isTTY`
|
||||
- **Direct JSON.parse**: Use `parseJsonc()` from shared utils
|
||||
- **Silent failures**: Return `warn` or `fail` in doctor instead of throwing
|
||||
- **Hardcoded paths**: Use `getOpenCodeConfigPaths()` from `config-manager.ts`
|
||||
|
||||
1491
src/cli/__snapshots__/model-fallback.test.ts.snap
Normal file
1491
src/cli/__snapshots__/model-fallback.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
@@ -170,7 +170,7 @@ describe("fetchNpmDistTags", () => {
|
||||
})
|
||||
|
||||
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
test("Gemini models include full spec (limit + modalities)", () => {
|
||||
test("all models include full spec (limit + modalities + Antigravity label)", () => {
|
||||
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
||||
expect(google).toBeTruthy()
|
||||
|
||||
@@ -178,9 +178,11 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(models).toBeTruthy()
|
||||
|
||||
const required = [
|
||||
"antigravity-gemini-3-pro-high",
|
||||
"antigravity-gemini-3-pro-low",
|
||||
"antigravity-gemini-3-pro",
|
||||
"antigravity-gemini-3-flash",
|
||||
"antigravity-claude-sonnet-4-5",
|
||||
"antigravity-claude-sonnet-4-5-thinking",
|
||||
"antigravity-claude-opus-4-5-thinking",
|
||||
]
|
||||
|
||||
for (const key of required) {
|
||||
@@ -198,134 +200,212 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(Array.isArray(model.modalities.output)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("Gemini models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Gemini Pro variants
|
||||
const pro = models["antigravity-gemini-3-pro"]
|
||||
// #then should have low and high variants
|
||||
expect(pro.variants).toBeTruthy()
|
||||
expect(pro.variants.low).toBeTruthy()
|
||||
expect(pro.variants.high).toBeTruthy()
|
||||
|
||||
// #when checking Gemini Flash variants
|
||||
const flash = models["antigravity-gemini-3-flash"]
|
||||
// #then should have minimal, low, medium, high variants
|
||||
expect(flash.variants).toBeTruthy()
|
||||
expect(flash.variants.minimal).toBeTruthy()
|
||||
expect(flash.variants.low).toBeTruthy()
|
||||
expect(flash.variants.medium).toBeTruthy()
|
||||
expect(flash.variants.high).toBeTruthy()
|
||||
})
|
||||
|
||||
test("Claude thinking models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Claude thinking variants
|
||||
const sonnetThinking = models["antigravity-claude-sonnet-4-5-thinking"]
|
||||
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
|
||||
|
||||
// #then both should have low and max variants
|
||||
for (const model of [sonnetThinking, opusThinking]) {
|
||||
expect(model.variants).toBeTruthy()
|
||||
expect(model.variants.low).toBeTruthy()
|
||||
expect(model.variants.max).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("generateOmoConfig - GitHub Copilot fallback", () => {
|
||||
test("frontend-ui-ux-engineer uses Copilot when no native providers", () => {
|
||||
// #given user has only Copilot (no Claude, ChatGPT, Gemini)
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then frontend-ui-ux-engineer should use Copilot Gemini
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["frontend-ui-ux-engineer"]?.model).toBe("github-copilot/gemini-3-pro-preview")
|
||||
})
|
||||
|
||||
test("document-writer uses Copilot when no native providers", () => {
|
||||
// #given user has only Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then document-writer should use Copilot Gemini Flash
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["document-writer"]?.model).toBe("github-copilot/gemini-3-flash-preview")
|
||||
})
|
||||
|
||||
test("multimodal-looker uses Copilot when no native providers", () => {
|
||||
// #given user has only Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then multimodal-looker should use Copilot Gemini Flash
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["multimodal-looker"]?.model).toBe("github-copilot/gemini-3-flash-preview")
|
||||
})
|
||||
|
||||
test("explore uses Copilot grok-code when no native providers", () => {
|
||||
// #given user has only Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then explore should use Copilot Grok
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["explore"]?.model).toBe("github-copilot/grok-code-fast-1")
|
||||
})
|
||||
|
||||
test("native Gemini takes priority over Copilot for frontend-ui-ux-engineer", () => {
|
||||
// #given user has both Gemini and Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: true,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then native Gemini should be used (NOT Copilot)
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["frontend-ui-ux-engineer"]?.model).toBe("google/antigravity-gemini-3-pro-high")
|
||||
})
|
||||
|
||||
test("native Claude takes priority over Copilot for frontend-ui-ux-engineer", () => {
|
||||
// #given user has Claude and Copilot but no Gemini
|
||||
describe("generateOmoConfig - model fallback system", () => {
|
||||
test("generates native sonnet models when Claude standard subscription", () => {
|
||||
// #given user has Claude standard subscription (not max20)
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then native Claude should be used (NOT Copilot)
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["frontend-ui-ux-engineer"]?.model).toBe("anthropic/claude-opus-4-5")
|
||||
// #then should use native anthropic sonnet (cost-efficient for standard plan)
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect(result.agents).toBeDefined()
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
})
|
||||
|
||||
test("categories use Copilot models when no native Gemini", () => {
|
||||
// #given user has Copilot but no Gemini
|
||||
test("generates native opus models when Claude max20 subscription", () => {
|
||||
// #given user has Claude max20 subscription
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then should use native anthropic opus (max power for max20 plan)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("uses github-copilot sonnet fallback when only copilot available", () => {
|
||||
// #given user has only copilot (no max plan)
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then categories should use Copilot models
|
||||
const categories = result.categories as Record<string, { model?: string }>
|
||||
expect(categories?.["visual-engineering"]?.model).toBe("github-copilot/gemini-3-pro-preview")
|
||||
expect(categories?.["artistry"]?.model).toBe("github-copilot/gemini-3-pro-preview")
|
||||
expect(categories?.["writing"]?.model).toBe("github-copilot/gemini-3-flash-preview")
|
||||
// #then should use github-copilot sonnet models (copilot fallback)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-sonnet-4.5")
|
||||
})
|
||||
|
||||
test("uses ultimate fallback when no providers configured", () => {
|
||||
// #given user has no providers
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then should use ultimate fallback for all agents
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/glm-4.7-free")
|
||||
})
|
||||
|
||||
test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => {
|
||||
// #given user has Z.ai and Claude max20
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: true,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then librarian should use zai-coding-plan/glm-4.7
|
||||
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
|
||||
// #then other agents should use native opus (max20 plan)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("uses native OpenAI models when only ChatGPT available", () => {
|
||||
// #given user has only ChatGPT subscription
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: true,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then Sisyphus should use native OpenAI (fallback within native tier)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("openai/gpt-5.2")
|
||||
// #then Oracle should use native OpenAI (first fallback entry)
|
||||
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2")
|
||||
// #then multimodal-looker should use native OpenAI (fallback within native tier)
|
||||
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.2")
|
||||
})
|
||||
|
||||
test("uses haiku for explore when Claude max20", () => {
|
||||
// #given user has Claude max20
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then explore should use haiku (max20 plan uses Claude quota)
|
||||
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("uses haiku for explore regardless of max20 flag", () => {
|
||||
// #given user has Claude but not max20
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then explore should use haiku (isMax20 doesn't affect explore anymore)
|
||||
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type OpenCodeConfigPaths,
|
||||
} from "../shared"
|
||||
import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
|
||||
import { generateModelConfig } from "./model-fallback"
|
||||
|
||||
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
||||
|
||||
@@ -307,79 +308,7 @@ function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial
|
||||
}
|
||||
|
||||
export function generateOmoConfig(installConfig: InstallConfig): Record<string, unknown> {
|
||||
const config: Record<string, unknown> = {
|
||||
$schema: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
}
|
||||
|
||||
const agents: Record<string, Record<string, unknown>> = {}
|
||||
|
||||
if (!installConfig.hasClaude) {
|
||||
agents["Sisyphus"] = {
|
||||
model: installConfig.hasCopilot ? "github-copilot/claude-opus-4.5" : "opencode/glm-4.7-free",
|
||||
}
|
||||
}
|
||||
|
||||
agents["librarian"] = { model: "opencode/glm-4.7-free" }
|
||||
|
||||
// Gemini models use `antigravity-` prefix for explicit Antigravity quota routing
|
||||
// @see ANTIGRAVITY_PROVIDER_CONFIG comments for rationale
|
||||
if (installConfig.hasGemini) {
|
||||
agents["explore"] = { model: "google/antigravity-gemini-3-flash" }
|
||||
} else if (installConfig.hasClaude && installConfig.isMax20) {
|
||||
agents["explore"] = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else if (installConfig.hasCopilot) {
|
||||
agents["explore"] = { model: "github-copilot/grok-code-fast-1" }
|
||||
} else {
|
||||
agents["explore"] = { model: "opencode/glm-4.7-free" }
|
||||
}
|
||||
|
||||
if (!installConfig.hasChatGPT) {
|
||||
const oracleFallback = installConfig.hasCopilot
|
||||
? "github-copilot/gpt-5.2"
|
||||
: installConfig.hasClaude
|
||||
? "anthropic/claude-opus-4-5"
|
||||
: "opencode/glm-4.7-free"
|
||||
agents["oracle"] = { model: oracleFallback }
|
||||
}
|
||||
|
||||
if (installConfig.hasGemini) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "google/antigravity-gemini-3-pro-high" }
|
||||
agents["document-writer"] = { model: "google/antigravity-gemini-3-flash" }
|
||||
agents["multimodal-looker"] = { model: "google/antigravity-gemini-3-flash" }
|
||||
} else if (installConfig.hasClaude) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "anthropic/claude-opus-4-5" }
|
||||
agents["document-writer"] = { model: "anthropic/claude-opus-4-5" }
|
||||
agents["multimodal-looker"] = { model: "anthropic/claude-opus-4-5" }
|
||||
} else if (installConfig.hasCopilot) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "github-copilot/gemini-3-pro-preview" }
|
||||
agents["document-writer"] = { model: "github-copilot/gemini-3-flash-preview" }
|
||||
agents["multimodal-looker"] = { model: "github-copilot/gemini-3-flash-preview" }
|
||||
} else {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "opencode/glm-4.7-free" }
|
||||
agents["document-writer"] = { model: "opencode/glm-4.7-free" }
|
||||
agents["multimodal-looker"] = { model: "opencode/glm-4.7-free" }
|
||||
}
|
||||
|
||||
if (Object.keys(agents).length > 0) {
|
||||
config.agents = agents
|
||||
}
|
||||
|
||||
// Categories: override model for Antigravity auth or GitHub Copilot fallback
|
||||
if (installConfig.hasGemini) {
|
||||
config.categories = {
|
||||
"visual-engineering": { model: "google/gemini-3-pro-high" },
|
||||
artistry: { model: "google/gemini-3-pro-high" },
|
||||
writing: { model: "google/gemini-3-flash-high" },
|
||||
}
|
||||
} else if (installConfig.hasCopilot) {
|
||||
config.categories = {
|
||||
"visual-engineering": { model: "github-copilot/gemini-3-pro-preview" },
|
||||
artistry: { model: "github-copilot/gemini-3-pro-preview" },
|
||||
writing: { model: "github-copilot/gemini-3-flash-preview" },
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
return generateModelConfig(installConfig)
|
||||
}
|
||||
|
||||
export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult {
|
||||
@@ -568,38 +497,61 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
*
|
||||
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
|
||||
*
|
||||
* The opencode-antigravity-auth plugin supports two naming conventions:
|
||||
* - `antigravity-gemini-3-pro-high` (RECOMMENDED, explicit Antigravity quota routing)
|
||||
* - `gemini-3-pro-high` (LEGACY, backward compatible but may break in future)
|
||||
* Since opencode-antigravity-auth v1.3.0, models use a variant system:
|
||||
* - `antigravity-gemini-3-pro` with variants: low, high
|
||||
* - `antigravity-gemini-3-flash` with variants: minimal, low, medium, high
|
||||
*
|
||||
* Legacy names rely on Gemini CLI using `-preview` suffix for disambiguation.
|
||||
* If Google removes `-preview`, legacy names may route to wrong quota.
|
||||
* Legacy tier-suffixed names (e.g., `antigravity-gemini-3-pro-high`) still work
|
||||
* but variants are the recommended approach.
|
||||
*
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#migration-guide-v127
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#models
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
models: {
|
||||
"antigravity-gemini-3-pro-high": {
|
||||
name: "Gemini 3 Pro High (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"antigravity-gemini-3-pro-low": {
|
||||
name: "Gemini 3 Pro Low (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
"antigravity-gemini-3-pro": {
|
||||
name: "Gemini 3 Pro (Antigravity)",
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingLevel: "low" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-gemini-3-flash": {
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65536 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
minimal: { thinkingLevel: "minimal" },
|
||||
low: { thinkingLevel: "low" },
|
||||
medium: { thinkingLevel: "medium" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-sonnet-4-5": {
|
||||
name: "Claude Sonnet 4.5 (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"antigravity-claude-sonnet-4-5-thinking": {
|
||||
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-opus-4-5-thinking": {
|
||||
name: "Claude Opus 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -646,8 +598,29 @@ export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
}
|
||||
}
|
||||
|
||||
interface OmoConfigData {
|
||||
agents?: Record<string, { model?: string }>
|
||||
function detectProvidersFromOmoConfig(): { hasOpenAI: boolean; hasOpencodeZen: boolean; hasZaiCodingPlan: boolean; hasKimiForCoding: boolean } {
|
||||
const omoConfigPath = getOmoConfig()
|
||||
if (!existsSync(omoConfigPath)) {
|
||||
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(omoConfigPath, "utf-8")
|
||||
const omoConfig = parseJsonc<Record<string, unknown>>(content)
|
||||
if (!omoConfig || typeof omoConfig !== "object") {
|
||||
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false }
|
||||
}
|
||||
|
||||
const configStr = JSON.stringify(omoConfig)
|
||||
const hasOpenAI = configStr.includes('"openai/')
|
||||
const hasOpencodeZen = configStr.includes('"opencode/')
|
||||
const hasZaiCodingPlan = configStr.includes('"zai-coding-plan/')
|
||||
const hasKimiForCoding = configStr.includes('"kimi-for-coding/')
|
||||
|
||||
return { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding }
|
||||
} catch {
|
||||
return { hasOpenAI: true, hasOpencodeZen: true, hasZaiCodingPlan: false, hasKimiForCoding: false }
|
||||
}
|
||||
}
|
||||
|
||||
export function detectCurrentConfig(): DetectedConfig {
|
||||
@@ -655,9 +628,12 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
isInstalled: false,
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
hasChatGPT: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: true,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
@@ -678,53 +654,14 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
return result
|
||||
}
|
||||
|
||||
// Gemini auth plugin detection still works via plugin presence
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
|
||||
const omoConfigPath = getOmoConfig()
|
||||
if (!existsSync(omoConfigPath)) {
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = statSync(omoConfigPath)
|
||||
if (stat.size === 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
const content = readFileSync(omoConfigPath, "utf-8")
|
||||
if (isEmptyOrWhitespace(content)) {
|
||||
return result
|
||||
}
|
||||
|
||||
const omoConfig = parseJsonc<OmoConfigData>(content)
|
||||
if (!omoConfig || typeof omoConfig !== "object") {
|
||||
return result
|
||||
}
|
||||
|
||||
const agents = omoConfig.agents ?? {}
|
||||
|
||||
if (agents["Sisyphus"]?.model === "opencode/glm-4.7-free") {
|
||||
result.hasClaude = false
|
||||
result.isMax20 = false
|
||||
} else if (agents["librarian"]?.model === "opencode/glm-4.7-free") {
|
||||
result.hasClaude = true
|
||||
result.isMax20 = false
|
||||
}
|
||||
|
||||
if (agents["oracle"]?.model?.startsWith("anthropic/")) {
|
||||
result.hasChatGPT = false
|
||||
} else if (agents["oracle"]?.model === "opencode/glm-4.7-free") {
|
||||
result.hasChatGPT = false
|
||||
}
|
||||
|
||||
const hasAnyCopilotModel = Object.values(agents).some(
|
||||
(agent) => agent?.model?.startsWith("github-copilot/")
|
||||
)
|
||||
result.hasCopilot = hasAnyCopilotModel
|
||||
|
||||
} catch {
|
||||
/* intentionally empty - malformed omo config returns defaults from opencode config detection */
|
||||
}
|
||||
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
|
||||
result.hasOpenAI = hasOpenAI
|
||||
result.hasOpencodeZen = hasOpencodeZen
|
||||
result.hasZaiCodingPlan = hasZaiCodingPlan
|
||||
result.hasKimiForCoding = hasKimiForCoding
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { CheckResult, CheckDefinition, AuthProviderInfo, AuthProviderId } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { parseJsonc } from "../../../shared"
|
||||
import { parseJsonc, getOpenCodeConfigDir } from "../../../shared"
|
||||
|
||||
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const OPENCODE_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
|
||||
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { CheckResult, CheckDefinition, ConfigInfo } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
|
||||
import { parseJsonc, detectConfigFile } from "../../../shared"
|
||||
import { parseJsonc, detectConfigFile, getOpenCodeConfigDir } from "../../../shared"
|
||||
import { OhMyOpenCodeConfigSchema } from "../../../config"
|
||||
|
||||
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
|
||||
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, `${PACKAGE_NAME}`)
|
||||
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ describe("dependencies check", () => {
|
||||
})
|
||||
|
||||
describe("checkAstGrepNapi", () => {
|
||||
it("returns dependency info", () => {
|
||||
it("returns dependency info", async () => {
|
||||
// #given
|
||||
// #when checking ast-grep napi
|
||||
const info = deps.checkAstGrepNapi()
|
||||
const info = await deps.checkAstGrepNapi()
|
||||
|
||||
// #then should return valid info
|
||||
expect(info.name).toBe("AST-Grep NAPI")
|
||||
@@ -95,7 +95,7 @@ describe("dependencies check", () => {
|
||||
|
||||
it("returns pass when installed", async () => {
|
||||
// #given napi installed
|
||||
checkSpy = spyOn(deps, "checkAstGrepNapi").mockReturnValue({
|
||||
checkSpy = spyOn(deps, "checkAstGrepNapi").mockResolvedValue({
|
||||
name: "AST-Grep NAPI",
|
||||
required: false,
|
||||
installed: true,
|
||||
|
||||
@@ -56,9 +56,10 @@ export async function checkAstGrepCli(): Promise<DependencyInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
export function checkAstGrepNapi(): DependencyInfo {
|
||||
export async function checkAstGrepNapi(): Promise<DependencyInfo> {
|
||||
// Try dynamic import first (works in bunx temporary environments)
|
||||
try {
|
||||
require.resolve("@ast-grep/napi")
|
||||
await import("@ast-grep/napi")
|
||||
return {
|
||||
name: "AST-Grep NAPI",
|
||||
required: false,
|
||||
@@ -67,6 +68,28 @@ export function checkAstGrepNapi(): DependencyInfo {
|
||||
path: null,
|
||||
}
|
||||
} catch {
|
||||
// Fallback: check common installation paths
|
||||
const { existsSync } = await import("fs")
|
||||
const { join } = await import("path")
|
||||
const { homedir } = await import("os")
|
||||
|
||||
const pathsToCheck = [
|
||||
join(homedir(), ".config", "opencode", "node_modules", "@ast-grep", "napi"),
|
||||
join(process.cwd(), "node_modules", "@ast-grep", "napi"),
|
||||
]
|
||||
|
||||
for (const napiPath of pathsToCheck) {
|
||||
if (existsSync(napiPath)) {
|
||||
return {
|
||||
name: "AST-Grep NAPI",
|
||||
required: false,
|
||||
installed: true,
|
||||
version: null,
|
||||
path: napiPath,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "AST-Grep NAPI",
|
||||
required: false,
|
||||
@@ -127,7 +150,7 @@ export async function checkDependencyAstGrepCli(): Promise<CheckResult> {
|
||||
}
|
||||
|
||||
export async function checkDependencyAstGrepNapi(): Promise<CheckResult> {
|
||||
const info = checkAstGrepNapi()
|
||||
const info = await checkAstGrepNapi()
|
||||
return dependencyToCheckResult(info, CHECK_NAMES[CHECK_IDS.DEP_AST_GREP_NAPI])
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,25 @@ import type { CheckDefinition } from "../types"
|
||||
import { getOpenCodeCheckDefinition } from "./opencode"
|
||||
import { getPluginCheckDefinition } from "./plugin"
|
||||
import { getConfigCheckDefinition } from "./config"
|
||||
import { getModelResolutionCheckDefinition } from "./model-resolution"
|
||||
import { getAuthCheckDefinitions } from "./auth"
|
||||
import { getDependencyCheckDefinitions } from "./dependencies"
|
||||
import { getGhCliCheckDefinition } from "./gh"
|
||||
import { getLspCheckDefinition } from "./lsp"
|
||||
import { getMcpCheckDefinitions } from "./mcp"
|
||||
import { getMcpOAuthCheckDefinition } from "./mcp-oauth"
|
||||
import { getVersionCheckDefinition } from "./version"
|
||||
|
||||
export * from "./opencode"
|
||||
export * from "./plugin"
|
||||
export * from "./config"
|
||||
export * from "./model-resolution"
|
||||
export * from "./auth"
|
||||
export * from "./dependencies"
|
||||
export * from "./gh"
|
||||
export * from "./lsp"
|
||||
export * from "./mcp"
|
||||
export * from "./mcp-oauth"
|
||||
export * from "./version"
|
||||
|
||||
export function getAllCheckDefinitions(): CheckDefinition[] {
|
||||
@@ -24,11 +28,13 @@ export function getAllCheckDefinitions(): CheckDefinition[] {
|
||||
getOpenCodeCheckDefinition(),
|
||||
getPluginCheckDefinition(),
|
||||
getConfigCheckDefinition(),
|
||||
getModelResolutionCheckDefinition(),
|
||||
...getAuthCheckDefinitions(),
|
||||
...getDependencyCheckDefinitions(),
|
||||
getGhCliCheckDefinition(),
|
||||
getLspCheckDefinition(),
|
||||
...getMcpCheckDefinitions(),
|
||||
getMcpOAuthCheckDefinition(),
|
||||
getVersionCheckDefinition(),
|
||||
]
|
||||
}
|
||||
|
||||
133
src/cli/doctor/checks/mcp-oauth.test.ts
Normal file
133
src/cli/doctor/checks/mcp-oauth.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, it, expect, spyOn, afterEach } from "bun:test"
|
||||
import * as mcpOauth from "./mcp-oauth"
|
||||
|
||||
describe("mcp-oauth check", () => {
|
||||
describe("getMcpOAuthCheckDefinition", () => {
|
||||
it("returns check definition with correct properties", () => {
|
||||
// #given
|
||||
// #when getting definition
|
||||
const def = mcpOauth.getMcpOAuthCheckDefinition()
|
||||
|
||||
// #then should have correct structure
|
||||
expect(def.id).toBe("mcp-oauth-tokens")
|
||||
expect(def.name).toBe("MCP OAuth Tokens")
|
||||
expect(def.category).toBe("tools")
|
||||
expect(def.critical).toBe(false)
|
||||
expect(typeof def.check).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("checkMcpOAuthTokens", () => {
|
||||
let readStoreSpy: ReturnType<typeof spyOn>
|
||||
|
||||
afterEach(() => {
|
||||
readStoreSpy?.mockRestore()
|
||||
})
|
||||
|
||||
it("returns skip when no tokens stored", async () => {
|
||||
// #given no OAuth tokens configured
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue(null)
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should skip
|
||||
expect(result.status).toBe("skip")
|
||||
expect(result.message).toContain("No OAuth")
|
||||
})
|
||||
|
||||
it("returns pass when all tokens valid", async () => {
|
||||
// #given valid tokens with future expiry (expiresAt is in epoch seconds)
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"example.com/resource1": {
|
||||
accessToken: "token1",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
"example.com/resource2": {
|
||||
accessToken: "token2",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should pass
|
||||
expect(result.status).toBe("pass")
|
||||
expect(result.message).toContain("2")
|
||||
expect(result.message).toContain("valid")
|
||||
})
|
||||
|
||||
it("returns warn when some tokens expired", async () => {
|
||||
// #given mix of valid and expired tokens (expiresAt is in epoch seconds)
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
||||
const pastTime = Math.floor(Date.now() / 1000) - 3600
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"example.com/resource1": {
|
||||
accessToken: "token1",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
"example.com/resource2": {
|
||||
accessToken: "token2",
|
||||
expiresAt: pastTime,
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should warn
|
||||
expect(result.status).toBe("warn")
|
||||
expect(result.message).toContain("1")
|
||||
expect(result.message).toContain("expired")
|
||||
expect(result.details?.some((d: string) => d.includes("Expired"))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("returns pass when tokens have no expiry", async () => {
|
||||
// #given tokens without expiry info
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"example.com/resource1": {
|
||||
accessToken: "token1",
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should pass (no expiry = assume valid)
|
||||
expect(result.status).toBe("pass")
|
||||
expect(result.message).toContain("1")
|
||||
})
|
||||
|
||||
it("includes token details in output", async () => {
|
||||
// #given multiple tokens
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"api.example.com/v1": {
|
||||
accessToken: "token1",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
"auth.example.com/oauth": {
|
||||
accessToken: "token2",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should list tokens in details
|
||||
expect(result.details).toBeDefined()
|
||||
expect(result.details?.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
result.details?.some((d: string) => d.includes("api.example.com"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
result.details?.some((d: string) => d.includes("auth.example.com"))
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
80
src/cli/doctor/checks/mcp-oauth.ts
Normal file
80
src/cli/doctor/checks/mcp-oauth.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { CheckResult, CheckDefinition } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { getMcpOauthStoragePath } from "../../../features/mcp-oauth/storage"
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
|
||||
interface OAuthTokenData {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
expiresAt?: number
|
||||
clientInfo?: {
|
||||
clientId: string
|
||||
clientSecret?: string
|
||||
}
|
||||
}
|
||||
|
||||
type TokenStore = Record<string, OAuthTokenData>
|
||||
|
||||
export function readTokenStore(): TokenStore | null {
|
||||
const filePath = getMcpOauthStoragePath()
|
||||
if (!existsSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8")
|
||||
return JSON.parse(content) as TokenStore
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkMcpOAuthTokens(): Promise<CheckResult> {
|
||||
const store = readTokenStore()
|
||||
|
||||
if (!store || Object.keys(store).length === 0) {
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
status: "skip",
|
||||
message: "No OAuth tokens configured",
|
||||
details: ["Optional: Configure OAuth tokens for MCP servers"],
|
||||
}
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const tokens = Object.entries(store)
|
||||
const expiredTokens = tokens.filter(
|
||||
([, token]) => token.expiresAt && token.expiresAt < now
|
||||
)
|
||||
|
||||
if (expiredTokens.length > 0) {
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
status: "warn",
|
||||
message: `${expiredTokens.length} of ${tokens.length} token(s) expired`,
|
||||
details: [
|
||||
...tokens
|
||||
.filter(([, token]) => !token.expiresAt || token.expiresAt >= now)
|
||||
.map(([key]) => `Valid: ${key}`),
|
||||
...expiredTokens.map(([key]) => `Expired: ${key}`),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
status: "pass",
|
||||
message: `${tokens.length} OAuth token(s) valid`,
|
||||
details: tokens.map(([key]) => `Configured: ${key}`),
|
||||
}
|
||||
}
|
||||
|
||||
export function getMcpOAuthCheckDefinition(): CheckDefinition {
|
||||
return {
|
||||
id: CHECK_IDS.MCP_OAUTH_TOKENS,
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
category: "tools",
|
||||
check: checkMcpOAuthTokens,
|
||||
critical: false,
|
||||
}
|
||||
}
|
||||
141
src/cli/doctor/checks/model-resolution.test.ts
Normal file
141
src/cli/doctor/checks/model-resolution.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, spyOn, mock } from "bun:test"
|
||||
|
||||
describe("model-resolution check", () => {
|
||||
describe("getModelResolutionInfo", () => {
|
||||
// #given: Model requirements are defined in model-requirements.ts
|
||||
// #when: Getting model resolution info
|
||||
// #then: Returns info for all agents and categories with their provider chains
|
||||
|
||||
it("returns agent requirements with provider chains", async () => {
|
||||
const { getModelResolutionInfo } = await import("./model-resolution")
|
||||
|
||||
const info = getModelResolutionInfo()
|
||||
|
||||
// #then: Should have agent entries
|
||||
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
|
||||
expect(sisyphus).toBeDefined()
|
||||
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-5")
|
||||
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("anthropic")
|
||||
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("github-copilot")
|
||||
})
|
||||
|
||||
it("returns category requirements with provider chains", async () => {
|
||||
const { getModelResolutionInfo } = await import("./model-resolution")
|
||||
|
||||
const info = getModelResolutionInfo()
|
||||
|
||||
// #then: Should have category entries
|
||||
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||
expect(visual).toBeDefined()
|
||||
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro")
|
||||
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("google")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelResolutionInfoWithOverrides", () => {
|
||||
// #given: User has overrides in oh-my-opencode.json
|
||||
// #when: Getting resolution info with config
|
||||
// #then: Shows user override in Step 1 position
|
||||
|
||||
it("shows user override for agent when configured", async () => {
|
||||
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
|
||||
|
||||
// #given: User has override for oracle agent
|
||||
const mockConfig = {
|
||||
agents: {
|
||||
oracle: { model: "anthropic/claude-opus-4-5" },
|
||||
},
|
||||
}
|
||||
|
||||
const info = getModelResolutionInfoWithOverrides(mockConfig)
|
||||
|
||||
// #then: Oracle should show the override
|
||||
const oracle = info.agents.find((a) => a.name === "oracle")
|
||||
expect(oracle).toBeDefined()
|
||||
expect(oracle!.userOverride).toBe("anthropic/claude-opus-4-5")
|
||||
expect(oracle!.effectiveResolution).toBe("User override: anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
it("shows user override for category when configured", async () => {
|
||||
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
|
||||
|
||||
// #given: User has override for visual-engineering category
|
||||
const mockConfig = {
|
||||
categories: {
|
||||
"visual-engineering": { model: "openai/gpt-5.2" },
|
||||
},
|
||||
}
|
||||
|
||||
const info = getModelResolutionInfoWithOverrides(mockConfig)
|
||||
|
||||
// #then: visual-engineering should show the override
|
||||
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||
expect(visual).toBeDefined()
|
||||
expect(visual!.userOverride).toBe("openai/gpt-5.2")
|
||||
expect(visual!.effectiveResolution).toBe("User override: openai/gpt-5.2")
|
||||
})
|
||||
|
||||
it("shows provider fallback when no override exists", async () => {
|
||||
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
|
||||
|
||||
// #given: No overrides configured
|
||||
const mockConfig = {}
|
||||
|
||||
const info = getModelResolutionInfoWithOverrides(mockConfig)
|
||||
|
||||
// #then: Should show provider fallback chain
|
||||
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
|
||||
expect(sisyphus).toBeDefined()
|
||||
expect(sisyphus!.userOverride).toBeUndefined()
|
||||
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
|
||||
expect(sisyphus!.effectiveResolution).toContain("anthropic")
|
||||
})
|
||||
})
|
||||
|
||||
describe("checkModelResolution", () => {
|
||||
// #given: Doctor check is executed
|
||||
// #when: Running the model resolution check
|
||||
// #then: Returns pass with details showing resolution flow
|
||||
|
||||
it("returns pass or warn status with agent and category counts", async () => {
|
||||
const { checkModelResolution } = await import("./model-resolution")
|
||||
|
||||
const result = await checkModelResolution()
|
||||
|
||||
// #then: Should pass (with cache) or warn (no cache) and show counts
|
||||
// In CI without model cache, status is "warn"; locally with cache, status is "pass"
|
||||
expect(["pass", "warn"]).toContain(result.status)
|
||||
expect(result.message).toMatch(/\d+ agents?, \d+ categories?/)
|
||||
})
|
||||
|
||||
it("includes resolution details in verbose mode details array", async () => {
|
||||
const { checkModelResolution } = await import("./model-resolution")
|
||||
|
||||
const result = await checkModelResolution()
|
||||
|
||||
// #then: Details should contain agent/category resolution info
|
||||
expect(result.details).toBeDefined()
|
||||
expect(result.details!.length).toBeGreaterThan(0)
|
||||
// Should have Available Models and Configured Models headers
|
||||
expect(result.details!.some((d) => d.includes("Available Models"))).toBe(true)
|
||||
expect(result.details!.some((d) => d.includes("Configured Models"))).toBe(true)
|
||||
expect(result.details!.some((d) => d.includes("Agents:"))).toBe(true)
|
||||
expect(result.details!.some((d) => d.includes("Categories:"))).toBe(true)
|
||||
// Should have legend
|
||||
expect(result.details!.some((d) => d.includes("user override"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModelResolutionCheckDefinition", () => {
|
||||
it("returns valid check definition", async () => {
|
||||
const { getModelResolutionCheckDefinition } = await import("./model-resolution")
|
||||
|
||||
const def = getModelResolutionCheckDefinition()
|
||||
|
||||
expect(def.id).toBe("model-resolution")
|
||||
expect(def.name).toBe("Model Resolution")
|
||||
expect(def.category).toBe("configuration")
|
||||
expect(typeof def.check).toBe("function")
|
||||
})
|
||||
})
|
||||
})
|
||||
264
src/cli/doctor/checks/model-resolution.ts
Normal file
264
src/cli/doctor/checks/model-resolution.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { readFileSync, existsSync } from "node:fs"
|
||||
import type { CheckResult, CheckDefinition } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { parseJsonc, detectConfigFile } from "../../../shared"
|
||||
import {
|
||||
AGENT_MODEL_REQUIREMENTS,
|
||||
CATEGORY_MODEL_REQUIREMENTS,
|
||||
type ModelRequirement,
|
||||
} from "../../../shared/model-requirements"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
function getOpenCodeCacheDir(): string {
|
||||
const xdgCache = process.env.XDG_CACHE_HOME
|
||||
if (xdgCache) return join(xdgCache, "opencode")
|
||||
return join(homedir(), ".cache", "opencode")
|
||||
}
|
||||
|
||||
function loadAvailableModels(): { providers: string[]; modelCount: number; cacheExists: boolean } {
|
||||
const cacheFile = join(getOpenCodeCacheDir(), "models.json")
|
||||
|
||||
if (!existsSync(cacheFile)) {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(cacheFile, "utf-8")
|
||||
const data = JSON.parse(content) as Record<string, { models?: Record<string, unknown> }>
|
||||
|
||||
const providers = Object.keys(data)
|
||||
let modelCount = 0
|
||||
for (const providerId of providers) {
|
||||
const models = data[providerId]?.models
|
||||
if (models && typeof models === "object") {
|
||||
modelCount += Object.keys(models).length
|
||||
}
|
||||
}
|
||||
|
||||
return { providers, modelCount, cacheExists: true }
|
||||
} catch {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, PACKAGE_NAME)
|
||||
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
|
||||
|
||||
export interface AgentResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface CategoryResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface ModelResolutionInfo {
|
||||
agents: AgentResolutionInfo[]
|
||||
categories: CategoryResolutionInfo[]
|
||||
}
|
||||
|
||||
interface OmoConfig {
|
||||
agents?: Record<string, { model?: string }>
|
||||
categories?: Record<string, { model?: string }>
|
||||
}
|
||||
|
||||
function loadConfig(): OmoConfig | null {
|
||||
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
|
||||
if (projectDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(projectDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const userDetected = detectConfigFile(USER_CONFIG_BASE)
|
||||
if (userDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(userDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function formatProviderChain(providers: string[]): string {
|
||||
return providers.join(" → ")
|
||||
}
|
||||
|
||||
function getEffectiveModel(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return userOverride
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "unknown"
|
||||
}
|
||||
return `${firstEntry.providers[0]}/${firstEntry.model}`
|
||||
}
|
||||
|
||||
function buildEffectiveResolution(
|
||||
requirement: ModelRequirement,
|
||||
userOverride?: string,
|
||||
): string {
|
||||
if (userOverride) {
|
||||
return `User override: ${userOverride}`
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "No fallback chain defined"
|
||||
}
|
||||
return `Provider fallback: ${formatProviderChain(firstEntry.providers)} → ${firstEntry.model}`
|
||||
}
|
||||
|
||||
export function getModelResolutionInfo(): ModelResolutionInfo {
|
||||
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
|
||||
([name, requirement]) => ({
|
||||
name,
|
||||
requirement,
|
||||
effectiveModel: getEffectiveModel(requirement),
|
||||
effectiveResolution: buildEffectiveResolution(requirement),
|
||||
}),
|
||||
)
|
||||
|
||||
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
|
||||
([name, requirement]) => ({
|
||||
name,
|
||||
requirement,
|
||||
effectiveModel: getEffectiveModel(requirement),
|
||||
effectiveResolution: buildEffectiveResolution(requirement),
|
||||
}),
|
||||
)
|
||||
|
||||
return { agents, categories }
|
||||
}
|
||||
|
||||
export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelResolutionInfo {
|
||||
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
|
||||
([name, requirement]) => {
|
||||
const userOverride = config.agents?.[name]?.model
|
||||
return {
|
||||
name,
|
||||
requirement,
|
||||
userOverride,
|
||||
effectiveModel: getEffectiveModel(requirement, userOverride),
|
||||
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
|
||||
([name, requirement]) => {
|
||||
const userOverride = config.categories?.[name]?.model
|
||||
return {
|
||||
name,
|
||||
requirement,
|
||||
userOverride,
|
||||
effectiveModel: getEffectiveModel(requirement, userOverride),
|
||||
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return { agents, categories }
|
||||
}
|
||||
|
||||
function formatModelWithVariant(model: string, variant?: string): string {
|
||||
return variant ? `${model} (${variant})` : model
|
||||
}
|
||||
|
||||
function getEffectiveVariant(requirement: ModelRequirement): string | undefined {
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
interface AvailableModelsInfo {
|
||||
providers: string[]
|
||||
modelCount: number
|
||||
cacheExists: boolean
|
||||
}
|
||||
|
||||
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo): string[] {
|
||||
const details: string[] = []
|
||||
|
||||
details.push("═══ Available Models (from cache) ═══")
|
||||
details.push("")
|
||||
if (available.cacheExists) {
|
||||
details.push(` Providers in cache: ${available.providers.length}`)
|
||||
details.push(` Sample: ${available.providers.slice(0, 6).join(", ")}${available.providers.length > 6 ? "..." : ""}`)
|
||||
details.push(` Total models: ${available.modelCount}`)
|
||||
details.push(` Cache: ~/.cache/opencode/models.json`)
|
||||
details.push(` ℹ Runtime: only connected providers used`)
|
||||
details.push(` Refresh: opencode models --refresh`)
|
||||
} else {
|
||||
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
|
||||
}
|
||||
details.push("")
|
||||
|
||||
details.push("═══ Configured Models ═══")
|
||||
details.push("")
|
||||
details.push("Agents:")
|
||||
for (const agent of info.agents) {
|
||||
const marker = agent.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(agent.effectiveModel, getEffectiveVariant(agent.requirement))
|
||||
details.push(` ${marker} ${agent.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("Categories:")
|
||||
for (const category of info.categories) {
|
||||
const marker = category.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(category.effectiveModel, getEffectiveVariant(category.requirement))
|
||||
details.push(` ${marker} ${category.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("● = user override, ○ = provider fallback")
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
export async function checkModelResolution(): Promise<CheckResult> {
|
||||
const config = loadConfig() ?? {}
|
||||
const info = getModelResolutionInfoWithOverrides(config)
|
||||
const available = loadAvailableModels()
|
||||
|
||||
const agentCount = info.agents.length
|
||||
const categoryCount = info.categories.length
|
||||
const agentOverrides = info.agents.filter((a) => a.userOverride).length
|
||||
const categoryOverrides = info.categories.filter((c) => c.userOverride).length
|
||||
const totalOverrides = agentOverrides + categoryOverrides
|
||||
|
||||
const overrideNote = totalOverrides > 0 ? ` (${totalOverrides} override${totalOverrides > 1 ? "s" : ""})` : ""
|
||||
const cacheNote = available.cacheExists ? `, ${available.modelCount} available` : ", cache not found"
|
||||
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
|
||||
status: available.cacheExists ? "pass" : "warn",
|
||||
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
|
||||
details: buildDetailsArray(info, available),
|
||||
}
|
||||
}
|
||||
|
||||
export function getModelResolutionCheckDefinition(): CheckDefinition {
|
||||
return {
|
||||
id: CHECK_IDS.MODEL_RESOLUTION,
|
||||
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
|
||||
category: "configuration",
|
||||
check: checkModelResolution,
|
||||
critical: false,
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ function findPluginEntry(plugins: string[]): { entry: string; isPinned: boolean;
|
||||
const version = isPinned ? plugin.split("@")[1] : null
|
||||
return { entry: plugin, isPinned, version }
|
||||
}
|
||||
if (plugin.startsWith("file://") && plugin.includes(PACKAGE_NAME)) {
|
||||
return { entry: plugin, isPinned: false, version: "local-dev" }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const CHECK_IDS = {
|
||||
OPENCODE_INSTALLATION: "opencode-installation",
|
||||
PLUGIN_REGISTRATION: "plugin-registration",
|
||||
CONFIG_VALIDATION: "config-validation",
|
||||
MODEL_RESOLUTION: "model-resolution",
|
||||
AUTH_ANTHROPIC: "auth-anthropic",
|
||||
AUTH_OPENAI: "auth-openai",
|
||||
AUTH_GOOGLE: "auth-google",
|
||||
@@ -31,6 +32,7 @@ export const CHECK_IDS = {
|
||||
LSP_SERVERS: "lsp-servers",
|
||||
MCP_BUILTIN: "mcp-builtin",
|
||||
MCP_USER: "mcp-user",
|
||||
MCP_OAUTH_TOKENS: "mcp-oauth-tokens",
|
||||
VERSION_STATUS: "version-status",
|
||||
} as const
|
||||
|
||||
@@ -38,6 +40,7 @@ export const CHECK_NAMES: Record<string, string> = {
|
||||
[CHECK_IDS.OPENCODE_INSTALLATION]: "OpenCode Installation",
|
||||
[CHECK_IDS.PLUGIN_REGISTRATION]: "Plugin Registration",
|
||||
[CHECK_IDS.CONFIG_VALIDATION]: "Configuration Validity",
|
||||
[CHECK_IDS.MODEL_RESOLUTION]: "Model Resolution",
|
||||
[CHECK_IDS.AUTH_ANTHROPIC]: "Anthropic (Claude) Auth",
|
||||
[CHECK_IDS.AUTH_OPENAI]: "OpenAI (ChatGPT) Auth",
|
||||
[CHECK_IDS.AUTH_GOOGLE]: "Google (Gemini) Auth",
|
||||
@@ -48,6 +51,7 @@ export const CHECK_NAMES: Record<string, string> = {
|
||||
[CHECK_IDS.LSP_SERVERS]: "LSP Servers",
|
||||
[CHECK_IDS.MCP_BUILTIN]: "Built-in MCP Servers",
|
||||
[CHECK_IDS.MCP_USER]: "User MCP Configuration",
|
||||
[CHECK_IDS.MCP_OAUTH_TOKENS]: "MCP OAuth Tokens",
|
||||
[CHECK_IDS.VERSION_STATUS]: "Version Status",
|
||||
} as const
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import color from "picocolors"
|
||||
import type { VersionInfo } from "./types"
|
||||
|
||||
const SYMBOLS = {
|
||||
check: color.green("✓"),
|
||||
cross: color.red("✗"),
|
||||
arrow: color.cyan("→"),
|
||||
info: color.blue("ℹ"),
|
||||
warn: color.yellow("⚠"),
|
||||
pin: color.magenta("📌"),
|
||||
dev: color.cyan("🔧"),
|
||||
check: color.green("[OK]"),
|
||||
cross: color.red("[X]"),
|
||||
arrow: color.cyan("->"),
|
||||
info: color.blue("[i]"),
|
||||
warn: color.yellow("[!]"),
|
||||
pin: color.magenta("[PINNED]"),
|
||||
dev: color.cyan("[DEV]"),
|
||||
}
|
||||
|
||||
export function formatVersionOutput(info: VersionInfo): string {
|
||||
|
||||
17
src/cli/index.test.ts
Normal file
17
src/cli/index.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import packageJson from "../../package.json" with { type: "json" }
|
||||
|
||||
describe("CLI version", () => {
|
||||
it("reads version from package.json as valid semver", () => {
|
||||
//#given
|
||||
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/
|
||||
|
||||
//#when
|
||||
const version = packageJson.version
|
||||
|
||||
//#then
|
||||
expect(version).toMatch(semverRegex)
|
||||
expect(typeof version).toBe("string")
|
||||
expect(version.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import { install } from "./install"
|
||||
import { run } from "./run"
|
||||
import { getLocalVersion } from "./get-local-version"
|
||||
import { doctor } from "./doctor"
|
||||
import { createMcpOAuthCommand } from "./mcp-oauth"
|
||||
import type { InstallArgs } from "./types"
|
||||
import type { RunOptions } from "./run"
|
||||
import type { GetLocalVersionOptions } from "./get-local-version/types"
|
||||
@@ -24,28 +25,38 @@ program
|
||||
.description("Install and configure oh-my-opencode with interactive setup")
|
||||
.option("--no-tui", "Run in non-interactive mode (requires all options)")
|
||||
.option("--claude <value>", "Claude subscription: no, yes, max20")
|
||||
.option("--chatgpt <value>", "ChatGPT subscription: no, yes")
|
||||
.option("--openai <value>", "OpenAI/ChatGPT subscription: no, yes (default: no)")
|
||||
.option("--gemini <value>", "Gemini integration: no, yes")
|
||||
.option("--copilot <value>", "GitHub Copilot subscription: no, yes")
|
||||
.option("--opencode-zen <value>", "OpenCode Zen access: no, yes (default: no)")
|
||||
.option("--zai-coding-plan <value>", "Z.ai Coding Plan subscription: no, yes (default: no)")
|
||||
.option("--kimi-for-coding <value>", "Kimi For Coding subscription: no, yes (default: no)")
|
||||
.option("--skip-auth", "Skip authentication setup hints")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode install
|
||||
$ bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes --copilot=no
|
||||
$ bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes
|
||||
$ bunx oh-my-opencode install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no
|
||||
$ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes --opencode-zen=yes
|
||||
|
||||
Model Providers:
|
||||
Claude Required for Sisyphus (main orchestrator) and Librarian agents
|
||||
ChatGPT Powers the Oracle agent for debugging and architecture
|
||||
Gemini Powers frontend, documentation, and multimodal agents
|
||||
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
|
||||
Claude Native anthropic/ models (Opus, Sonnet, Haiku)
|
||||
OpenAI Native openai/ models (GPT-5.2 for Oracle)
|
||||
Gemini Native google/ models (Gemini 3 Pro, Flash)
|
||||
Copilot github-copilot/ models (fallback)
|
||||
OpenCode Zen opencode/ models (opencode/claude-opus-4-5, etc.)
|
||||
Z.ai zai-coding-plan/glm-4.7 (Librarian priority)
|
||||
Kimi kimi-for-coding/k2p5 (Sisyphus/Prometheus fallback)
|
||||
`)
|
||||
.action(async (options) => {
|
||||
const args: InstallArgs = {
|
||||
tui: options.tui !== false,
|
||||
claude: options.claude,
|
||||
chatgpt: options.chatgpt,
|
||||
openai: options.openai,
|
||||
gemini: options.gemini,
|
||||
copilot: options.copilot,
|
||||
opencodeZen: options.opencodeZen,
|
||||
zaiCodingPlan: options.zaiCodingPlan,
|
||||
kimiForCoding: options.kimiForCoding,
|
||||
skipAuth: options.skipAuth ?? false,
|
||||
}
|
||||
const exitCode = await install(args)
|
||||
@@ -143,4 +154,6 @@ program
|
||||
console.log(`oh-my-opencode v${VERSION}`)
|
||||
})
|
||||
|
||||
program.addCommand(createMcpOAuthCommand())
|
||||
|
||||
program.parse()
|
||||
|
||||
151
src/cli/install.test.ts
Normal file
151
src/cli/install.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { install } from "./install"
|
||||
import * as configManager from "./config-manager"
|
||||
import type { InstallArgs } from "./types"
|
||||
|
||||
// Mock console methods to capture output
|
||||
const mockConsoleLog = mock(() => {})
|
||||
const mockConsoleError = mock(() => {})
|
||||
|
||||
describe("install CLI - binary check behavior", () => {
|
||||
let tempDir: string
|
||||
let originalEnv: string | undefined
|
||||
let isOpenCodeInstalledSpy: ReturnType<typeof spyOn>
|
||||
let getOpenCodeVersionSpy: ReturnType<typeof spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
// #given temporary config directory
|
||||
tempDir = join(tmpdir(), `omo-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
||||
mkdirSync(tempDir, { recursive: true })
|
||||
|
||||
originalEnv = process.env.OPENCODE_CONFIG_DIR
|
||||
process.env.OPENCODE_CONFIG_DIR = tempDir
|
||||
|
||||
// Reset config context
|
||||
configManager.resetConfigContext()
|
||||
configManager.initConfigContext("opencode", null)
|
||||
|
||||
// Capture console output
|
||||
console.log = mockConsoleLog
|
||||
mockConsoleLog.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.OPENCODE_CONFIG_DIR = originalEnv
|
||||
} else {
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
}
|
||||
|
||||
if (existsSync(tempDir)) {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
isOpenCodeInstalledSpy?.mockRestore()
|
||||
getOpenCodeVersionSpy?.mockRestore()
|
||||
})
|
||||
|
||||
test("non-TUI mode: should show warning but continue when OpenCode binary not found", async () => {
|
||||
// #given OpenCode binary is NOT installed
|
||||
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
|
||||
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
|
||||
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
claude: "yes",
|
||||
openai: "no",
|
||||
gemini: "no",
|
||||
copilot: "no",
|
||||
opencodeZen: "no",
|
||||
zaiCodingPlan: "no",
|
||||
}
|
||||
|
||||
// #when running install
|
||||
const exitCode = await install(args)
|
||||
|
||||
// #then should return success (0), not failure (1)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
// #then should have printed a warning (not error)
|
||||
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
|
||||
expect(allCalls).toContain("[!]") // warning symbol
|
||||
expect(allCalls).toContain("OpenCode")
|
||||
})
|
||||
|
||||
test("non-TUI mode: should create opencode.json with plugin even when binary not found", async () => {
|
||||
// #given OpenCode binary is NOT installed
|
||||
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
|
||||
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
|
||||
|
||||
// #given mock npm fetch
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "3.0.0" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
claude: "yes",
|
||||
openai: "no",
|
||||
gemini: "no",
|
||||
copilot: "no",
|
||||
opencodeZen: "no",
|
||||
zaiCodingPlan: "no",
|
||||
}
|
||||
|
||||
// #when running install
|
||||
const exitCode = await install(args)
|
||||
|
||||
// #then should create opencode.json
|
||||
const configPath = join(tempDir, "opencode.json")
|
||||
expect(existsSync(configPath)).toBe(true)
|
||||
|
||||
// #then opencode.json should have plugin entry
|
||||
const config = JSON.parse(readFileSync(configPath, "utf-8"))
|
||||
expect(config.plugin).toBeDefined()
|
||||
expect(config.plugin.some((p: string) => p.includes("oh-my-opencode"))).toBe(true)
|
||||
|
||||
// #then exit code should be 0 (success)
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
test("non-TUI mode: should still succeed and complete all steps when binary exists", async () => {
|
||||
// #given OpenCode binary IS installed
|
||||
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true)
|
||||
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.0.200")
|
||||
|
||||
// #given mock npm fetch
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "3.0.0" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
claude: "yes",
|
||||
openai: "no",
|
||||
gemini: "no",
|
||||
copilot: "no",
|
||||
opencodeZen: "no",
|
||||
zaiCodingPlan: "no",
|
||||
}
|
||||
|
||||
// #when running install
|
||||
const exitCode = await install(args)
|
||||
|
||||
// #then should return success
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
// #then should have printed success (OK symbol)
|
||||
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
|
||||
expect(allCalls).toContain("[OK]")
|
||||
expect(allCalls).toContain("OpenCode 1.0.200")
|
||||
})
|
||||
})
|
||||
@@ -10,18 +10,19 @@ import {
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
} from "./config-manager"
|
||||
import { shouldShowChatGPTOnlyWarning } from "./model-fallback"
|
||||
import packageJson from "../../package.json" with { type: "json" }
|
||||
|
||||
const VERSION = packageJson.version
|
||||
|
||||
const SYMBOLS = {
|
||||
check: color.green("✓"),
|
||||
cross: color.red("✗"),
|
||||
arrow: color.cyan("→"),
|
||||
bullet: color.dim("•"),
|
||||
info: color.blue("ℹ"),
|
||||
warn: color.yellow("⚠"),
|
||||
star: color.yellow("★"),
|
||||
check: color.green("[OK]"),
|
||||
cross: color.red("[X]"),
|
||||
arrow: color.cyan("->"),
|
||||
bullet: color.dim("*"),
|
||||
info: color.blue("[i]"),
|
||||
warn: color.yellow("[!]"),
|
||||
star: color.yellow("*"),
|
||||
}
|
||||
|
||||
function formatProvider(name: string, enabled: boolean, detail?: string): string {
|
||||
@@ -39,26 +40,21 @@ function formatConfigSummary(config: InstallConfig): string {
|
||||
|
||||
const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined
|
||||
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
|
||||
lines.push(formatProvider("ChatGPT", config.hasChatGPT))
|
||||
lines.push(formatProvider("OpenAI/ChatGPT", config.hasOpenAI, "GPT-5.2 for Oracle"))
|
||||
lines.push(formatProvider("Gemini", config.hasGemini))
|
||||
lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback provider"))
|
||||
lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback"))
|
||||
lines.push(formatProvider("OpenCode Zen", config.hasOpencodeZen, "opencode/ models"))
|
||||
lines.push(formatProvider("Z.ai Coding Plan", config.hasZaiCodingPlan, "Librarian/Multimodal"))
|
||||
lines.push(formatProvider("Kimi For Coding", config.hasKimiForCoding, "Sisyphus/Prometheus fallback"))
|
||||
|
||||
lines.push("")
|
||||
lines.push(color.dim("─".repeat(40)))
|
||||
lines.push("")
|
||||
|
||||
lines.push(color.bold(color.white("Agent Configuration")))
|
||||
lines.push(color.bold(color.white("Model Assignment")))
|
||||
lines.push("")
|
||||
|
||||
const sisyphusModel = config.hasClaude ? "claude-opus-4-5" : (config.hasCopilot ? "github-copilot/claude-opus-4.5" : "glm-4.7-free")
|
||||
const oracleModel = config.hasChatGPT ? "gpt-5.2" : (config.hasCopilot ? "github-copilot/gpt-5.2" : (config.hasClaude ? "claude-opus-4-5" : "glm-4.7-free"))
|
||||
const librarianModel = "glm-4.7-free"
|
||||
const frontendModel = config.hasGemini ? "antigravity-gemini-3-pro-high" : (config.hasClaude ? "claude-opus-4-5" : "glm-4.7-free")
|
||||
|
||||
lines.push(` ${SYMBOLS.bullet} Sisyphus ${SYMBOLS.arrow} ${color.cyan(sisyphusModel)}`)
|
||||
lines.push(` ${SYMBOLS.bullet} Oracle ${SYMBOLS.arrow} ${color.cyan(oracleModel)}`)
|
||||
lines.push(` ${SYMBOLS.bullet} Librarian ${SYMBOLS.arrow} ${color.cyan(librarianModel)}`)
|
||||
lines.push(` ${SYMBOLS.bullet} Frontend ${SYMBOLS.arrow} ${color.cyan(frontendModel)}`)
|
||||
lines.push(` ${SYMBOLS.info} Models auto-configured based on provider priority`)
|
||||
lines.push(` ${SYMBOLS.bullet} Priority: Native > Copilot > OpenCode Zen > Z.ai`)
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
@@ -122,12 +118,6 @@ function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string
|
||||
errors.push(`Invalid --claude value: ${args.claude} (expected: no, yes, max20)`)
|
||||
}
|
||||
|
||||
if (args.chatgpt === undefined) {
|
||||
errors.push("--chatgpt is required (values: no, yes)")
|
||||
} else if (!["no", "yes"].includes(args.chatgpt)) {
|
||||
errors.push(`Invalid --chatgpt value: ${args.chatgpt} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.gemini === undefined) {
|
||||
errors.push("--gemini is required (values: no, yes)")
|
||||
} else if (!["no", "yes"].includes(args.gemini)) {
|
||||
@@ -140,6 +130,22 @@ function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string
|
||||
errors.push(`Invalid --copilot value: ${args.copilot} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.openai !== undefined && !["no", "yes"].includes(args.openai)) {
|
||||
errors.push(`Invalid --openai value: ${args.openai} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.opencodeZen !== undefined && !["no", "yes"].includes(args.opencodeZen)) {
|
||||
errors.push(`Invalid --opencode-zen value: ${args.opencodeZen} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.zaiCodingPlan !== undefined && !["no", "yes"].includes(args.zaiCodingPlan)) {
|
||||
errors.push(`Invalid --zai-coding-plan value: ${args.zaiCodingPlan} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.kimiForCoding !== undefined && !["no", "yes"].includes(args.kimiForCoding)) {
|
||||
errors.push(`Invalid --kimi-for-coding value: ${args.kimiForCoding} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
|
||||
@@ -147,13 +153,16 @@ function argsToConfig(args: InstallArgs): InstallConfig {
|
||||
return {
|
||||
hasClaude: args.claude !== "no",
|
||||
isMax20: args.claude === "max20",
|
||||
hasChatGPT: args.chatgpt === "yes",
|
||||
hasOpenAI: args.openai === "yes",
|
||||
hasGemini: args.gemini === "yes",
|
||||
hasCopilot: args.copilot === "yes",
|
||||
hasOpencodeZen: args.opencodeZen === "yes",
|
||||
hasZaiCodingPlan: args.zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: args.kimiForCoding === "yes",
|
||||
}
|
||||
}
|
||||
|
||||
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; chatgpt: BooleanArg; gemini: BooleanArg; copilot: BooleanArg } {
|
||||
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; openai: BooleanArg; gemini: BooleanArg; copilot: BooleanArg; opencodeZen: BooleanArg; zaiCodingPlan: BooleanArg; kimiForCoding: BooleanArg } {
|
||||
let claude: ClaudeSubscription = "no"
|
||||
if (detected.hasClaude) {
|
||||
claude = detected.isMax20 ? "max20" : "yes"
|
||||
@@ -161,9 +170,12 @@ function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubs
|
||||
|
||||
return {
|
||||
claude,
|
||||
chatgpt: detected.hasChatGPT ? "yes" : "no",
|
||||
openai: detected.hasOpenAI ? "yes" : "no",
|
||||
gemini: detected.hasGemini ? "yes" : "no",
|
||||
copilot: detected.hasCopilot ? "yes" : "no",
|
||||
opencodeZen: detected.hasOpencodeZen ? "yes" : "no",
|
||||
zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no",
|
||||
kimiForCoding: detected.hasKimiForCoding ? "yes" : "no",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,16 +197,16 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
|
||||
return null
|
||||
}
|
||||
|
||||
const chatgpt = await p.select({
|
||||
message: "Do you have a ChatGPT Plus/Pro subscription?",
|
||||
const openai = await p.select({
|
||||
message: "Do you have an OpenAI/ChatGPT Plus subscription?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Oracle will use fallback model" },
|
||||
{ value: "yes" as const, label: "Yes", hint: "GPT-5.2 for debugging and architecture" },
|
||||
{ value: "no" as const, label: "No", hint: "Oracle will use fallback models" },
|
||||
{ value: "yes" as const, label: "Yes", hint: "GPT-5.2 for Oracle (high-IQ debugging)" },
|
||||
],
|
||||
initialValue: initial.chatgpt,
|
||||
initialValue: initial.openai,
|
||||
})
|
||||
|
||||
if (p.isCancel(chatgpt)) {
|
||||
if (p.isCancel(openai)) {
|
||||
p.cancel("Installation cancelled.")
|
||||
return null
|
||||
}
|
||||
@@ -227,12 +239,57 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
|
||||
return null
|
||||
}
|
||||
|
||||
const opencodeZen = await p.select({
|
||||
message: "Do you have access to OpenCode Zen (opencode/ models)?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
|
||||
{ value: "yes" as const, label: "Yes", hint: "opencode/claude-opus-4-5, opencode/gpt-5.2, etc." },
|
||||
],
|
||||
initialValue: initial.opencodeZen,
|
||||
})
|
||||
|
||||
if (p.isCancel(opencodeZen)) {
|
||||
p.cancel("Installation cancelled.")
|
||||
return null
|
||||
}
|
||||
|
||||
const zaiCodingPlan = await p.select({
|
||||
message: "Do you have a Z.ai Coding Plan subscription?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
|
||||
{ value: "yes" as const, label: "Yes", hint: "Fallback for Librarian and Multimodal Looker" },
|
||||
],
|
||||
initialValue: initial.zaiCodingPlan,
|
||||
})
|
||||
|
||||
if (p.isCancel(zaiCodingPlan)) {
|
||||
p.cancel("Installation cancelled.")
|
||||
return null
|
||||
}
|
||||
|
||||
const kimiForCoding = await p.select({
|
||||
message: "Do you have a Kimi For Coding subscription?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
|
||||
{ value: "yes" as const, label: "Yes", hint: "Kimi K2.5 for Sisyphus/Prometheus fallback" },
|
||||
],
|
||||
initialValue: initial.kimiForCoding,
|
||||
})
|
||||
|
||||
if (p.isCancel(kimiForCoding)) {
|
||||
p.cancel("Installation cancelled.")
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
hasClaude: claude !== "no",
|
||||
isMax20: claude === "max20",
|
||||
hasChatGPT: chatgpt === "yes",
|
||||
hasOpenAI: openai === "yes",
|
||||
hasGemini: gemini === "yes",
|
||||
hasCopilot: copilot === "yes",
|
||||
hasOpencodeZen: opencodeZen === "yes",
|
||||
hasZaiCodingPlan: zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: kimiForCoding === "yes",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +302,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
console.log(` ${SYMBOLS.bullet} ${err}`)
|
||||
}
|
||||
console.log()
|
||||
printInfo("Usage: bunx oh-my-opencode install --no-tui --claude=<no|yes|max20> --chatgpt=<no|yes> --gemini=<no|yes> --copilot=<no|yes>")
|
||||
printInfo("Usage: bunx oh-my-opencode install --no-tui --claude=<no|yes|max20> --gemini=<no|yes> --copilot=<no|yes>")
|
||||
console.log()
|
||||
return 1
|
||||
}
|
||||
@@ -260,18 +317,17 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
|
||||
printStep(step++, totalSteps, "Checking OpenCode installation...")
|
||||
const installed = await isOpenCodeInstalled()
|
||||
if (!installed) {
|
||||
printError("OpenCode is not installed on this system.")
|
||||
printInfo("Visit https://opencode.ai/docs for installation instructions")
|
||||
return 1
|
||||
}
|
||||
|
||||
const version = await getOpenCodeVersion()
|
||||
printSuccess(`OpenCode ${version ?? ""} detected`)
|
||||
if (!installed) {
|
||||
printWarning("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
|
||||
printInfo("Visit https://opencode.ai/docs for installation instructions")
|
||||
} else {
|
||||
printSuccess(`OpenCode ${version ?? ""} detected`)
|
||||
}
|
||||
|
||||
if (isUpdate) {
|
||||
const initial = detectedToInitialValues(detected)
|
||||
printInfo(`Current config: Claude=${initial.claude}, ChatGPT=${initial.chatgpt}, Gemini=${initial.gemini}`)
|
||||
printInfo(`Current config: Claude=${initial.claude}, Gemini=${initial.gemini}`)
|
||||
}
|
||||
|
||||
const config = argsToConfig(args)
|
||||
@@ -314,7 +370,21 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
|
||||
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini && !config.hasCopilot) {
|
||||
if (!config.hasClaude) {
|
||||
console.log()
|
||||
console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING "))))
|
||||
console.log()
|
||||
console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5.")))
|
||||
console.log(color.red(" Without Claude, you may experience significantly degraded performance:"))
|
||||
console.log(color.dim(" • Reduced orchestration quality"))
|
||||
console.log(color.dim(" • Weaker tool selection and delegation"))
|
||||
console.log(color.dim(" • Less reliable task completion"))
|
||||
console.log()
|
||||
console.log(color.yellow(" Consider subscribing to Claude Pro/Max for the best experience."))
|
||||
console.log()
|
||||
}
|
||||
|
||||
if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) {
|
||||
printWarning("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
}
|
||||
|
||||
@@ -326,23 +396,22 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
`${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` +
|
||||
`All features work like magic—parallel agents, background tasks,\n` +
|
||||
`deep exploration, and relentless execution until completion.`,
|
||||
"🪄 The Magic Word"
|
||||
"The Magic Word"
|
||||
)
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
|
||||
console.log(` ${color.dim("gh repo star code-yeongyu/oh-my-opencode")}`)
|
||||
console.log(` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`)
|
||||
console.log()
|
||||
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
||||
console.log()
|
||||
|
||||
if ((config.hasClaude || config.hasChatGPT || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
printBox(
|
||||
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
||||
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
|
||||
(config.hasChatGPT ? ` ${SYMBOLS.bullet} OpenAI ${color.gray("→ ChatGPT Plus/Pro")}\n` : "") +
|
||||
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
|
||||
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
|
||||
"🔐 Authenticate Your Providers"
|
||||
"Authenticate Your Providers"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -361,23 +430,21 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
|
||||
if (isUpdate) {
|
||||
const initial = detectedToInitialValues(detected)
|
||||
p.log.info(`Existing configuration detected: Claude=${initial.claude}, ChatGPT=${initial.chatgpt}, Gemini=${initial.gemini}`)
|
||||
p.log.info(`Existing configuration detected: Claude=${initial.claude}, Gemini=${initial.gemini}`)
|
||||
}
|
||||
|
||||
const s = p.spinner()
|
||||
s.start("Checking OpenCode installation")
|
||||
|
||||
const installed = await isOpenCodeInstalled()
|
||||
if (!installed) {
|
||||
s.stop("OpenCode is not installed")
|
||||
p.log.error("OpenCode is not installed on this system.")
|
||||
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
|
||||
p.outro(color.red("Please install OpenCode first."))
|
||||
return 1
|
||||
}
|
||||
|
||||
const version = await getOpenCodeVersion()
|
||||
s.stop(`OpenCode ${version ?? "installed"} ${color.green("✓")}`)
|
||||
if (!installed) {
|
||||
s.stop(`OpenCode binary not found ${color.yellow("[!]")}`)
|
||||
p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
|
||||
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
|
||||
} else {
|
||||
s.stop(`OpenCode ${version ?? "installed"} ${color.green("[OK]")}`)
|
||||
}
|
||||
|
||||
const config = await runTuiMode(detected)
|
||||
if (!config) return 1
|
||||
@@ -420,7 +487,21 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
s.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
|
||||
|
||||
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini && !config.hasCopilot) {
|
||||
if (!config.hasClaude) {
|
||||
console.log()
|
||||
console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING "))))
|
||||
console.log()
|
||||
console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5.")))
|
||||
console.log(color.red(" Without Claude, you may experience significantly degraded performance:"))
|
||||
console.log(color.dim(" • Reduced orchestration quality"))
|
||||
console.log(color.dim(" • Weaker tool selection and delegation"))
|
||||
console.log(color.dim(" • Less reliable task completion"))
|
||||
console.log()
|
||||
console.log(color.yellow(" Consider subscribing to Claude Pro/Max for the best experience."))
|
||||
console.log()
|
||||
}
|
||||
|
||||
if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) {
|
||||
p.log.warn("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
}
|
||||
|
||||
@@ -433,23 +514,22 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
`Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` +
|
||||
`All features work like magic—parallel agents, background tasks,\n` +
|
||||
`deep exploration, and relentless execution until completion.`,
|
||||
"🪄 The Magic Word"
|
||||
"The Magic Word"
|
||||
)
|
||||
|
||||
p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`)
|
||||
p.log.message(` ${color.dim("gh repo star code-yeongyu/oh-my-opencode")}`)
|
||||
p.log.message(` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-opencode >/dev/null 2>&1 || true")}`)
|
||||
|
||||
p.outro(color.green("oMoMoMoMo... Enjoy!"))
|
||||
|
||||
if ((config.hasClaude || config.hasChatGPT || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
const providers: string[] = []
|
||||
if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`)
|
||||
if (config.hasChatGPT) providers.push(`OpenAI ${color.gray("→ ChatGPT Plus/Pro")}`)
|
||||
if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`)
|
||||
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
|
||||
|
||||
console.log()
|
||||
console.log(color.bold("🔐 Authenticate Your Providers"))
|
||||
console.log(color.bold("Authenticate Your Providers"))
|
||||
console.log()
|
||||
console.log(` Run ${color.cyan("opencode auth login")} and select:`)
|
||||
for (const provider of providers) {
|
||||
|
||||
123
src/cli/mcp-oauth/index.test.ts
Normal file
123
src/cli/mcp-oauth/index.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { Command } from "commander"
|
||||
import { createMcpOAuthCommand } from "./index"
|
||||
|
||||
describe("mcp oauth command", () => {
|
||||
|
||||
describe("command structure", () => {
|
||||
it("creates mcp command group with oauth subcommand", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
|
||||
// when
|
||||
const subcommands = mcpCommand.commands.map((cmd: Command) => cmd.name())
|
||||
|
||||
// then
|
||||
expect(subcommands).toContain("oauth")
|
||||
})
|
||||
|
||||
it("oauth subcommand has login, logout, and status subcommands", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
|
||||
// when
|
||||
const subcommands = oauthCommand?.commands.map((cmd: Command) => cmd.name()) ?? []
|
||||
|
||||
// then
|
||||
expect(subcommands).toContain("login")
|
||||
expect(subcommands).toContain("logout")
|
||||
expect(subcommands).toContain("status")
|
||||
})
|
||||
})
|
||||
|
||||
describe("login subcommand", () => {
|
||||
it("exists and has description", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const description = loginCommand?.description() ?? ""
|
||||
|
||||
// then
|
||||
expect(loginCommand).toBeDefined()
|
||||
expect(description).toContain("OAuth")
|
||||
})
|
||||
|
||||
it("accepts --server-url option", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const options = loginCommand?.options ?? []
|
||||
const serverUrlOption = options.find((opt: { long?: string }) => opt.long === "--server-url")
|
||||
|
||||
// then
|
||||
expect(serverUrlOption).toBeDefined()
|
||||
})
|
||||
|
||||
it("accepts --client-id option", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const options = loginCommand?.options ?? []
|
||||
const clientIdOption = options.find((opt: { long?: string }) => opt.long === "--client-id")
|
||||
|
||||
// then
|
||||
expect(clientIdOption).toBeDefined()
|
||||
})
|
||||
|
||||
it("accepts --scopes option", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const options = loginCommand?.options ?? []
|
||||
const scopesOption = options.find((opt: { long?: string }) => opt.long === "--scopes")
|
||||
|
||||
// then
|
||||
expect(scopesOption).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("logout subcommand", () => {
|
||||
it("exists and has description", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const logoutCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "logout")
|
||||
|
||||
// when
|
||||
const description = logoutCommand?.description() ?? ""
|
||||
|
||||
// then
|
||||
expect(logoutCommand).toBeDefined()
|
||||
expect(description).toContain("tokens")
|
||||
})
|
||||
})
|
||||
|
||||
describe("status subcommand", () => {
|
||||
it("exists and has description", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const statusCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "status")
|
||||
|
||||
// when
|
||||
const description = statusCommand?.description() ?? ""
|
||||
|
||||
// then
|
||||
expect(statusCommand).toBeDefined()
|
||||
expect(description).toContain("status")
|
||||
})
|
||||
})
|
||||
})
|
||||
43
src/cli/mcp-oauth/index.ts
Normal file
43
src/cli/mcp-oauth/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Command } from "commander"
|
||||
import { login } from "./login"
|
||||
import { logout } from "./logout"
|
||||
import { status } from "./status"
|
||||
|
||||
export function createMcpOAuthCommand(): Command {
|
||||
const mcp = new Command("mcp").description("MCP server management")
|
||||
|
||||
const oauth = new Command("oauth").description("OAuth token management for MCP servers")
|
||||
|
||||
oauth
|
||||
.command("login <server-name>")
|
||||
.description("Authenticate with an MCP server using OAuth")
|
||||
.option("--server-url <url>", "OAuth server URL (required if not in config)")
|
||||
.option("--client-id <id>", "OAuth client ID (optional, uses DCR if not provided)")
|
||||
.option("--scopes <scopes...>", "OAuth scopes to request")
|
||||
.action(async (serverName: string, options) => {
|
||||
const exitCode = await login(serverName, options)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
oauth
|
||||
.command("logout <server-name>")
|
||||
.description("Remove stored OAuth tokens for an MCP server")
|
||||
.option("--server-url <url>", "OAuth server URL (use if server name differs from URL)")
|
||||
.action(async (serverName: string, options) => {
|
||||
const exitCode = await logout(serverName, options)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
oauth
|
||||
.command("status [server-name]")
|
||||
.description("Show OAuth token status for MCP servers")
|
||||
.action(async (serverName: string | undefined) => {
|
||||
const exitCode = await status(serverName)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
mcp.addCommand(oauth)
|
||||
return mcp
|
||||
}
|
||||
|
||||
export { login, logout, status }
|
||||
80
src/cli/mcp-oauth/login.test.ts
Normal file
80
src/cli/mcp-oauth/login.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
|
||||
const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 }))
|
||||
|
||||
mock.module("../../features/mcp-oauth/provider", () => ({
|
||||
McpOAuthProvider: class MockMcpOAuthProvider {
|
||||
constructor(public options: { serverUrl: string; clientId?: string; scopes?: string[] }) {}
|
||||
async login() {
|
||||
return mockLogin()
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const { login } = await import("./login")
|
||||
|
||||
describe("login command", () => {
|
||||
beforeEach(() => {
|
||||
mockLogin.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// cleanup
|
||||
})
|
||||
|
||||
it("returns error code when server-url is not provided", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {}
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
|
||||
it("returns success code when login succeeds", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {
|
||||
serverUrl: "https://oauth.example.com",
|
||||
}
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(0)
|
||||
expect(mockLogin).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns error code when login throws", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {
|
||||
serverUrl: "https://oauth.example.com",
|
||||
}
|
||||
mockLogin.mockRejectedValueOnce(new Error("Network error"))
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
|
||||
it("returns error code when server-url is missing", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {
|
||||
clientId: "test-client-id",
|
||||
}
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
})
|
||||
38
src/cli/mcp-oauth/login.ts
Normal file
38
src/cli/mcp-oauth/login.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { McpOAuthProvider } from "../../features/mcp-oauth/provider"
|
||||
|
||||
export interface LoginOptions {
|
||||
serverUrl?: string
|
||||
clientId?: string
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
export async function login(serverName: string, options: LoginOptions): Promise<number> {
|
||||
try {
|
||||
const serverUrl = options.serverUrl
|
||||
if (!serverUrl) {
|
||||
console.error(`Error: --server-url is required for server "${serverName}"`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const provider = new McpOAuthProvider({
|
||||
serverUrl,
|
||||
clientId: options.clientId,
|
||||
scopes: options.scopes,
|
||||
})
|
||||
|
||||
console.log(`Authenticating with ${serverName}...`)
|
||||
const tokenData = await provider.login()
|
||||
|
||||
console.log(`✓ Successfully authenticated with ${serverName}`)
|
||||
if (tokenData.expiresAt) {
|
||||
const expiryDate = new Date(tokenData.expiresAt * 1000)
|
||||
console.log(` Token expires at: ${expiryDate.toISOString()}`)
|
||||
}
|
||||
|
||||
return 0
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error: Failed to authenticate with ${serverName}: ${message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
65
src/cli/mcp-oauth/logout.test.ts
Normal file
65
src/cli/mcp-oauth/logout.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { saveToken } from "../../features/mcp-oauth/storage"
|
||||
|
||||
const { logout } = await import("./logout")
|
||||
|
||||
describe("logout command", () => {
|
||||
const TEST_CONFIG_DIR = join(tmpdir(), "mcp-oauth-logout-test-" + Date.now())
|
||||
let originalConfigDir: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalConfigDir = process.env.OPENCODE_CONFIG_DIR
|
||||
process.env.OPENCODE_CONFIG_DIR = TEST_CONFIG_DIR
|
||||
if (!existsSync(TEST_CONFIG_DIR)) {
|
||||
mkdirSync(TEST_CONFIG_DIR, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.OPENCODE_CONFIG_DIR = originalConfigDir
|
||||
}
|
||||
if (existsSync(TEST_CONFIG_DIR)) {
|
||||
rmSync(TEST_CONFIG_DIR, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("returns success code when logout succeeds", async () => {
|
||||
// given
|
||||
const serverUrl = "https://test-server.example.com"
|
||||
saveToken(serverUrl, serverUrl, { accessToken: "test-token" })
|
||||
|
||||
// when
|
||||
const exitCode = await logout("test-server", { serverUrl })
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("handles non-existent server gracefully", async () => {
|
||||
// given
|
||||
const serverName = "non-existent-server"
|
||||
|
||||
// when
|
||||
const exitCode = await logout(serverName, { serverUrl: "https://nonexistent.example.com" })
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("returns error when --server-url is not provided", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
|
||||
// when
|
||||
const exitCode = await logout(serverName)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
})
|
||||
30
src/cli/mcp-oauth/logout.ts
Normal file
30
src/cli/mcp-oauth/logout.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { deleteToken } from "../../features/mcp-oauth/storage"
|
||||
|
||||
export interface LogoutOptions {
|
||||
serverUrl?: string
|
||||
}
|
||||
|
||||
export async function logout(serverName: string, options?: LogoutOptions): Promise<number> {
|
||||
try {
|
||||
const serverUrl = options?.serverUrl
|
||||
if (!serverUrl) {
|
||||
console.error(`Error: --server-url is required for logout. Token storage uses server URLs, not names.`)
|
||||
console.error(` Usage: mcp oauth logout ${serverName} --server-url https://your-server.example.com`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const success = deleteToken(serverUrl, serverUrl)
|
||||
|
||||
if (success) {
|
||||
console.log(`✓ Successfully removed tokens for ${serverName}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.error(`Error: Failed to remove tokens for ${serverName}`)
|
||||
return 1
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error: Failed to remove tokens for ${serverName}: ${message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
48
src/cli/mcp-oauth/status.test.ts
Normal file
48
src/cli/mcp-oauth/status.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { status } from "./status"
|
||||
|
||||
describe("status command", () => {
|
||||
beforeEach(() => {
|
||||
// setup
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// cleanup
|
||||
})
|
||||
|
||||
it("returns success code when checking status for specific server", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
|
||||
// when
|
||||
const exitCode = await status(serverName)
|
||||
|
||||
// then
|
||||
expect(typeof exitCode).toBe("number")
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("returns success code when checking status for all servers", async () => {
|
||||
// given
|
||||
const serverName = undefined
|
||||
|
||||
// when
|
||||
const exitCode = await status(serverName)
|
||||
|
||||
// then
|
||||
expect(typeof exitCode).toBe("number")
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("handles non-existent server gracefully", async () => {
|
||||
// given
|
||||
const serverName = "non-existent-server"
|
||||
|
||||
// when
|
||||
const exitCode = await status(serverName)
|
||||
|
||||
// then
|
||||
expect(typeof exitCode).toBe("number")
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
})
|
||||
50
src/cli/mcp-oauth/status.ts
Normal file
50
src/cli/mcp-oauth/status.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { listAllTokens, listTokensByHost } from "../../features/mcp-oauth/storage"
|
||||
|
||||
export async function status(serverName: string | undefined): Promise<number> {
|
||||
try {
|
||||
if (serverName) {
|
||||
const tokens = listTokensByHost(serverName)
|
||||
|
||||
if (Object.keys(tokens).length === 0) {
|
||||
console.log(`No tokens found for ${serverName}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.log(`OAuth Status for ${serverName}:`)
|
||||
for (const [key, token] of Object.entries(tokens)) {
|
||||
console.log(` ${key}:`)
|
||||
console.log(` Access Token: [REDACTED]`)
|
||||
if (token.refreshToken) {
|
||||
console.log(` Refresh Token: [REDACTED]`)
|
||||
}
|
||||
if (token.expiresAt) {
|
||||
const expiryDate = new Date(token.expiresAt * 1000)
|
||||
const now = Date.now() / 1000
|
||||
const isExpired = token.expiresAt < now
|
||||
const tokenStatus = isExpired ? "EXPIRED" : "VALID"
|
||||
console.log(` Expiry: ${expiryDate.toISOString()} (${tokenStatus})`)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const tokens = listAllTokens()
|
||||
if (Object.keys(tokens).length === 0) {
|
||||
console.log("No OAuth tokens stored")
|
||||
return 0
|
||||
}
|
||||
|
||||
console.log("Stored OAuth Tokens:")
|
||||
for (const [key, token] of Object.entries(tokens)) {
|
||||
const isExpired = token.expiresAt && token.expiresAt < Date.now() / 1000
|
||||
const tokenStatus = isExpired ? "EXPIRED" : "VALID"
|
||||
console.log(` ${key}: ${tokenStatus}`)
|
||||
}
|
||||
|
||||
return 0
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error: Failed to get token status: ${message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
435
src/cli/model-fallback.test.ts
Normal file
435
src/cli/model-fallback.test.ts
Normal file
@@ -0,0 +1,435 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { generateModelConfig } from "./model-fallback"
|
||||
import type { InstallConfig } from "./types"
|
||||
|
||||
function createConfig(overrides: Partial<InstallConfig> = {}): InstallConfig {
|
||||
return {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe("generateModelConfig", () => {
|
||||
describe("no providers available", () => {
|
||||
test("returns ULTIMATE_FALLBACK for all agents and categories when no providers", () => {
|
||||
// #given no providers are available
|
||||
const config = createConfig()
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use ULTIMATE_FALLBACK for everything
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("single native provider", () => {
|
||||
test("uses Claude models when only Claude is available", () => {
|
||||
// #given only Claude is available
|
||||
const config = createConfig({ hasClaude: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use Claude models per NATIVE_FALLBACK_CHAINS
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Claude models with isMax20 flag", () => {
|
||||
// #given Claude is available with Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models for Sisyphus
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenAI models when only OpenAI is available", () => {
|
||||
// #given only OpenAI is available
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use OpenAI models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenAI models with isMax20 flag", () => {
|
||||
// #given OpenAI is available with Max 20 plan
|
||||
const config = createConfig({ hasOpenAI: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Gemini models when only Gemini is available", () => {
|
||||
// #given only Gemini is available
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use Gemini models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Gemini models with isMax20 flag", () => {
|
||||
// #given Gemini is available with Max 20 plan
|
||||
const config = createConfig({ hasGemini: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("all native providers", () => {
|
||||
test("uses preferred models from fallback chains when all natives available", () => {
|
||||
// #given all native providers are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use first provider in each fallback chain
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses preferred models with isMax20 flag when all natives available", () => {
|
||||
// #given all native providers are available with Max 20 plan
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: true,
|
||||
isMax20: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("fallback providers", () => {
|
||||
test("uses OpenCode Zen models when only OpenCode Zen is available", () => {
|
||||
// #given only OpenCode Zen is available
|
||||
const config = createConfig({ hasOpencodeZen: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use OPENCODE_ZEN_MODELS
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenCode Zen models with isMax20 flag", () => {
|
||||
// #given OpenCode Zen is available with Max 20 plan
|
||||
const config = createConfig({ hasOpencodeZen: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses GitHub Copilot models when only Copilot is available", () => {
|
||||
// #given only GitHub Copilot is available
|
||||
const config = createConfig({ hasCopilot: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use GITHUB_COPILOT_MODELS
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses GitHub Copilot models with isMax20 flag", () => {
|
||||
// #given GitHub Copilot is available with Max 20 plan
|
||||
const config = createConfig({ hasCopilot: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses ZAI model for librarian when only ZAI is available", () => {
|
||||
// #given only ZAI is available
|
||||
const config = createConfig({ hasZaiCodingPlan: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use ZAI_MODEL for librarian
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses ZAI model for librarian with isMax20 flag", () => {
|
||||
// #given ZAI is available with Max 20 plan
|
||||
const config = createConfig({ hasZaiCodingPlan: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use ZAI_MODEL for librarian
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("mixed provider scenarios", () => {
|
||||
test("uses Claude + OpenCode Zen combination", () => {
|
||||
// #given Claude and OpenCode Zen are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpencodeZen: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should prefer Claude (native) over OpenCode Zen
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses OpenAI + Copilot combination", () => {
|
||||
// #given OpenAI and Copilot are available
|
||||
const config = createConfig({
|
||||
hasOpenAI: true,
|
||||
hasCopilot: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should prefer OpenAI (native) over Copilot
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Claude + ZAI combination (librarian uses ZAI)", () => {
|
||||
// #given Claude and ZAI are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then librarian should use ZAI, others use Claude
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses Gemini + Claude combination (explore uses Gemini)", () => {
|
||||
// #given Gemini and Claude are available
|
||||
const config = createConfig({
|
||||
hasGemini: true,
|
||||
hasClaude: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use Gemini flash
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses all fallback providers together", () => {
|
||||
// #given all fallback providers are available
|
||||
const config = createConfig({
|
||||
hasOpencodeZen: true,
|
||||
hasCopilot: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should prefer OpenCode Zen, but librarian uses ZAI
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses all providers together", () => {
|
||||
// #given all providers are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: true,
|
||||
hasOpencodeZen: true,
|
||||
hasCopilot: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should prefer native providers, librarian uses ZAI
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("uses all providers with isMax20 flag", () => {
|
||||
// #given all providers are available with Max 20 plan
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: true,
|
||||
hasOpencodeZen: true,
|
||||
hasCopilot: true,
|
||||
hasZaiCodingPlan: true,
|
||||
isMax20: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should use higher capability models
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("explore agent special cases", () => {
|
||||
test("explore uses gpt-5-nano when only Gemini available (no Claude)", () => {
|
||||
// #given only Gemini is available (no Claude)
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use gpt-5-nano (Claude haiku not available)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses Claude haiku when Claude available", () => {
|
||||
// #given Claude is available
|
||||
const config = createConfig({ hasClaude: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use claude-haiku-4-5
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("explore uses Claude haiku regardless of isMax20 flag", () => {
|
||||
// #given Claude is available without Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: false })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use claude-haiku-4-5 (isMax20 doesn't affect explore)
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("explore uses gpt-5-nano when only OpenAI available", () => {
|
||||
// #given only OpenAI is available
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use gpt-5-nano (fallback)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses gpt-5-mini when only Copilot available", () => {
|
||||
// #given only Copilot is available
|
||||
const config = createConfig({ hasCopilot: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use gpt-5-mini (Copilot fallback)
|
||||
expect(result.agents?.explore?.model).toBe("github-copilot/gpt-5-mini")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Sisyphus agent special cases", () => {
|
||||
test("Sisyphus uses sisyphus-high capability when isMax20 is true", () => {
|
||||
// #given Claude is available with Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then Sisyphus should use opus (sisyphus-high)
|
||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("Sisyphus uses sisyphus-low capability when isMax20 is false", () => {
|
||||
// #given Claude is available without Max 20 plan
|
||||
const config = createConfig({ hasClaude: true, isMax20: false })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then Sisyphus should use sonnet (sisyphus-low)
|
||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
})
|
||||
})
|
||||
|
||||
describe("librarian agent special cases", () => {
|
||||
test("librarian uses ZAI when ZAI is available regardless of other providers", () => {
|
||||
// #given ZAI and Claude are available
|
||||
const config = createConfig({
|
||||
hasClaude: true,
|
||||
hasZaiCodingPlan: true,
|
||||
})
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then librarian should use ZAI_MODEL
|
||||
expect(result.agents?.librarian?.model).toBe("zai-coding-plan/glm-4.7")
|
||||
})
|
||||
|
||||
test("librarian uses claude-sonnet when ZAI not available but Claude is", () => {
|
||||
// #given only Claude is available (no ZAI)
|
||||
const config = createConfig({ hasClaude: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm)
|
||||
expect(result.agents?.librarian?.model).toBe("anthropic/claude-sonnet-4-5")
|
||||
})
|
||||
})
|
||||
|
||||
describe("schema URL", () => {
|
||||
test("always includes correct schema URL", () => {
|
||||
// #given any config
|
||||
const config = createConfig()
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then should include correct schema URL
|
||||
expect(result.$schema).toBe(
|
||||
"https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
200
src/cli/model-fallback.ts
Normal file
200
src/cli/model-fallback.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
AGENT_MODEL_REQUIREMENTS,
|
||||
CATEGORY_MODEL_REQUIREMENTS,
|
||||
type FallbackEntry,
|
||||
} from "../shared/model-requirements"
|
||||
import type { InstallConfig } from "./types"
|
||||
|
||||
interface ProviderAvailability {
|
||||
native: {
|
||||
claude: boolean
|
||||
openai: boolean
|
||||
gemini: boolean
|
||||
}
|
||||
opencodeZen: boolean
|
||||
copilot: boolean
|
||||
zai: boolean
|
||||
kimiForCoding: boolean
|
||||
isMaxPlan: boolean
|
||||
}
|
||||
|
||||
interface AgentConfig {
|
||||
model: string
|
||||
variant?: string
|
||||
}
|
||||
|
||||
interface CategoryConfig {
|
||||
model: string
|
||||
variant?: string
|
||||
}
|
||||
|
||||
export interface GeneratedOmoConfig {
|
||||
$schema: string
|
||||
agents?: Record<string, AgentConfig>
|
||||
categories?: Record<string, CategoryConfig>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const ZAI_MODEL = "zai-coding-plan/glm-4.7"
|
||||
|
||||
const ULTIMATE_FALLBACK = "opencode/glm-4.7-free"
|
||||
const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
|
||||
|
||||
function toProviderAvailability(config: InstallConfig): ProviderAvailability {
|
||||
return {
|
||||
native: {
|
||||
claude: config.hasClaude,
|
||||
openai: config.hasOpenAI,
|
||||
gemini: config.hasGemini,
|
||||
},
|
||||
opencodeZen: config.hasOpencodeZen,
|
||||
copilot: config.hasCopilot,
|
||||
zai: config.hasZaiCodingPlan,
|
||||
kimiForCoding: config.hasKimiForCoding,
|
||||
isMaxPlan: config.isMax20,
|
||||
}
|
||||
}
|
||||
|
||||
function isProviderAvailable(provider: string, avail: ProviderAvailability): boolean {
|
||||
const mapping: Record<string, boolean> = {
|
||||
anthropic: avail.native.claude,
|
||||
openai: avail.native.openai,
|
||||
google: avail.native.gemini,
|
||||
"github-copilot": avail.copilot,
|
||||
opencode: avail.opencodeZen,
|
||||
"zai-coding-plan": avail.zai,
|
||||
"kimi-for-coding": avail.kimiForCoding,
|
||||
}
|
||||
return mapping[provider] ?? false
|
||||
}
|
||||
|
||||
function transformModelForProvider(provider: string, model: string): string {
|
||||
if (provider === "github-copilot") {
|
||||
return model
|
||||
.replace("claude-opus-4-5", "claude-opus-4.5")
|
||||
.replace("claude-sonnet-4-5", "claude-sonnet-4.5")
|
||||
.replace("claude-haiku-4-5", "claude-haiku-4.5")
|
||||
.replace("claude-sonnet-4", "claude-sonnet-4")
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
function resolveModelFromChain(
|
||||
fallbackChain: FallbackEntry[],
|
||||
avail: ProviderAvailability
|
||||
): { model: string; variant?: string } | null {
|
||||
for (const entry of fallbackChain) {
|
||||
for (const provider of entry.providers) {
|
||||
if (isProviderAvailable(provider, avail)) {
|
||||
const transformedModel = transformModelForProvider(provider, entry.model)
|
||||
return {
|
||||
model: `${provider}/${transformedModel}`,
|
||||
variant: entry.variant,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getSisyphusFallbackChain(isMaxPlan: boolean): FallbackEntry[] {
|
||||
// Sisyphus uses opus when isMaxPlan, sonnet otherwise
|
||||
if (isMaxPlan) {
|
||||
return AGENT_MODEL_REQUIREMENTS.sisyphus.fallbackChain
|
||||
}
|
||||
// For non-max plan, use sonnet instead of opus
|
||||
return [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
]
|
||||
}
|
||||
|
||||
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
const avail = toProviderAvailability(config)
|
||||
const hasAnyProvider =
|
||||
avail.native.claude ||
|
||||
avail.native.openai ||
|
||||
avail.native.gemini ||
|
||||
avail.opencodeZen ||
|
||||
avail.copilot ||
|
||||
avail.zai ||
|
||||
avail.kimiForCoding
|
||||
|
||||
if (!hasAnyProvider) {
|
||||
return {
|
||||
$schema: SCHEMA_URL,
|
||||
agents: Object.fromEntries(
|
||||
Object.keys(AGENT_MODEL_REQUIREMENTS).map((role) => [role, { model: ULTIMATE_FALLBACK }])
|
||||
),
|
||||
categories: Object.fromEntries(
|
||||
Object.keys(CATEGORY_MODEL_REQUIREMENTS).map((cat) => [cat, { model: ULTIMATE_FALLBACK }])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const agents: Record<string, AgentConfig> = {}
|
||||
const categories: Record<string, CategoryConfig> = {}
|
||||
|
||||
for (const [role, req] of Object.entries(AGENT_MODEL_REQUIREMENTS)) {
|
||||
// Special case: librarian always uses ZAI first if available
|
||||
if (role === "librarian" && avail.zai) {
|
||||
agents[role] = { model: ZAI_MODEL }
|
||||
continue
|
||||
}
|
||||
|
||||
// Special case: explore uses Claude haiku → GitHub Copilot gpt-5-mini → OpenCode gpt-5-nano
|
||||
if (role === "explore") {
|
||||
if (avail.native.claude) {
|
||||
agents[role] = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else if (avail.opencodeZen) {
|
||||
agents[role] = { model: "opencode/claude-haiku-4-5" }
|
||||
} else if (avail.copilot) {
|
||||
agents[role] = { model: "github-copilot/gpt-5-mini" }
|
||||
} else {
|
||||
agents[role] = { model: "opencode/gpt-5-nano" }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Special case: Sisyphus uses different fallbackChain based on isMaxPlan
|
||||
const fallbackChain =
|
||||
role === "sisyphus" ? getSisyphusFallbackChain(avail.isMaxPlan) : req.fallbackChain
|
||||
|
||||
const resolved = resolveModelFromChain(fallbackChain, avail)
|
||||
if (resolved) {
|
||||
const variant = resolved.variant ?? req.variant
|
||||
agents[role] = variant ? { model: resolved.model, variant } : { model: resolved.model }
|
||||
} else {
|
||||
agents[role] = { model: ULTIMATE_FALLBACK }
|
||||
}
|
||||
}
|
||||
|
||||
for (const [cat, req] of Object.entries(CATEGORY_MODEL_REQUIREMENTS)) {
|
||||
// Special case: unspecified-high downgrades to unspecified-low when not isMaxPlan
|
||||
const fallbackChain =
|
||||
cat === "unspecified-high" && !avail.isMaxPlan
|
||||
? CATEGORY_MODEL_REQUIREMENTS["unspecified-low"].fallbackChain
|
||||
: req.fallbackChain
|
||||
|
||||
const resolved = resolveModelFromChain(fallbackChain, avail)
|
||||
if (resolved) {
|
||||
const variant = resolved.variant ?? req.variant
|
||||
categories[cat] = variant ? { model: resolved.model, variant } : { model: resolved.model }
|
||||
} else {
|
||||
categories[cat] = { model: ULTIMATE_FALLBACK }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$schema: SCHEMA_URL,
|
||||
agents,
|
||||
categories,
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldShowChatGPTOnlyWarning(config: InstallConfig): boolean {
|
||||
return !config.hasClaude && !config.hasGemini && config.hasOpenAI
|
||||
}
|
||||
@@ -82,6 +82,7 @@ describe("createEventState", () => {
|
||||
expect(state.lastOutput).toBe("")
|
||||
expect(state.lastPartText).toBe("")
|
||||
expect(state.currentTool).toBe(null)
|
||||
expect(state.hasReceivedMeaningfulWork).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -126,6 +127,119 @@ describe("event handling", () => {
|
||||
expect(state.mainSessionIdle).toBe(false)
|
||||
})
|
||||
|
||||
it("hasReceivedMeaningfulWork is false initially after session.idle", async () => {
|
||||
// #given - session goes idle without any assistant output (race condition scenario)
|
||||
const ctx = createMockContext("my-session")
|
||||
const state = createEventState()
|
||||
|
||||
const payload: EventPayload = {
|
||||
type: "session.idle",
|
||||
properties: { sessionID: "my-session" },
|
||||
}
|
||||
|
||||
const events = toAsyncIterable([payload])
|
||||
const { processEvents } = await import("./events")
|
||||
|
||||
// #when
|
||||
await processEvents(ctx, events, state)
|
||||
|
||||
// #then - idle but no meaningful work yet
|
||||
expect(state.mainSessionIdle).toBe(true)
|
||||
expect(state.hasReceivedMeaningfulWork).toBe(false)
|
||||
})
|
||||
|
||||
it("message.updated with assistant role sets hasReceivedMeaningfulWork", async () => {
|
||||
// #given
|
||||
const ctx = createMockContext("my-session")
|
||||
const state = createEventState()
|
||||
|
||||
const payload: EventPayload = {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: { sessionID: "my-session", role: "assistant" },
|
||||
},
|
||||
}
|
||||
|
||||
const events = toAsyncIterable([payload])
|
||||
const { processEvents } = await import("./events")
|
||||
|
||||
// #when
|
||||
await processEvents(ctx, events, state)
|
||||
|
||||
// #then
|
||||
expect(state.hasReceivedMeaningfulWork).toBe(true)
|
||||
})
|
||||
|
||||
it("message.updated with user role does not set hasReceivedMeaningfulWork", async () => {
|
||||
// #given - user message should not count as meaningful work
|
||||
const ctx = createMockContext("my-session")
|
||||
const state = createEventState()
|
||||
|
||||
const payload: EventPayload = {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: { sessionID: "my-session", role: "user" },
|
||||
},
|
||||
}
|
||||
|
||||
const events = toAsyncIterable([payload])
|
||||
const { processEvents } = await import("./events")
|
||||
|
||||
// #when
|
||||
await processEvents(ctx, events, state)
|
||||
|
||||
// #then - user role should not count as meaningful work
|
||||
expect(state.hasReceivedMeaningfulWork).toBe(false)
|
||||
})
|
||||
|
||||
it("tool.execute sets hasReceivedMeaningfulWork", async () => {
|
||||
// #given
|
||||
const ctx = createMockContext("my-session")
|
||||
const state = createEventState()
|
||||
|
||||
const payload: EventPayload = {
|
||||
type: "tool.execute",
|
||||
properties: {
|
||||
sessionID: "my-session",
|
||||
name: "read_file",
|
||||
input: { filePath: "/src/index.ts" },
|
||||
},
|
||||
}
|
||||
|
||||
const events = toAsyncIterable([payload])
|
||||
const { processEvents } = await import("./events")
|
||||
|
||||
// #when
|
||||
await processEvents(ctx, events, state)
|
||||
|
||||
// #then
|
||||
expect(state.hasReceivedMeaningfulWork).toBe(true)
|
||||
})
|
||||
|
||||
it("tool.execute from different session does not set hasReceivedMeaningfulWork", async () => {
|
||||
// #given
|
||||
const ctx = createMockContext("my-session")
|
||||
const state = createEventState()
|
||||
|
||||
const payload: EventPayload = {
|
||||
type: "tool.execute",
|
||||
properties: {
|
||||
sessionID: "other-session",
|
||||
name: "read_file",
|
||||
input: { filePath: "/src/index.ts" },
|
||||
},
|
||||
}
|
||||
|
||||
const events = toAsyncIterable([payload])
|
||||
const { processEvents } = await import("./events")
|
||||
|
||||
// #when
|
||||
await processEvents(ctx, events, state)
|
||||
|
||||
// #then - different session's tool call shouldn't count
|
||||
expect(state.hasReceivedMeaningfulWork).toBe(false)
|
||||
})
|
||||
|
||||
it("session.status with busy type sets mainSessionIdle to false", async () => {
|
||||
// #given
|
||||
const ctx = createMockContext("my-session")
|
||||
@@ -136,6 +250,7 @@ describe("event handling", () => {
|
||||
lastOutput: "",
|
||||
lastPartText: "",
|
||||
currentTool: null,
|
||||
hasReceivedMeaningfulWork: false,
|
||||
}
|
||||
|
||||
const payload: EventPayload = {
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface EventState {
|
||||
lastOutput: string
|
||||
lastPartText: string
|
||||
currentTool: string | null
|
||||
/** Set to true when the main session has produced meaningful work (text, tool call, or tool result) */
|
||||
hasReceivedMeaningfulWork: boolean
|
||||
}
|
||||
|
||||
export function createEventState(): EventState {
|
||||
@@ -73,6 +75,7 @@ export function createEventState(): EventState {
|
||||
lastOutput: "",
|
||||
lastPartText: "",
|
||||
currentTool: null,
|
||||
hasReceivedMeaningfulWork: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +116,9 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
const isMainSession = sessionID === ctx.sessionID
|
||||
const sessionTag = isMainSession
|
||||
? pc.green("[MAIN]")
|
||||
: pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
|
||||
: sessionID
|
||||
? pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
|
||||
: pc.dim("[system]")
|
||||
|
||||
switch (payload.type) {
|
||||
case "session.idle":
|
||||
@@ -124,8 +129,6 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
}
|
||||
|
||||
case "message.part.updated": {
|
||||
// Skip verbose logging for partial message updates
|
||||
// Only log tool invocation state changes, not text streaming
|
||||
const partProps = props as MessagePartUpdatedProps | undefined
|
||||
const part = partProps?.part
|
||||
if (part?.type === "tool-invocation") {
|
||||
@@ -133,6 +136,11 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
console.error(
|
||||
pc.dim(`${sessionTag} message.part (tool): ${toolPart.toolName} [${toolPart.state}]`)
|
||||
)
|
||||
} else if (part?.type === "text" && part.text) {
|
||||
const preview = part.text.slice(0, 80).replace(/\n/g, "\\n")
|
||||
console.error(
|
||||
pc.dim(`${sessionTag} message.part (text): "${preview}${part.text.length > 80 ? "..." : ""}"`)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -140,11 +148,10 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
case "message.updated": {
|
||||
const msgProps = props as MessageUpdatedProps | undefined
|
||||
const role = msgProps?.info?.role ?? "unknown"
|
||||
const content = msgProps?.content ?? ""
|
||||
const preview = content.slice(0, 100).replace(/\n/g, "\\n")
|
||||
console.error(
|
||||
pc.dim(`${sessionTag} message.updated (${role}): "${preview}${content.length > 100 ? "..." : ""}"`)
|
||||
)
|
||||
const model = msgProps?.info?.modelID
|
||||
const agent = msgProps?.info?.agent
|
||||
const details = [role, agent, model].filter(Boolean).join(", ")
|
||||
console.error(pc.dim(`${sessionTag} message.updated (${details})`))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -154,7 +161,7 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
const input = toolProps?.input ?? {}
|
||||
const inputStr = JSON.stringify(input).slice(0, 150)
|
||||
console.error(
|
||||
pc.cyan(`${sessionTag} ⚡ TOOL.EXECUTE: ${pc.bold(toolName)}`)
|
||||
pc.cyan(`${sessionTag} TOOL.EXECUTE: ${pc.bold(toolName)}`)
|
||||
)
|
||||
console.error(pc.dim(` input: ${inputStr}${inputStr.length >= 150 ? "..." : ""}`))
|
||||
break
|
||||
@@ -165,7 +172,7 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
const output = resultProps?.output ?? ""
|
||||
const preview = output.slice(0, 200).replace(/\n/g, "\\n")
|
||||
console.error(
|
||||
pc.green(`${sessionTag} ✓ TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`)
|
||||
pc.green(`${sessionTag} TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`)
|
||||
)
|
||||
break
|
||||
}
|
||||
@@ -173,7 +180,7 @@ function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
case "session.error": {
|
||||
const errorProps = props as SessionErrorProps | undefined
|
||||
const errorMsg = serializeError(errorProps?.error)
|
||||
console.error(pc.red(`${sessionTag} ❌ SESSION.ERROR: ${errorMsg}`))
|
||||
console.error(pc.red(`${sessionTag} SESSION.ERROR: ${errorMsg}`))
|
||||
break
|
||||
}
|
||||
|
||||
@@ -241,6 +248,7 @@ function handleMessagePartUpdated(
|
||||
const newText = part.text.slice(state.lastPartText.length)
|
||||
if (newText) {
|
||||
process.stdout.write(newText)
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
}
|
||||
state.lastPartText = part.text
|
||||
}
|
||||
@@ -257,16 +265,7 @@ function handleMessageUpdated(
|
||||
if (props?.info?.sessionID !== ctx.sessionID) return
|
||||
if (props?.info?.role !== "assistant") return
|
||||
|
||||
const content = props.content
|
||||
if (!content || content === state.lastOutput) return
|
||||
|
||||
if (state.lastPartText.length === 0) {
|
||||
const newContent = content.slice(state.lastOutput.length)
|
||||
if (newContent) {
|
||||
process.stdout.write(newContent)
|
||||
}
|
||||
}
|
||||
state.lastOutput = content
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
}
|
||||
|
||||
function handleToolExecute(
|
||||
@@ -296,7 +295,8 @@ function handleToolExecute(
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`\n${pc.cyan("⚡")} ${pc.bold(toolName)}${inputPreview}\n`)
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
process.stdout.write(`\n${pc.cyan(">")} ${pc.bold(toolName)}${inputPreview}\n`)
|
||||
}
|
||||
|
||||
function handleToolResult(
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createEventState, processEvents, serializeError } from "./events"
|
||||
|
||||
const POLL_INTERVAL_MS = 500
|
||||
const DEFAULT_TIMEOUT_MS = 0
|
||||
const SESSION_CREATE_MAX_RETRIES = 3
|
||||
const SESSION_CREATE_RETRY_DELAY_MS = 1000
|
||||
|
||||
export async function run(options: RunOptions): Promise<number> {
|
||||
const {
|
||||
@@ -29,8 +31,18 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
}
|
||||
|
||||
try {
|
||||
// Support custom OpenCode server port via environment variable
|
||||
// This allows Open Agent and other orchestrators to run multiple
|
||||
// concurrent missions without port conflicts
|
||||
const serverPort = process.env.OPENCODE_SERVER_PORT
|
||||
? parseInt(process.env.OPENCODE_SERVER_PORT, 10)
|
||||
: undefined
|
||||
const serverHostname = process.env.OPENCODE_SERVER_HOSTNAME || undefined
|
||||
|
||||
const { client, server } = await createOpencode({
|
||||
signal: abortController.signal,
|
||||
...(serverPort && !isNaN(serverPort) ? { port: serverPort } : {}),
|
||||
...(serverHostname ? { hostname: serverHostname } : {}),
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
@@ -45,13 +57,49 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
})
|
||||
|
||||
try {
|
||||
const sessionRes = await client.session.create({
|
||||
body: { title: "oh-my-opencode run" },
|
||||
})
|
||||
// Retry session creation with exponential backoff
|
||||
// Server might not be fully ready even after "listening" message
|
||||
let sessionID: string | undefined
|
||||
let lastError: unknown
|
||||
|
||||
for (let attempt = 1; attempt <= SESSION_CREATE_MAX_RETRIES; attempt++) {
|
||||
const sessionRes = await client.session.create({
|
||||
body: { title: "oh-my-opencode run" },
|
||||
})
|
||||
|
||||
if (sessionRes.error) {
|
||||
lastError = sessionRes.error
|
||||
console.error(pc.yellow(`Session create attempt ${attempt}/${SESSION_CREATE_MAX_RETRIES} failed:`))
|
||||
console.error(pc.dim(` Error: ${serializeError(sessionRes.error)}`))
|
||||
|
||||
if (attempt < SESSION_CREATE_MAX_RETRIES) {
|
||||
const delay = SESSION_CREATE_RETRY_DELAY_MS * attempt
|
||||
console.log(pc.dim(` Retrying in ${delay}ms...`))
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sessionID = sessionRes.data?.id
|
||||
if (sessionID) {
|
||||
break
|
||||
}
|
||||
|
||||
// No error but also no session ID - unexpected response
|
||||
lastError = new Error(`Unexpected response: ${JSON.stringify(sessionRes, null, 2)}`)
|
||||
console.error(pc.yellow(`Session create attempt ${attempt}/${SESSION_CREATE_MAX_RETRIES}: No session ID returned`))
|
||||
|
||||
if (attempt < SESSION_CREATE_MAX_RETRIES) {
|
||||
const delay = SESSION_CREATE_RETRY_DELAY_MS * attempt
|
||||
console.log(pc.dim(` Retrying in ${delay}ms...`))
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
|
||||
const sessionID = sessionRes.data?.id
|
||||
if (!sessionID) {
|
||||
console.error(pc.red("Failed to create session"))
|
||||
console.error(pc.red("Failed to create session after all retries"))
|
||||
console.error(pc.dim(`Last error: ${serializeError(lastError)}`))
|
||||
cleanup()
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -95,6 +143,14 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Guard against premature completion: don't check completion until the
|
||||
// session has produced meaningful work (text output, tool call, or tool result).
|
||||
// Without this, a session that goes busy->idle before the LLM responds
|
||||
// would exit immediately because 0 todos + 0 children = "complete".
|
||||
if (!eventState.hasReceivedMeaningfulWork) {
|
||||
continue
|
||||
}
|
||||
|
||||
const shouldExit = await checkCompletionConditions(ctx)
|
||||
if (shouldExit) {
|
||||
console.log(pc.green("\n\nAll tasks completed."))
|
||||
|
||||
@@ -44,8 +44,13 @@ export interface SessionStatusProps {
|
||||
}
|
||||
|
||||
export interface MessageUpdatedProps {
|
||||
info?: { sessionID?: string; role?: string }
|
||||
content?: string
|
||||
info?: {
|
||||
sessionID?: string
|
||||
role?: string
|
||||
modelID?: string
|
||||
providerID?: string
|
||||
agent?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessagePartUpdatedProps {
|
||||
|
||||
@@ -4,18 +4,24 @@ export type BooleanArg = "no" | "yes"
|
||||
export interface InstallArgs {
|
||||
tui: boolean
|
||||
claude?: ClaudeSubscription
|
||||
chatgpt?: BooleanArg
|
||||
openai?: BooleanArg
|
||||
gemini?: BooleanArg
|
||||
copilot?: BooleanArg
|
||||
opencodeZen?: BooleanArg
|
||||
zaiCodingPlan?: BooleanArg
|
||||
kimiForCoding?: BooleanArg
|
||||
skipAuth?: boolean
|
||||
}
|
||||
|
||||
export interface InstallConfig {
|
||||
hasClaude: boolean
|
||||
isMax20: boolean
|
||||
hasChatGPT: boolean
|
||||
hasOpenAI: boolean
|
||||
hasGemini: boolean
|
||||
hasCopilot: boolean
|
||||
hasOpencodeZen: boolean
|
||||
hasZaiCodingPlan: boolean
|
||||
hasKimiForCoding: boolean
|
||||
}
|
||||
|
||||
export interface ConfigMergeResult {
|
||||
@@ -28,7 +34,10 @@ export interface DetectedConfig {
|
||||
isInstalled: boolean
|
||||
hasClaude: boolean
|
||||
isMax20: boolean
|
||||
hasChatGPT: boolean
|
||||
hasOpenAI: boolean
|
||||
hasGemini: boolean
|
||||
hasCopilot: boolean
|
||||
hasOpencodeZen: boolean
|
||||
hasZaiCodingPlan: boolean
|
||||
hasKimiForCoding: boolean
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user