Compare commits
441 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18442a1637 | ||
|
|
d076187f0a | ||
|
|
8a5f61724d | ||
|
|
3f557e593c | ||
|
|
284fafad11 | ||
|
|
884a3addf8 | ||
|
|
c8172697d9 | ||
|
|
6dc8b7b875 | ||
|
|
361d9a82d7 | ||
|
|
d8b4dba963 | ||
|
|
7b89df01a3 | ||
|
|
dcb76f7efd | ||
|
|
7b62f0c68b | ||
|
|
2a7dfac50e | ||
|
|
2b4651e119 | ||
|
|
37d3086658 | ||
|
|
e7dc3721df | ||
|
|
e995443120 | ||
|
|
3a690965fd | ||
|
|
74d2ae1023 | ||
|
|
a0c9381672 | ||
|
|
65a06aa2b7 | ||
|
|
754e6ee064 | ||
|
|
affefee12f | ||
|
|
90463bafd2 | ||
|
|
073a074f8d | ||
|
|
cdda08cdb0 | ||
|
|
a8d26e3f74 | ||
|
|
8401f0a918 | ||
|
|
32470f5ca0 | ||
|
|
c3793f779b | ||
|
|
3de05f6442 | ||
|
|
8514906c3d | ||
|
|
f20e1aa0d0 | ||
|
|
936b51de79 | ||
|
|
38a4bbc75f | ||
|
|
7186c368b9 | ||
|
|
121a3c45c5 | ||
|
|
072b30593e | ||
|
|
dd9eeaa6d6 | ||
|
|
3fa543e851 | ||
|
|
9f52e48e8f | ||
|
|
26ae666bc3 | ||
|
|
422db236fe | ||
|
|
b7c32e8f50 | ||
|
|
c24c4a85b4 | ||
|
|
f3ff32fd18 | ||
|
|
daf011c616 | ||
|
|
c8bc267127 | ||
|
|
c41b38990c | ||
|
|
a4a5502e61 | ||
|
|
4ab93c0cf7 | ||
|
|
a809ac3dfc | ||
|
|
ac99f98b27 | ||
|
|
c8cd6370e2 | ||
|
|
3a68a891c0 | ||
|
|
32d469796b | ||
|
|
f876d60e87 | ||
|
|
4e5321a970 | ||
|
|
7a3df05e47 | ||
|
|
c6bea11cda | ||
|
|
9fe48d252c | ||
|
|
adf8049d4a | ||
|
|
b520eac6f1 | ||
|
|
f722fe6877 | ||
|
|
9742f7d0b9 | ||
|
|
e3924437ce | ||
|
|
0946a6c8f3 | ||
|
|
a413e57676 | ||
|
|
a7b56a0391 | ||
|
|
2ba148be12 | ||
|
|
6df24d3592 | ||
|
|
b58f3edf6d | ||
|
|
0b1fdd508f | ||
|
|
4f3371ce2c | ||
|
|
f9ea9a4ee9 | ||
|
|
b008a57007 | ||
|
|
1a5c9f228d | ||
|
|
6fb933f99b | ||
|
|
f6fbac458e | ||
|
|
4c10723b33 | ||
|
|
10a60854dc | ||
|
|
a6372feaae | ||
|
|
6914f2fd04 | ||
|
|
c8851b51ad | ||
|
|
75f35f1337 | ||
|
|
e99088d70f | ||
|
|
492029ff7c | ||
|
|
58b7aff7bd | ||
|
|
4a991b5a83 | ||
|
|
60b4d20fd8 | ||
|
|
b8c12495b6 | ||
|
|
5a83c61d77 | ||
|
|
ad468ec93f | ||
|
|
0001bc87c2 | ||
|
|
aab8a23243 | ||
|
|
50afb6b2de | ||
|
|
41d790dc04 | ||
|
|
2ac2241367 | ||
|
|
1511886c0c | ||
|
|
283c7e6cb7 | ||
|
|
95aa7595f8 | ||
|
|
c6349dc38a | ||
|
|
17b475eefd | ||
|
|
3a019792e9 | ||
|
|
1ceaaa4311 | ||
|
|
ff8a5f343a | ||
|
|
118150035c | ||
|
|
6c7b6115dd | ||
|
|
157952f293 | ||
|
|
5c8d694491 | ||
|
|
d358e6e48e | ||
|
|
9afd0d1d41 | ||
|
|
eb56701996 | ||
|
|
e4be8cea75 | ||
|
|
d3978ab491 | ||
|
|
306c7f4c8e | ||
|
|
c12c6fa0c0 | ||
|
|
ef1baea163 | ||
|
|
d33af1d27f | ||
|
|
b2f019a987 | ||
|
|
f80b72c2b7 | ||
|
|
ce7fb00847 | ||
|
|
63d3fa7439 | ||
|
|
3eb7dc73b7 | ||
|
|
2df61a2199 | ||
|
|
96f0e787e7 | ||
|
|
4ef6188a41 | ||
|
|
d5fd918bff | ||
|
|
5d3215167a | ||
|
|
3b2d3acd17 | ||
|
|
bb6a011964 | ||
|
|
bfe1730e9f | ||
|
|
67b4665c28 | ||
|
|
b0c570e054 | ||
|
|
fd99a29d6e | ||
|
|
308ad1e98e | ||
|
|
d60697bb13 | ||
|
|
95a4e971a0 | ||
|
|
d8901fa658 | ||
|
|
82c71425a0 | ||
|
|
7e0ab828f9 | ||
|
|
13d960f3ca | ||
|
|
687cc2386f | ||
|
|
d88449b1e2 | ||
|
|
074d8dff09 | ||
|
|
fba916db60 | ||
|
|
f727aab892 | ||
|
|
686f32929c | ||
|
|
af7733f89f | ||
|
|
3553ab79e1 | ||
|
|
fb19e544c9 | ||
|
|
88e1e3d0fa | ||
|
|
11d1e70067 | ||
|
|
17c56d8814 | ||
|
|
6694082a7e | ||
|
|
f9d3a9493a | ||
|
|
7427922e6f | ||
|
|
ea1b22454d | ||
|
|
a8681a9ffe | ||
|
|
c677042f05 | ||
|
|
25c7337fd1 | ||
|
|
b4768014e0 | ||
|
|
162701f56e | ||
|
|
087ce06055 | ||
|
|
967058fe3d | ||
|
|
257eb9277b | ||
|
|
2b87719c83 | ||
|
|
1199e2b839 | ||
|
|
df0b9f7664 | ||
|
|
7fe1a653c8 | ||
|
|
2bf11a8ed7 | ||
|
|
fe1faa6d0f | ||
|
|
6d17ac7d3a | ||
|
|
5a527e214a | ||
|
|
231e790a0c | ||
|
|
45dfc4ec66 | ||
|
|
f84ef532c1 | ||
|
|
563da9470d | ||
|
|
a8a4f54428 | ||
|
|
83f1304e01 | ||
|
|
b538806d5e | ||
|
|
a25d8dfdae | ||
|
|
4f9cec434b | ||
|
|
f3f5b98c68 | ||
|
|
97b7215848 | ||
|
|
61531ca26c | ||
|
|
19a4324b3e | ||
|
|
2fd847d88d | ||
|
|
1717050f73 | ||
|
|
44675fb57f | ||
|
|
7255fec8b3 | ||
|
|
fecc488848 | ||
|
|
b45af0e4d2 | ||
|
|
25be4ab905 | ||
|
|
4f03aea0a1 | ||
|
|
0565ce839e | ||
|
|
bb2df9fec6 | ||
|
|
564bb20f6a | ||
|
|
096233b23f | ||
|
|
7eb67521cb | ||
|
|
498fda11a0 | ||
|
|
5b34a98e0a | ||
|
|
a37259326a | ||
|
|
a5bdb64933 | ||
|
|
11f587194f | ||
|
|
20d009964d | ||
|
|
f22f14d9d1 | ||
|
|
3d5abb950e | ||
|
|
c71f0aa700 | ||
|
|
70ac962fca | ||
|
|
133da2624a | ||
|
|
6a91d72a72 | ||
|
|
b0202e23f7 | ||
|
|
c4572a25fb | ||
|
|
554926209d | ||
|
|
0e49214ee7 | ||
|
|
edc3317e37 | ||
|
|
7fdba56d8f | ||
|
|
247940bf02 | ||
|
|
d6fbe7bd8d | ||
|
|
5ca3d9c489 | ||
|
|
e5abf8702e | ||
|
|
8dd07973a9 | ||
|
|
e55fc1f14c | ||
|
|
f07e364171 | ||
|
|
e26c355c76 | ||
|
|
5f9c3262a2 | ||
|
|
9d726d91fc | ||
|
|
a1d7f9e822 | ||
|
|
06d265c1de | ||
|
|
8a2c3cc98d | ||
|
|
be03e27faf | ||
|
|
2834445067 | ||
|
|
7331cbdea2 | ||
|
|
babcb0050a | ||
|
|
ce37924fd8 | ||
|
|
71728e1546 | ||
|
|
f67a4df07e | ||
|
|
9353ac5b9d | ||
|
|
fecc6b8605 | ||
|
|
34e5eddb49 | ||
|
|
441fda9177 | ||
|
|
46a30cd7ec | ||
|
|
006e6ade02 | ||
|
|
aa447765cb | ||
|
|
bdaa8fc6c1 | ||
|
|
7788ba3d8a | ||
|
|
1324fee30f | ||
|
|
cbb7771525 | ||
|
|
d5f0e75b7d | ||
|
|
c9be2e1696 | ||
|
|
caf08af88b | ||
|
|
e663d7b335 | ||
|
|
e257bff31c | ||
|
|
23bca2b4d5 | ||
|
|
83a05630cd | ||
|
|
6717349e5b | ||
|
|
ee72c45552 | ||
|
|
9377c7eba9 | ||
|
|
f1316bc800 | ||
|
|
1f8f7b592b | ||
|
|
c6fafd6624 | ||
|
|
42dbc8f39c | ||
|
|
6bb9a3b7bc | ||
|
|
f3f6ba47fe | ||
|
|
984da95f15 | ||
|
|
bb86523240 | ||
|
|
f2b7b759c8 | ||
|
|
a5af7e95c0 | ||
|
|
a5489718f9 | ||
|
|
cd5485a472 | ||
|
|
582e0ead27 | ||
|
|
598a4389d1 | ||
|
|
d525958a9d | ||
|
|
3c1e71f256 | ||
|
|
4e5792ce4d | ||
|
|
052beb364f | ||
|
|
4400e18a52 | ||
|
|
480dcff420 | ||
|
|
6e0f6d53a7 | ||
|
|
76fad73550 | ||
|
|
e4583668c0 | ||
|
|
2d22a54b55 | ||
|
|
c2efdb4334 | ||
|
|
d3a3f0c3a6 | ||
|
|
0f145b2e40 | ||
|
|
161d6e4159 | ||
|
|
8dff42830c | ||
|
|
9b841c6edc | ||
|
|
39dc62c62a | ||
|
|
46969935cd | ||
|
|
51ced65b5f | ||
|
|
f8b5771443 | ||
|
|
e3bd43ff64 | ||
|
|
0743855b40 | ||
|
|
2588f33075 | ||
|
|
32193dc10d | ||
|
|
321b319b58 | ||
|
|
c7122b4127 | ||
|
|
a3dd1dbaf9 | ||
|
|
4c1e369176 | ||
|
|
119e18c810 | ||
|
|
06611a7645 | ||
|
|
676ff513fa | ||
|
|
4738379ad7 | ||
|
|
44415e3f59 | ||
|
|
870a2a54f7 | ||
|
|
cfd63482d7 | ||
|
|
5845604a01 | ||
|
|
74a1d70f57 | ||
|
|
89e251da72 | ||
|
|
e7f4f6dd13 | ||
|
|
d8e7e4f170 | ||
|
|
2db9accfc7 | ||
|
|
29155ec7bc | ||
|
|
6b4e149881 | ||
|
|
7f4338b6ed | ||
|
|
24a013b867 | ||
|
|
d769b95869 | ||
|
|
72cf908738 | ||
|
|
f035be842d | ||
|
|
6ce482668b | ||
|
|
a85da59358 | ||
|
|
b88a868173 | ||
|
|
d0bdf521c3 | ||
|
|
7abefcca1f | ||
|
|
a06364081b | ||
|
|
104b9fbb39 | ||
|
|
f6fc30ada5 | ||
|
|
f1fcc26aaa | ||
|
|
09999587f5 | ||
|
|
139f392d76 | ||
|
|
71ac54c33e | ||
|
|
cbeeee4053 | ||
|
|
737bda680c | ||
|
|
ff94aa3033 | ||
|
|
d0c4085ae1 | ||
|
|
56f9de4652 | ||
|
|
b2661be833 | ||
|
|
3d4ed912d7 | ||
|
|
9a338b16f1 | ||
|
|
471bc6e52d | ||
|
|
0cbbdd566e | ||
|
|
825a5e70f7 | ||
|
|
18c161a9cd | ||
|
|
414cecd7df | ||
|
|
2b541b8725 | ||
|
|
ac6e7d00f2 | ||
|
|
fa77be0daf | ||
|
|
13da4ef4aa | ||
|
|
6451b212f8 | ||
|
|
fad7354b13 | ||
|
|
55dc64849f | ||
|
|
e984a5c639 | ||
|
|
46e02b9457 | ||
|
|
5f21ddf473 | ||
|
|
108e860ddd | ||
|
|
b8221a883e | ||
|
|
2c394cd497 | ||
|
|
d84a1c9e95 | ||
|
|
cf29cd137e | ||
|
|
d3f8c7d288 | ||
|
|
d1659152bc | ||
|
|
1cb8f8bee6 | ||
|
|
1760367a25 | ||
|
|
747edcb6e6 | ||
|
|
f3540a9ea3 | ||
|
|
8280e45fe1 | ||
|
|
0eddd28a95 | ||
|
|
36e54acc51 | ||
|
|
817c593e12 | ||
|
|
3ccef5d9b3 | ||
|
|
ae4e113c7e | ||
|
|
403457f9e4 | ||
|
|
5e5c091356 | ||
|
|
1df025ad44 | ||
|
|
844ac26e2a | ||
|
|
2727f0f429 | ||
|
|
89b1205ccf | ||
|
|
d44f5db1e2 | ||
|
|
180fcc3e5d | ||
|
|
3947084cc5 | ||
|
|
67f701cd9e | ||
|
|
f94ae2032c | ||
|
|
c81384456c | ||
|
|
9040383da7 | ||
|
|
c688e978fd | ||
|
|
a0201e17b9 | ||
|
|
dbbec868d5 | ||
|
|
6e2f3b1f50 | ||
|
|
e4bbd6bf15 | ||
|
|
476f154ef5 | ||
|
|
83519cae11 | ||
|
|
9a8f03462f | ||
|
|
daf6c7a19e | ||
|
|
2bb82c250c | ||
|
|
8e92704316 | ||
|
|
f980e256dd | ||
|
|
4d19a22679 | ||
|
|
e1010846c4 | ||
|
|
38169523c4 | ||
|
|
b98697238b | ||
|
|
d5b6a7c575 | ||
|
|
78a08959f6 | ||
|
|
db6a899297 | ||
|
|
7fdbabb264 | ||
|
|
b3ebf6c124 | ||
|
|
8a1b398119 | ||
|
|
66419918f9 | ||
|
|
755a3a94c8 | ||
|
|
5e316499e5 | ||
|
|
266c045b69 | ||
|
|
eafcac1593 | ||
|
|
7927d3675d | ||
|
|
4059d02047 | ||
|
|
c2dfcadbac | ||
|
|
e343e625c7 | ||
|
|
050e6a2187 | ||
|
|
7ede8e04f0 | ||
|
|
1ae7d7d67e | ||
|
|
f9742ddfca | ||
|
|
eb5cc873ea | ||
|
|
847d994199 | ||
|
|
bbe08f0eef | ||
|
|
4454753bb4 | ||
|
|
1c0b41aa65 | ||
|
|
4c6b31e5b4 | ||
|
|
67990293a9 | ||
|
|
dbf584af95 | ||
|
|
368ac310a1 | ||
|
|
cb2169f334 | ||
|
|
ec520e6228 | ||
|
|
6febebc166 | ||
|
|
98f4adbf4b | ||
|
|
d209f3c677 | ||
|
|
a691a3ac0a | ||
|
|
01594a67af | ||
|
|
551dbc95f2 | ||
|
|
f4a9d0c3aa | ||
|
|
f796fdbe0a |
BIN
.github/assets/elestyle.jpg
vendored
Normal file
BIN
.github/assets/elestyle.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -52,12 +52,31 @@ jobs:
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
bun test src/cli/doctor/formatter.test.ts
|
||||
bun test src/cli/doctor/format-default.test.ts
|
||||
bun test src/tools/call-omo-agent/sync-executor.test.ts
|
||||
bun test src/tools/call-omo-agent/session-creator.test.ts
|
||||
bun test src/features/opencode-skill-loader/loader.test.ts
|
||||
|
||||
- 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 \
|
||||
# Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files
|
||||
# that were already run in isolation above.
|
||||
# Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts
|
||||
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
|
||||
bun test bin script src/config src/mcp src/index.test.ts \
|
||||
src/agents src/shared \
|
||||
src/cli/run src/cli/config-manager src/cli/mcp-oauth \
|
||||
src/cli/index.test.ts src/cli/install.test.ts src/cli/model-fallback.test.ts \
|
||||
src/cli/config-manager.test.ts \
|
||||
src/cli/doctor/runner.test.ts src/cli/doctor/checks \
|
||||
src/tools/ast-grep src/tools/background-task src/tools/delegate-task \
|
||||
src/tools/glob src/tools/grep src/tools/interactive-bash \
|
||||
src/tools/look-at src/tools/lsp src/tools/session-manager \
|
||||
src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \
|
||||
src/tools/call-omo-agent/background-agent-executor.test.ts \
|
||||
src/tools/call-omo-agent/background-executor.test.ts \
|
||||
src/tools/call-omo-agent/subagent-session-creator.test.ts \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
@@ -70,7 +89,11 @@ jobs:
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/opencode-skill-loader/config-source-discovery.test.ts \
|
||||
src/features/opencode-skill-loader/merger.test.ts \
|
||||
src/features/opencode-skill-loader/skill-content.test.ts \
|
||||
src/features/opencode-skill-loader/blocking.test.ts \
|
||||
src/features/opencode-skill-loader/async-loader.test.ts \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
|
||||
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: code-yeongyu,bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
|
||||
allowlist: code-yeongyu,bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai,web-flow
|
||||
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).
|
||||
|
||||
|
||||
155
.github/workflows/publish.yml
vendored
155
.github/workflows/publish.yml
vendored
@@ -51,13 +51,33 @@ jobs:
|
||||
# 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
|
||||
bun test src/cli/doctor/formatter.test.ts
|
||||
bun test src/cli/doctor/format-default.test.ts
|
||||
bun test src/tools/call-omo-agent/sync-executor.test.ts
|
||||
bun test src/tools/call-omo-agent/session-creator.test.ts
|
||||
bun test src/features/opencode-skill-loader/loader.test.ts
|
||||
|
||||
- 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 \
|
||||
# Enumerate subdirectories/files explicitly to EXCLUDE mock-heavy files
|
||||
# that were already run in isolation above.
|
||||
# Excluded from src/cli: doctor/formatter.test.ts, doctor/format-default.test.ts
|
||||
# Excluded from src/tools: call-omo-agent/sync-executor.test.ts, call-omo-agent/session-creator.test.ts
|
||||
bun test bin script src/config src/mcp src/index.test.ts \
|
||||
src/agents src/shared \
|
||||
src/cli/run src/cli/config-manager src/cli/mcp-oauth \
|
||||
src/cli/index.test.ts src/cli/install.test.ts src/cli/model-fallback.test.ts \
|
||||
src/cli/config-manager.test.ts \
|
||||
src/cli/doctor/runner.test.ts src/cli/doctor/checks \
|
||||
src/tools/ast-grep src/tools/background-task src/tools/delegate-task \
|
||||
src/tools/glob src/tools/grep src/tools/interactive-bash \
|
||||
src/tools/look-at src/tools/lsp src/tools/session-manager \
|
||||
src/tools/skill src/tools/skill-mcp src/tools/slashcommand src/tools/task \
|
||||
src/tools/call-omo-agent/background-agent-executor.test.ts \
|
||||
src/tools/call-omo-agent/background-executor.test.ts \
|
||||
src/tools/call-omo-agent/subagent-session-creator.test.ts \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
@@ -70,7 +90,11 @@ jobs:
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/opencode-skill-loader/config-source-discovery.test.ts \
|
||||
src/features/opencode-skill-loader/merger.test.ts \
|
||||
src/features/opencode-skill-loader/skill-content.test.ts \
|
||||
src/features/opencode-skill-loader/blocking.test.ts \
|
||||
src/features/opencode-skill-loader/async-loader.test.ts \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
@@ -223,118 +247,23 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- 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: 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}"
|
||||
|
||||
# Get all commits between tags
|
||||
COMMITS=$(git log "v${PREV_TAG}..v${VERSION}" --format="%s" 2>/dev/null || echo "")
|
||||
|
||||
# Initialize sections
|
||||
FEATURES=""
|
||||
FIXES=""
|
||||
REFACTOR=""
|
||||
DOCS=""
|
||||
OTHER=""
|
||||
|
||||
# Store regexes in variables for bash 5.2+ compatibility
|
||||
# (bash 5.2 changed how parentheses are parsed inside [[ =~ ]])
|
||||
re_skip='^(chore|ci|release|test|ignore)'
|
||||
re_feat_scoped='^feat\(([^)]+)\): (.+)$'
|
||||
re_fix_scoped='^fix\(([^)]+)\): (.+)$'
|
||||
re_refactor_scoped='^refactor\(([^)]+)\): (.+)$'
|
||||
re_docs_scoped='^docs\(([^)]+)\): (.+)$'
|
||||
|
||||
while IFS= read -r commit; do
|
||||
[ -z "$commit" ] && continue
|
||||
# Skip chore, ci, release, test commits
|
||||
[[ "$commit" =~ $re_skip ]] && continue
|
||||
|
||||
if [[ "$commit" =~ ^feat ]]; then
|
||||
# Extract scope and message: feat(scope): message -> **scope**: message
|
||||
if [[ "$commit" =~ $re_feat_scoped ]]; then
|
||||
FEATURES="${FEATURES}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||
else
|
||||
MSG="${commit#feat: }"
|
||||
FEATURES="${FEATURES}\n- ${MSG}"
|
||||
fi
|
||||
elif [[ "$commit" =~ ^fix ]]; then
|
||||
if [[ "$commit" =~ $re_fix_scoped ]]; then
|
||||
FIXES="${FIXES}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||
else
|
||||
MSG="${commit#fix: }"
|
||||
FIXES="${FIXES}\n- ${MSG}"
|
||||
fi
|
||||
elif [[ "$commit" =~ ^refactor ]]; then
|
||||
if [[ "$commit" =~ $re_refactor_scoped ]]; then
|
||||
REFACTOR="${REFACTOR}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||
else
|
||||
MSG="${commit#refactor: }"
|
||||
REFACTOR="${REFACTOR}\n- ${MSG}"
|
||||
fi
|
||||
elif [[ "$commit" =~ ^docs ]]; then
|
||||
if [[ "$commit" =~ $re_docs_scoped ]]; then
|
||||
DOCS="${DOCS}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||
else
|
||||
MSG="${commit#docs: }"
|
||||
DOCS="${DOCS}\n- ${MSG}"
|
||||
fi
|
||||
else
|
||||
OTHER="${OTHER}\n- ${commit}"
|
||||
fi
|
||||
done <<< "$COMMITS"
|
||||
|
||||
# Build release notes
|
||||
{
|
||||
echo "## What's Changed"
|
||||
echo ""
|
||||
if [ -n "$FEATURES" ]; then
|
||||
echo "### Features"
|
||||
echo -e "$FEATURES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$FIXES" ]; then
|
||||
echo "### Bug Fixes"
|
||||
echo -e "$FIXES"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$REFACTOR" ]; then
|
||||
echo "### Refactoring"
|
||||
echo -e "$REFACTOR"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$DOCS" ]; then
|
||||
echo "### Documentation"
|
||||
echo -e "$DOCS"
|
||||
echo ""
|
||||
fi
|
||||
if [ -n "$OTHER" ]; then
|
||||
echo "### Other Changes"
|
||||
echo -e "$OTHER"
|
||||
echo ""
|
||||
fi
|
||||
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/v${PREV_TAG}...v${VERSION}"
|
||||
} > /tmp/changelog.md
|
||||
|
||||
bun run script/generate-changelog.ts > /tmp/changelog.md
|
||||
cat /tmp/changelog.md
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub release
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Dependencies
|
||||
.sisyphus/
|
||||
.sisyphus/*
|
||||
!.sisyphus/rules/
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
|
||||
@@ -31,9 +31,9 @@ You are the release manager for oh-my-opencode. Execute the FULL publish workflo
|
||||
{ "id": "sync-remote", "content": "Sync with remote (pull --rebase && push if unpushed commits)", "status": "pending", "priority": "high" },
|
||||
{ "id": "run-workflow", "content": "Trigger GitHub Actions publish workflow", "status": "pending", "priority": "high" },
|
||||
{ "id": "wait-workflow", "content": "Wait for workflow completion (poll every 30s)", "status": "pending", "priority": "high" },
|
||||
{ "id": "verify-release", "content": "Verify GitHub release was created", "status": "pending", "priority": "high" },
|
||||
{ "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-and-preview", "content": "Verify release created + preview auto-generated changelog & contributor thanks", "status": "pending", "priority": "high" },
|
||||
{ "id": "draft-summary", "content": "Draft enhanced release summary (mandatory for minor/major, optional for patch — ask user)", "status": "pending", "priority": "high" },
|
||||
{ "id": "apply-summary", "content": "Prepend enhanced summary to release (if user opted in)", "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" },
|
||||
@@ -111,102 +111,165 @@ gh run view {run_id} --log-failed
|
||||
|
||||
---
|
||||
|
||||
## STEP 5: VERIFY GITHUB RELEASE
|
||||
## STEP 5: VERIFY RELEASE & PREVIEW AUTO-GENERATED CONTENT
|
||||
|
||||
Two goals: confirm the release exists, then show the user what the workflow already generated.
|
||||
|
||||
Get the new version and verify release exists:
|
||||
```bash
|
||||
# Get new version from package.json (workflow updates it)
|
||||
# Pull latest (workflow committed version bump)
|
||||
git pull --rebase
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
gh release view "v${NEW_VERSION}"
|
||||
|
||||
# Verify release exists on GitHub
|
||||
gh release view "v${NEW_VERSION}" --json tagName,url --jq '{tag: .tagName, url: .url}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 6: DRAFT ENHANCED RELEASE NOTES
|
||||
|
||||
Analyze commits since the previous version and draft release notes following project conventions:
|
||||
|
||||
### For PATCH releases:
|
||||
Keep simple format - just list commits:
|
||||
```markdown
|
||||
- {hash} {conventional commit message}
|
||||
- ...
|
||||
```
|
||||
|
||||
### For MINOR releases:
|
||||
Use feature-focused format:
|
||||
```markdown
|
||||
## New Features
|
||||
|
||||
### Feature Name
|
||||
- Description of what it does
|
||||
- Why it matters
|
||||
|
||||
## Bug Fixes
|
||||
- fix(scope): description
|
||||
|
||||
## Improvements
|
||||
- refactor(scope): description
|
||||
```
|
||||
|
||||
### For MAJOR releases:
|
||||
Full changelog format:
|
||||
```markdown
|
||||
# v{version}
|
||||
|
||||
Brief description of the release.
|
||||
|
||||
## What's New Since v{previous}
|
||||
|
||||
### Breaking Changes
|
||||
- Description of breaking change
|
||||
|
||||
### Features
|
||||
- **Feature Name**: Description
|
||||
|
||||
### Bug Fixes
|
||||
- Description
|
||||
|
||||
### Documentation
|
||||
- Description
|
||||
|
||||
## Migration Guide (if applicable)
|
||||
...
|
||||
```
|
||||
|
||||
**CRITICAL: The enhanced notes must ADD to existing workflow-generated notes, not replace them.**
|
||||
|
||||
---
|
||||
|
||||
## STEP 7: UPDATE GITHUB RELEASE
|
||||
|
||||
**ZERO CONTENT LOSS POLICY:**
|
||||
- First, fetch the existing release body with `gh release view`
|
||||
- Your enhanced notes must be PREPENDED to the existing content
|
||||
- **NOT A SINGLE CHARACTER of existing content may be removed or modified**
|
||||
- The final release body = `{your_enhanced_notes}\n\n---\n\n{existing_body_exactly_as_is}`
|
||||
**After verifying, generate a local preview of the auto-generated content:**
|
||||
|
||||
```bash
|
||||
# Get existing body
|
||||
EXISTING_BODY=$(gh release view "v${NEW_VERSION}" --json body --jq '.body')
|
||||
bun run script/generate-changelog.ts
|
||||
```
|
||||
|
||||
# Write enhanced notes to temp file (prepend to existing)
|
||||
cat > /tmp/release-notes-v${NEW_VERSION}.md << 'EOF'
|
||||
{your_enhanced_notes}
|
||||
<agent-instruction>
|
||||
After running the preview, present the output to the user and say:
|
||||
|
||||
> **The following content is ALREADY included in the release automatically:**
|
||||
> - Commit changelog (grouped by feat/fix/refactor)
|
||||
> - Contributor thank-you messages (for non-team contributors)
|
||||
>
|
||||
> You do NOT need to write any of this. It's handled.
|
||||
>
|
||||
> **For a patch release**, this is usually sufficient on its own. However, if there are notable bug fixes or changes worth highlighting, an enhanced summary can be added.
|
||||
> **For a minor/major release**, an enhanced summary is **required** — I'll draft one in the next step.
|
||||
|
||||
Wait for the user to acknowledge before proceeding.
|
||||
</agent-instruction>
|
||||
|
||||
---
|
||||
|
||||
EOF
|
||||
## STEP 6: DRAFT ENHANCED RELEASE SUMMARY
|
||||
|
||||
# Append existing body EXACTLY as-is (zero modifications)
|
||||
echo "$EXISTING_BODY" >> /tmp/release-notes-v${NEW_VERSION}.md
|
||||
<decision-gate>
|
||||
|
||||
# Update release
|
||||
gh release edit "v${NEW_VERSION}" --notes-file /tmp/release-notes-v${NEW_VERSION}.md
|
||||
| Release Type | Action |
|
||||
|-------------|--------|
|
||||
| **patch** | ASK the user: "Would you like me to draft an enhanced summary highlighting the key bug fixes / changes? Or is the auto-generated changelog sufficient?" If user declines → skip to Step 8. If user accepts → draft a concise bug-fix / change summary below. |
|
||||
| **minor** | MANDATORY. Draft a concise feature summary. Do NOT proceed without one. |
|
||||
| **major** | MANDATORY. Draft a full release narrative with migration notes if applicable. Do NOT proceed without one. |
|
||||
|
||||
</decision-gate>
|
||||
|
||||
### What You're Writing (and What You're NOT)
|
||||
|
||||
You are writing the **headline layer** — a product announcement that sits ABOVE the auto-generated commit log. Think "release blog post", not "git log".
|
||||
|
||||
<rules>
|
||||
- NEVER duplicate commit messages. The auto-generated section already lists every commit.
|
||||
- NEVER write generic filler like "Various bug fixes and improvements" or "Several enhancements".
|
||||
- ALWAYS focus on USER IMPACT: what can users DO now that they couldn't before?
|
||||
- ALWAYS group by THEME or CAPABILITY, not by commit type (feat/fix/refactor).
|
||||
- ALWAYS use concrete language: "You can now do X" not "Added X feature".
|
||||
</rules>
|
||||
|
||||
<examples>
|
||||
<bad title="Commit regurgitation — DO NOT do this">
|
||||
## What's New
|
||||
- feat(auth): add JWT refresh token rotation
|
||||
- fix(auth): handle expired token edge case
|
||||
- refactor(auth): extract middleware
|
||||
</bad>
|
||||
|
||||
<good title="User-impact narrative — DO this">
|
||||
## 🔐 Smarter Authentication
|
||||
|
||||
Token refresh is now automatic and seamless. Sessions no longer expire mid-task — the system silently rotates credentials in the background. If you've been frustrated by random logouts, this release fixes that.
|
||||
</good>
|
||||
|
||||
<bad title="Vague filler — DO NOT do this">
|
||||
## Improvements
|
||||
- Various performance improvements
|
||||
- Bug fixes and stability enhancements
|
||||
</bad>
|
||||
|
||||
<good title="Specific and measurable — DO this">
|
||||
## ⚡ 3x Faster Rule Parsing
|
||||
|
||||
Rules are now cached by file modification time. If your project has 50+ rule files, you'll notice startup is noticeably faster — we measured a 3x improvement in our test suite.
|
||||
</good>
|
||||
</examples>
|
||||
|
||||
### Drafting Process
|
||||
|
||||
1. **Analyze** the commit list from Step 5's preview. Identify 2-5 themes that matter to users.
|
||||
2. **Write** the summary to `/tmp/release-summary-v${NEW_VERSION}.md`.
|
||||
3. **Present** the draft to the user for review and approval before applying.
|
||||
|
||||
```bash
|
||||
# Write your draft here
|
||||
cat > /tmp/release-summary-v${NEW_VERSION}.md << 'SUMMARY_EOF'
|
||||
{your_enhanced_summary}
|
||||
SUMMARY_EOF
|
||||
|
||||
cat /tmp/release-summary-v${NEW_VERSION}.md
|
||||
```
|
||||
|
||||
**CRITICAL: This is ADDITIVE ONLY. You are adding your notes on top. The existing content remains 100% intact.**
|
||||
<agent-instruction>
|
||||
After drafting, ask the user:
|
||||
> "Here's the release summary I drafted. This will appear AT THE TOP of the release notes, above the auto-generated commit changelog and contributor thanks. Want me to adjust anything before applying?"
|
||||
|
||||
Do NOT proceed to Step 7 without user confirmation.
|
||||
</agent-instruction>
|
||||
|
||||
---
|
||||
|
||||
## STEP 7: APPLY ENHANCED SUMMARY TO RELEASE
|
||||
|
||||
**Skip this step ONLY if the user opted out of the enhanced summary in Step 6** — proceed directly to Step 8.
|
||||
|
||||
<architecture>
|
||||
The final release note structure:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Enhanced Summary (from Step 6) │ ← You wrote this
|
||||
│ - Theme-based, user-impact focused │
|
||||
├─────────────────────────────────────┤
|
||||
│ --- (separator) │
|
||||
├─────────────────────────────────────┤
|
||||
│ Auto-generated Commit Changelog │ ← Workflow wrote this
|
||||
│ - feat/fix/refactor grouped │
|
||||
│ - Contributor thank-you messages │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
</architecture>
|
||||
|
||||
<zero-content-loss-policy>
|
||||
- Fetch the existing release body FIRST
|
||||
- PREPEND your summary above it
|
||||
- The existing auto-generated content must remain 100% INTACT
|
||||
- NOT A SINGLE CHARACTER of existing content may be removed or modified
|
||||
</zero-content-loss-policy>
|
||||
|
||||
```bash
|
||||
# 1. Fetch existing auto-generated body
|
||||
EXISTING_BODY=$(gh release view "v${NEW_VERSION}" --json body --jq '.body')
|
||||
|
||||
# 2. Combine: enhanced summary on top, auto-generated below
|
||||
{
|
||||
cat /tmp/release-summary-v${NEW_VERSION}.md
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "$EXISTING_BODY"
|
||||
} > /tmp/final-release-v${NEW_VERSION}.md
|
||||
|
||||
# 3. Update the release (additive only)
|
||||
gh release edit "v${NEW_VERSION}" --notes-file /tmp/final-release-v${NEW_VERSION}.md
|
||||
|
||||
# 4. Confirm
|
||||
echo "✅ Release v${NEW_VERSION} updated with enhanced summary."
|
||||
gh release view "v${NEW_VERSION}" --json url --jq '.url'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -41,27 +41,27 @@ Fire ALL simultaneously:
|
||||
|
||||
```
|
||||
// Agent 1: Find all exported symbols
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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.")
|
||||
```
|
||||
|
||||
@@ -21,7 +21,7 @@ You are a GitHub issue triage automation agent. Your job is to:
|
||||
|
||||
| Aspect | Rule |
|
||||
|--------|------|
|
||||
| **Task Granularity** | 1 Issue = Exactly 1 `delegate_task()` call |
|
||||
| **Task Granularity** | 1 Issue = Exactly 1 `task()` call |
|
||||
| **Execution Mode** | `run_in_background=true` (Each issue runs independently) |
|
||||
| **Result Handling** | `background_output()` to collect results as they complete |
|
||||
| **Reporting** | IMMEDIATE streaming when each task finishes |
|
||||
@@ -67,7 +67,7 @@ for (let i = 0; i < allIssues.length; i++) {
|
||||
const issue = allIssues[i]
|
||||
const category = getCategory(i)
|
||||
|
||||
const taskId = await delegate_task(
|
||||
const taskId = await task(
|
||||
category=category,
|
||||
load_skills=[],
|
||||
run_in_background=true, // ← CRITICAL: Each issue is independent background task
|
||||
@@ -195,7 +195,7 @@ for (let i = 0; i < allIssues.length; i++) {
|
||||
|
||||
console.log(`🚀 Launching background task for Issue #${issue.number} (${category})...`)
|
||||
|
||||
const taskId = await delegate_task(
|
||||
const taskId = await task(
|
||||
category=category,
|
||||
load_skills=[],
|
||||
run_in_background=true, // ← BACKGROUND TASK: Each issue runs independently
|
||||
@@ -480,7 +480,7 @@ When invoked, immediately:
|
||||
4. Exhaustive pagination for issues
|
||||
5. Exhaustive pagination for PRs
|
||||
6. **LAUNCH**: For each issue:
|
||||
- `delegate_task(run_in_background=true)` - 1 task per issue
|
||||
- `task(run_in_background=true)` - 1 task per issue
|
||||
- Store taskId mapped to issue number
|
||||
7. **STREAM**: Poll `background_output()` for each task:
|
||||
- As each completes, immediately report result
|
||||
|
||||
@@ -22,7 +22,7 @@ You are a GitHub Pull Request triage automation agent. Your job is to:
|
||||
|
||||
| Aspect | Rule |
|
||||
|--------|------|
|
||||
| **Task Granularity** | 1 PR = Exactly 1 `delegate_task()` call |
|
||||
| **Task Granularity** | 1 PR = Exactly 1 `task()` call |
|
||||
| **Execution Mode** | `run_in_background=true` (Each PR runs independently) |
|
||||
| **Result Handling** | `background_output()` to collect results as they complete |
|
||||
| **Reporting** | IMMEDIATE streaming when each task finishes |
|
||||
@@ -68,7 +68,7 @@ for (let i = 0; i < allPRs.length; i++) {
|
||||
const pr = allPRs[i]
|
||||
const category = getCategory(i)
|
||||
|
||||
const taskId = await delegate_task(
|
||||
const taskId = await task(
|
||||
category=category,
|
||||
load_skills=[],
|
||||
run_in_background=true, // ← CRITICAL: Each PR is independent background task
|
||||
@@ -178,7 +178,7 @@ for (let i = 0; i < allPRs.length; i++) {
|
||||
|
||||
console.log(`🚀 Launching background task for PR #${pr.number} (${category})...`)
|
||||
|
||||
const taskId = await delegate_task(
|
||||
const taskId = await task(
|
||||
category=category,
|
||||
load_skills=[],
|
||||
run_in_background=true, // ← BACKGROUND TASK: Each PR runs independently
|
||||
@@ -474,7 +474,7 @@ When invoked, immediately:
|
||||
2. `gh repo view --json nameWithOwner -q .nameWithOwner`
|
||||
3. Exhaustive pagination for ALL open PRs
|
||||
4. **LAUNCH**: For each PR:
|
||||
- `delegate_task(run_in_background=true)` - 1 task per PR
|
||||
- `task(run_in_background=true)` - 1 task per PR
|
||||
- Store taskId mapped to PR number
|
||||
5. **STREAM**: Poll `background_output()` for each task:
|
||||
- As each completes, immediately report result
|
||||
|
||||
117
.sisyphus/rules/modular-code-enforcement.md
Normal file
117
.sisyphus/rules/modular-code-enforcement.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
globs: ["**/*.ts", "**/*.tsx"]
|
||||
alwaysApply: false
|
||||
description: "Enforces strict modular code architecture: SRP, no monolithic index.ts, 200 LOC hard limit"
|
||||
---
|
||||
|
||||
<MANDATORY_ARCHITECTURE_RULE severity="BLOCKING" priority="HIGHEST">
|
||||
|
||||
# Modular Code Architecture — Zero Tolerance Policy
|
||||
|
||||
This rule is NON-NEGOTIABLE. Violations BLOCK all further work until resolved.
|
||||
|
||||
## Rule 1: index.ts is an ENTRY POINT, NOT a dumping ground
|
||||
|
||||
`index.ts` files MUST ONLY contain:
|
||||
- Re-exports (`export { ... } from "./module"`)
|
||||
- Factory function calls that compose modules
|
||||
- Top-level wiring/registration (hook registration, plugin setup)
|
||||
|
||||
`index.ts` MUST NEVER contain:
|
||||
- Business logic implementation
|
||||
- Helper/utility functions
|
||||
- Type definitions beyond simple re-exports
|
||||
- Multiple unrelated responsibilities mixed together
|
||||
|
||||
**If you find mixed logic in index.ts**: Extract each responsibility into its own dedicated file BEFORE making any other changes. This is not optional.
|
||||
|
||||
## Rule 2: No Catch-All Files — utils.ts / service.ts are CODE SMELLS
|
||||
|
||||
A single `utils.ts`, `helpers.ts`, `service.ts`, or `common.ts` is a **gravity well** — every unrelated function gets tossed in, and it grows into an untestable, unreviewable blob.
|
||||
|
||||
**These file names are BANNED as top-level catch-alls.** Instead:
|
||||
|
||||
| Anti-Pattern | Refactor To |
|
||||
|--------------|-------------|
|
||||
| `utils.ts` with `formatDate()`, `slugify()`, `retry()` | `date-formatter.ts`, `slugify.ts`, `retry.ts` |
|
||||
| `service.ts` handling auth + billing + notifications | `auth-service.ts`, `billing-service.ts`, `notification-service.ts` |
|
||||
| `helpers.ts` with 15 unrelated exports | One file per logical domain |
|
||||
|
||||
**Design for reusability from the start.** Each module should be:
|
||||
- **Independently importable** — no consumer should need to pull in unrelated code
|
||||
- **Self-contained** — its dependencies are explicit, not buried in a shared grab-bag
|
||||
- **Nameable by purpose** — the filename alone tells you what it does
|
||||
|
||||
If you catch yourself typing `utils.ts` or `service.ts`, STOP and name the file after what it actually does.
|
||||
|
||||
## Rule 3: Single Responsibility Principle — ABSOLUTE
|
||||
|
||||
Every `.ts` file MUST have exactly ONE clear, nameable responsibility.
|
||||
|
||||
**Self-test**: If you cannot describe the file's purpose in ONE short phrase (e.g., "parses YAML frontmatter", "matches rules against file paths"), the file does too much. Split it.
|
||||
|
||||
| Signal | Action |
|
||||
|--------|--------|
|
||||
| File has 2+ unrelated exported functions | **SPLIT NOW** — each into its own module |
|
||||
| File mixes I/O with pure logic | **SPLIT NOW** — separate side effects from computation |
|
||||
| File has both types and implementation | **SPLIT NOW** — types.ts + implementation.ts |
|
||||
| You need to scroll to understand the file | **SPLIT NOW** — it's too large |
|
||||
|
||||
## Rule 4: 200 LOC Hard Limit — CODE SMELL DETECTOR
|
||||
|
||||
Any `.ts`/`.tsx` file exceeding **200 lines of code** (excluding prompt strings, template literals containing prompts, and `.md` content) is an **immediate code smell**.
|
||||
|
||||
**When you detect a file > 200 LOC**:
|
||||
1. **STOP** current work
|
||||
2. **Identify** the multiple responsibilities hiding in the file
|
||||
3. **Extract** each responsibility into a focused module
|
||||
4. **Verify** each resulting file is < 200 LOC and has a single purpose
|
||||
5. **Resume** original work
|
||||
|
||||
Prompt-heavy files (agent definitions, skill definitions) where the bulk of content is template literal prompt text are EXEMPT from the LOC count — but their non-prompt logic must still be < 200 LOC.
|
||||
|
||||
### How to Count LOC
|
||||
|
||||
**Count these** (= actual logic):
|
||||
- Import statements
|
||||
- Variable/constant declarations
|
||||
- Function/class/interface/type definitions
|
||||
- Control flow (`if`, `for`, `while`, `switch`, `try/catch`)
|
||||
- Expressions, assignments, return statements
|
||||
- Closing braces `}` that belong to logic blocks
|
||||
|
||||
**Exclude these** (= not logic):
|
||||
- Blank lines
|
||||
- Comment-only lines (`//`, `/* */`, `/** */`)
|
||||
- Lines inside template literals that are prompt/instruction text (e.g., the string body of `` const prompt = `...` ``)
|
||||
- Lines inside multi-line strings used as documentation/prompt content
|
||||
|
||||
**Quick method**: Read the file → subtract blank lines, comment-only lines, and prompt string content → remaining count = LOC.
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// 1 import { foo } from "./foo"; ← COUNT
|
||||
// 2 ← SKIP (blank)
|
||||
// 3 // Helper for bar ← SKIP (comment)
|
||||
// 4 export function bar(x: number) { ← COUNT
|
||||
// 5 const prompt = ` ← COUNT (declaration)
|
||||
// 6 You are an assistant. ← SKIP (prompt text)
|
||||
// 7 Follow these rules: ← SKIP (prompt text)
|
||||
// 8 `; ← COUNT (closing)
|
||||
// 9 return process(prompt, x); ← COUNT
|
||||
// 10 } ← COUNT
|
||||
```
|
||||
→ LOC = **5** (lines 1, 4, 5, 9, 10). Not 10.
|
||||
|
||||
When in doubt, **round up** — err on the side of splitting.
|
||||
|
||||
## How to Apply
|
||||
|
||||
When reading, writing, or editing ANY `.ts`/`.tsx` file:
|
||||
|
||||
1. **Check the file you're touching** — does it violate any rule above?
|
||||
2. **If YES** — refactor FIRST, then proceed with your task
|
||||
3. **If creating a new file** — ensure it has exactly one responsibility and stays under 200 LOC
|
||||
4. **If adding code to an existing file** — verify the addition doesn't push the file past 200 LOC or add a second responsibility. If it does, extract into a new module.
|
||||
|
||||
</MANDATORY_ARCHITECTURE_RULE>
|
||||
208
AGENTS.md
208
AGENTS.md
@@ -1,7 +1,7 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-02-06T18:30:00+09:00
|
||||
**Commit:** c6c149e
|
||||
**Generated:** 2026-02-10T14:44:00+09:00
|
||||
**Commit:** b538806d
|
||||
**Branch:** dev
|
||||
|
||||
---
|
||||
@@ -27,12 +27,14 @@ feature branches (your work)
|
||||
| **ALL PRs → `dev`** | Every pull request MUST target the `dev` branch |
|
||||
| **NEVER PR → `master`** | PRs to `master` are **automatically rejected** by CI |
|
||||
| **"Create a PR" = target `dev`** | When asked to create a new PR, it ALWAYS means targeting `dev` |
|
||||
| **Merge commit ONLY** | Squash merge is **disabled** in this repo. Always use merge commit when merging PRs. |
|
||||
|
||||
### Why This Matters
|
||||
|
||||
- `master` = production/published npm package
|
||||
- `dev` = integration branch where features are merged and tested
|
||||
- Feature branches → `dev` → (after testing) → `master`
|
||||
- Squash merge is disabled at the repository level — attempting it will fail
|
||||
|
||||
**If you create a PR targeting `master`, it WILL be rejected. No exceptions.**
|
||||
|
||||
@@ -75,11 +77,6 @@ Oh-My-OpenCode is a **plugin for OpenCode**. You will frequently need to examine
|
||||
| Debugging plugin issues | Fire `librarian` to find relevant OpenCode internals |
|
||||
| Answering "how does OpenCode do X?" | Fire `librarian` FIRST |
|
||||
|
||||
**The `librarian` agent is specialized for:**
|
||||
- Searching remote codebases (GitHub)
|
||||
- Retrieving official documentation
|
||||
- Finding implementation examples in open source
|
||||
|
||||
**DO NOT guess or hallucinate about OpenCode internals.** Always verify by examining actual source code via `librarian` or direct clone.
|
||||
|
||||
---
|
||||
@@ -90,8 +87,6 @@ Oh-My-OpenCode is a **plugin for OpenCode**. You will frequently need to examine
|
||||
|
||||
### All Project Communications MUST Be in English
|
||||
|
||||
This is an **international open-source project**. To ensure accessibility and maintainability:
|
||||
|
||||
| Context | Language Requirement |
|
||||
|---------|---------------------|
|
||||
| **GitHub Issues** | English ONLY |
|
||||
@@ -101,64 +96,74 @@ This is an **international open-source project**. To ensure accessibility and ma
|
||||
| **Documentation** | English ONLY |
|
||||
| **AGENTS.md files** | English ONLY |
|
||||
|
||||
### Why This Matters
|
||||
|
||||
- **Global Collaboration**: Contributors from all countries can participate
|
||||
- **Searchability**: English keywords are universally searchable
|
||||
- **AI Agent Compatibility**: AI tools work best with English content
|
||||
- **Consistency**: Mixed languages create confusion and fragmentation
|
||||
|
||||
### Enforcement
|
||||
|
||||
- Issues/PRs with non-English content may be closed with a request to resubmit in English
|
||||
- Commit messages must be in English - CI may reject non-English commits
|
||||
- Translated READMEs exist (README.ko.md, README.ja.md, etc.) but the primary docs are English
|
||||
|
||||
**If you're not comfortable writing in English, use translation tools. Broken English is fine - we'll help fix it. Non-English is not acceptable.**
|
||||
**If you're not comfortable writing in English, use translation tools. Broken English is fine. Non-English is not acceptable.**
|
||||
|
||||
---
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
OpenCode plugin: multi-model agent orchestration (Claude Opus 4.6, GPT-5.3 Codex, Gemini 3 Flash). 40+ lifecycle hooks, 25+ tools (LSP, AST-Grep, delegation), 11 specialized agents, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
|
||||
OpenCode plugin (v3.4.0): multi-model agent orchestration with 11 specialized agents (Claude Opus 4.6, GPT-5.3 Codex, Gemini 3 Flash, GLM-4.7, Grok). 41 lifecycle hooks across 7 event types, 25+ tools (LSP, AST-Grep, delegation, task management), full Claude Code compatibility layer. "oh-my-zsh" for OpenCode.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
oh-my-opencode/
|
||||
├── src/
|
||||
│ ├── agents/ # 11 AI agents - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 40+ lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 25+ tools - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, skills, Claude Code compat - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 66 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 (schema.ts 455 lines), TypeScript types
|
||||
│ ├── plugin-handlers/ # Plugin config loading (config-handler.ts 501 lines)
|
||||
│ ├── index.ts # Main plugin entry (924 lines)
|
||||
│ ├── plugin-config.ts # Config loading orchestration
|
||||
│ └── plugin-state.ts # Model cache state
|
||||
├── script/ # build-schema.ts, build-binaries.ts, publish.ts
|
||||
├── packages/ # 11 platform-specific binaries
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
│ ├── agents/ # 11 AI agents - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 41 lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 25+ tools - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, skills, CC compat - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 84 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 - see src/config/AGENTS.md
|
||||
│ ├── plugin-handlers/ # Config loading - see src/plugin-handlers/AGENTS.md
|
||||
│ ├── plugin/ # Plugin interface composition (21 files)
|
||||
│ ├── index.ts # Main plugin entry (88 lines)
|
||||
│ ├── create-hooks.ts # Hook creation coordination (62 lines)
|
||||
│ ├── create-managers.ts # Manager initialization (80 lines)
|
||||
│ ├── create-tools.ts # Tool registry composition (54 lines)
|
||||
│ ├── plugin-interface.ts # Plugin interface assembly (66 lines)
|
||||
│ ├── plugin-config.ts # Config loading orchestration
|
||||
│ └── plugin-state.ts # Model cache state
|
||||
├── script/ # build-schema.ts, build-binaries.ts, publish.ts, generate-changelog.ts
|
||||
├── packages/ # 7 platform-specific binary packages
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
```
|
||||
|
||||
## INITIALIZATION FLOW
|
||||
|
||||
```
|
||||
OhMyOpenCodePlugin(ctx)
|
||||
1. injectServerAuthIntoClient(ctx.client)
|
||||
2. startTmuxCheck()
|
||||
3. loadPluginConfig(ctx.directory, ctx) → OhMyOpenCodeConfig
|
||||
4. createFirstMessageVariantGate()
|
||||
5. createModelCacheState()
|
||||
6. createManagers(ctx, config, tmux, cache) → TmuxSessionManager, BackgroundManager, SkillMcpManager, ConfigHandler
|
||||
7. createTools(ctx, config, managers) → filteredTools, mergedSkills, availableSkills, availableCategories
|
||||
8. createHooks(ctx, config, backgroundMgr) → 41 hooks (core + continuation + skill)
|
||||
9. createPluginInterface(...) → tool, chat.params, chat.message, event, tool.execute.before/after
|
||||
10. Return plugin with experimental.session.compacting
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Add agent | `src/agents/` | Create .ts with factory, add to `agentSources` in utils.ts |
|
||||
| Add hook | `src/hooks/` | Create dir with `createXXXHook()`, register in index.ts |
|
||||
| Add agent | `src/agents/` | Create .ts with factory, add to `agentSources` in builtin-agents/ |
|
||||
| Add hook | `src/hooks/` | Create dir, register in `src/plugin/hooks/create-*-hooks.ts` |
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
|
||||
| Add MCP | `src/mcp/` | Create config, add to `createBuiltinMcps()` |
|
||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||
| Add skill | `src/features/builtin-skills/` | Create .ts in skills/ |
|
||||
| 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` |
|
||||
| Config schema | `src/config/schema/` | 21 schema component files, run `bun run build:schema` |
|
||||
| Plugin config | `src/plugin-handlers/config-handler.ts` | JSONC loading, merging, migration |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1556 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (770 lines) |
|
||||
| Delegation | `src/tools/delegate-task/` | Category routing (executor.ts 983 lines) |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1646 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (1976 lines) |
|
||||
| Delegation | `src/tools/delegate-task/` | Category routing (constants.ts 569 lines) |
|
||||
| Task system | `src/features/claude-tasks/` | Task schema, storage, todo sync |
|
||||
| Plugin interface | `src/plugin/` | 21 files composing hooks, handlers, registries |
|
||||
|
||||
## TDD (Test-Driven Development)
|
||||
|
||||
@@ -170,7 +175,7 @@ oh-my-opencode/
|
||||
**Rules:**
|
||||
- NEVER write implementation before test
|
||||
- NEVER delete failing tests - fix the code
|
||||
- Test file: `*.test.ts` alongside source (100+ test files)
|
||||
- Test file: `*.test.ts` alongside source (176 test files)
|
||||
- BDD comments: `//#given`, `//#when`, `//#then`
|
||||
|
||||
## CONVENTIONS
|
||||
@@ -180,8 +185,9 @@ oh-my-opencode/
|
||||
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||
- **Exports**: Barrel pattern via index.ts
|
||||
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
|
||||
- **Testing**: BDD comments, 100+ test files
|
||||
- **Testing**: BDD comments, 176 test files, 117k+ lines TypeScript
|
||||
- **Temperature**: 0.1 for code agents, max 0.3
|
||||
- **Modular architecture**: 200 LOC hard limit per file (prompt strings exempt)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
@@ -195,7 +201,7 @@ oh-my-opencode/
|
||||
| Type Safety | `as any`, `@ts-ignore`, `@ts-expect-error` |
|
||||
| Error Handling | Empty catch blocks |
|
||||
| Testing | Deleting failing tests, writing implementation before test |
|
||||
| Agent Calls | Sequential - use `delegate_task` parallel |
|
||||
| Agent Calls | Sequential - use `task` parallel |
|
||||
| Hook Logic | Heavy PreToolUse - slows every call |
|
||||
| Commits | Giant (3+ files), separate test from impl |
|
||||
| Temperature | >0.3 for code agents |
|
||||
@@ -204,22 +210,57 @@ oh-my-opencode/
|
||||
| Git | Skip hooks (--no-verify), force push without request |
|
||||
| Bash | `sleep N` - use conditional waits |
|
||||
| Bash | `cd dir && cmd` - use workdir parameter |
|
||||
| Files | Catch-all utils.ts/helpers.ts - name by purpose |
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-6 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.3-codex → gemini-3-pro) |
|
||||
| Hephaestus | openai/gpt-5.3-codex | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.3-codex, no fallback) |
|
||||
| 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 | xai/grok-code-fast-1 | Fast codebase grep (fallback: claude-haiku-4-5 → gpt-5-mini → gpt-5-nano) |
|
||||
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-6 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Metis | anthropic/claude-opus-4-6 | Pre-planning analysis (temp 0.3, fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Momus | openai/gpt-5.2 | Plan validation (temp 0.1, fallback: claude-opus-4-6) |
|
||||
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | Category-spawned executor (temp 0.1) |
|
||||
| Agent | Model | Temp | Purpose |
|
||||
|-------|-------|------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-6 | 0.1 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.3-codex → gemini-3-pro) |
|
||||
| Hephaestus | openai/gpt-5.3-codex | 0.1 | Autonomous deep worker (NO fallback) |
|
||||
| Atlas | anthropic/claude-sonnet-4-5 | 0.1 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Prometheus | anthropic/claude-opus-4-6 | 0.1 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| oracle | openai/gpt-5.2 | 0.1 | Consultation, debugging (fallback: claude-opus-4-6) |
|
||||
| librarian | zai-coding-plan/glm-4.7 | 0.1 | Docs, GitHub search (fallback: glm-4.7-free) |
|
||||
| explore | xai/grok-code-fast-1 | 0.1 | Fast codebase grep (fallback: claude-haiku-4-5 → gpt-5-mini → gpt-5-nano) |
|
||||
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
|
||||
| Metis | anthropic/claude-opus-4-6 | 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-6) |
|
||||
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
|
||||
|
||||
## OPENCODE PLUGIN API
|
||||
|
||||
Plugin SDK from `@opencode-ai/plugin` (v1.1.19). Plugin = `async (PluginInput) => Hooks`.
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `tool` | Register custom tools (Record<string, ToolDefinition>) |
|
||||
| `chat.message` | Intercept user messages (can modify parts) |
|
||||
| `chat.params` | Modify LLM parameters (temperature, topP, options) |
|
||||
| `tool.execute.before` | Pre-tool interception (can modify args) |
|
||||
| `tool.execute.after` | Post-tool processing (can modify output) |
|
||||
| `event` | Session lifecycle events (session.created, session.stop, etc.) |
|
||||
| `config` | Config modification (register agents, MCPs, commands) |
|
||||
| `experimental.chat.messages.transform` | Transform message history |
|
||||
| `experimental.session.compacting` | Session compaction customization |
|
||||
|
||||
## DEPENDENCIES
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `@opencode-ai/plugin` + `sdk` | OpenCode integration SDK |
|
||||
| `@ast-grep/cli` + `napi` | AST pattern matching (search/replace) |
|
||||
| `@code-yeongyu/comment-checker` | AI comment detection/prevention |
|
||||
| `@modelcontextprotocol/sdk` | MCP client for remote HTTP servers |
|
||||
| `@clack/prompts` | Interactive CLI TUI |
|
||||
| `commander` | CLI argument parsing |
|
||||
| `zod` (v4) | Schema validation for config |
|
||||
| `jsonc-parser` | JSONC config with comments |
|
||||
| `picocolors` | Terminal colors |
|
||||
| `picomatch` | Glob pattern matching |
|
||||
| `vscode-jsonrpc` | LSP communication |
|
||||
| `js-yaml` | YAML parsing (tasks, skills) |
|
||||
| `detect-libc` | Platform binary selection |
|
||||
|
||||
## COMMANDS
|
||||
|
||||
@@ -227,7 +268,8 @@ oh-my-opencode/
|
||||
bun run typecheck # Type check
|
||||
bun run build # ESM + declarations + schema
|
||||
bun run rebuild # Clean + Build
|
||||
bun test # 100+ test files
|
||||
bun test # 176 test files
|
||||
bun run build:schema # Regenerate JSON schema
|
||||
```
|
||||
|
||||
## DEPLOYMENT
|
||||
@@ -241,38 +283,38 @@ bun test # 100+ test files
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/features/background-agent/manager.ts` | 1556 | Task lifecycle, concurrency |
|
||||
| `src/features/builtin-skills/skills/git-master.ts` | 1107 | Git master skill definition |
|
||||
| `src/tools/delegate-task/executor.ts` | 983 | Category-based delegation executor |
|
||||
| `src/index.ts` | 924 | Main plugin entry |
|
||||
| `src/tools/lsp/client.ts` | 803 | LSP client operations |
|
||||
| `src/hooks/atlas/index.ts` | 770 | Orchestrator hook |
|
||||
| `src/tools/background-task/tools.ts` | 734 | Background task tools |
|
||||
| `src/cli/config-manager.ts` | 667 | JSONC config parsing |
|
||||
| `src/features/skill-mcp-manager/manager.ts` | 640 | MCP client lifecycle |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
||||
| `src/agents/hephaestus.ts` | 618 | Autonomous deep worker agent |
|
||||
| `src/tools/delegate-task/constants.ts` | 552 | Delegation constants |
|
||||
| `src/cli/install.ts` | 542 | Interactive CLI installer |
|
||||
| `src/agents/sisyphus.ts` | 530 | Main orchestrator agent |
|
||||
| `src/features/background-agent/manager.ts` | 1646 | Task lifecycle, concurrency |
|
||||
| `src/hooks/anthropic-context-window-limit-recovery/` | 2232 | Multi-strategy context recovery |
|
||||
| `src/hooks/claude-code-hooks/` | 2110 | Claude Code settings.json compat |
|
||||
| `src/hooks/todo-continuation-enforcer/` | 2061 | Core boulder mechanism |
|
||||
| `src/hooks/atlas/` | 1976 | Session orchestration |
|
||||
| `src/hooks/ralph-loop/` | 1687 | Self-referential dev loop |
|
||||
| `src/hooks/keyword-detector/` | 1665 | Mode detection (ultrawork/search) |
|
||||
| `src/hooks/rules-injector/` | 1604 | Conditional rules injection |
|
||||
| `src/hooks/think-mode/` | 1365 | Model/variant switching |
|
||||
| `src/hooks/session-recovery/` | 1279 | Auto error recovery |
|
||||
| `src/features/builtin-skills/skills/git-master.ts` | 1111 | Git master skill |
|
||||
| `src/tools/delegate-task/constants.ts` | 569 | Category routing configs |
|
||||
|
||||
## MCP ARCHITECTURE
|
||||
|
||||
Three-tier system:
|
||||
1. **Built-in**: websearch (Exa/Tavily), context7 (docs), grep_app (GitHub)
|
||||
2. **Claude Code compat**: .mcp.json with `${VAR}` expansion
|
||||
3. **Skill-embedded**: YAML frontmatter in skills
|
||||
1. **Built-in** (src/mcp/): websearch (Exa/Tavily), context7 (docs), grep_app (GitHub)
|
||||
2. **Claude Code compat** (features/claude-code-mcp-loader/): .mcp.json with `${VAR}` expansion
|
||||
3. **Skill-embedded** (features/opencode-skill-loader/): YAML frontmatter in SKILL.md
|
||||
|
||||
## CONFIG SYSTEM
|
||||
|
||||
- **Zod validation**: `src/config/schema.ts` (455 lines)
|
||||
- **Zod validation**: 21 schema component files in `src/config/schema/`
|
||||
- **JSONC support**: Comments, trailing commas
|
||||
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
|
||||
- **Loading**: `src/plugin-handlers/config-handler.ts` → merge → validate
|
||||
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`) → Defaults
|
||||
- **Migration**: Legacy config auto-migration in `src/shared/migration/`
|
||||
|
||||
## NOTES
|
||||
|
||||
- **OpenCode**: Requires >= 1.0.150
|
||||
- **1069 TypeScript files**, 176 test files, 117k+ lines
|
||||
- **Flaky tests**: ralph-loop (CI timeout), session-state (parallel pollution)
|
||||
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
||||
- **No linter/formatter**: No ESLint, Prettier, or Biome configured
|
||||
- **License**: SUL-1.0 (Sisyphus Use License)
|
||||
|
||||
@@ -370,6 +370,8 @@ OpenCode が Debian / ArchLinux だとしたら、Oh My OpenCode は Ubuntu / [O
|
||||
- Making Spray - influencer marketing solution, vovushop - crossborder commerce platform, vreview - ai commerce review marketing solution
|
||||
- [Google](https://google.com)
|
||||
- [Microsoft](https://microsoft.com)
|
||||
- [ELESTYLE](https://elestyle.jp)
|
||||
- elepay - マルチモバイル決済ゲートウェイ、OneQR - キャッシュレスソリューション向けモバイルアプリケーションSaaS
|
||||
|
||||
## スポンサー
|
||||
- **Numman Ali** [GitHub](https://github.com/numman-ali) [X](https://x.com/nummanali)
|
||||
|
||||
@@ -379,5 +379,7 @@ OpenCode가 Debian/Arch라면 Oh My OpenCode는 Ubuntu/[Omarchy](https://omarchy
|
||||
- Spray(인플루언서 마케팅 솔루션), vovushop(국가 간 상거래 플랫폼), vreview(AI 상거래 리뷰 마케팅 솔루션) 제작
|
||||
- [Google](https://google.com)
|
||||
- [Microsoft](https://microsoft.com)
|
||||
- [ELESTYLE](https://elestyle.jp)
|
||||
- elepay - 멀티 모바일 결제 게이트웨이, OneQR - 캐시리스 솔루션용 모바일 애플리케이션 SaaS
|
||||
|
||||
*이 놀라운 히어로 이미지에 대해 [@junhoyeo](https://github.com/junhoyeo)에게 특별히 감사드립니다.*
|
||||
|
||||
@@ -280,10 +280,10 @@ To remove oh-my-opencode:
|
||||
|
||||
```bash
|
||||
# Remove user config
|
||||
rm -f ~/.config/opencode/oh-my-opencode.json
|
||||
rm -f ~/.config/opencode/oh-my-opencode.json ~/.config/opencode/oh-my-opencode.jsonc
|
||||
|
||||
# Remove project config (if exists)
|
||||
rm -f .opencode/oh-my-opencode.json
|
||||
rm -f .opencode/oh-my-opencode.json .opencode/oh-my-opencode.jsonc
|
||||
```
|
||||
|
||||
3. **Verify removal**
|
||||
@@ -314,7 +314,7 @@ Highly opinionated, but adjustable to taste.
|
||||
See the full [Configuration Documentation](docs/configurations.md) for detailed information.
|
||||
|
||||
**Quick Overview:**
|
||||
- **Config Locations**: `.opencode/oh-my-opencode.json` (project) or `~/.config/opencode/oh-my-opencode.json` (user)
|
||||
- **Config Locations**: `.opencode/oh-my-opencode.jsonc` or `.opencode/oh-my-opencode.json` (project), `~/.config/opencode/oh-my-opencode.jsonc` or `~/.config/opencode/oh-my-opencode.json` (user)
|
||||
- **JSONC Support**: Comments and trailing commas supported
|
||||
- **Agents**: Override models, temperatures, prompts, and permissions for any agent
|
||||
- **Built-in Skills**: `playwright` (browser automation), `git-master` (atomic commits)
|
||||
@@ -378,5 +378,7 @@ I have no affiliation with any project or model mentioned here. This is purely p
|
||||
- Making Spray - influencer marketing solution, vovushop - crossborder commerce platform, vreview - ai commerce review marketing solution
|
||||
- [Google](https://google.com)
|
||||
- [Microsoft](https://microsoft.com)
|
||||
- [ELESTYLE](https://elestyle.jp)
|
||||
- Making elepay - multi-mobile payment gateway, OneQR - mobile application SaaS for cashless solutions
|
||||
|
||||
*Special thanks to [@junhoyeo](https://github.com/junhoyeo) for this amazing hero image.*
|
||||
|
||||
@@ -376,6 +376,8 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads
|
||||
- 制作 Spray - 网红营销解决方案、vovushop - 跨境电商平台、vreview - AI 电商评论营销解决方案
|
||||
- [Google](https://google.com)
|
||||
- [Microsoft](https://microsoft.com)
|
||||
- [ELESTYLE](https://elestyle.jp)
|
||||
- elepay - 多渠道移动支付网关、OneQR - 无现金解决方案移动应用 SaaS
|
||||
|
||||
## 赞助商
|
||||
- **Numman Ali** [GitHub](https://github.com/numman-ali) [X](https://x.com/nummanali)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
28
bun.lock
28
bun.lock
@@ -28,13 +28,13 @@
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.2.3",
|
||||
"oh-my-opencode-darwin-x64": "3.2.3",
|
||||
"oh-my-opencode-linux-arm64": "3.2.3",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.2.3",
|
||||
"oh-my-opencode-linux-x64": "3.2.3",
|
||||
"oh-my-opencode-linux-x64-musl": "3.2.3",
|
||||
"oh-my-opencode-windows-x64": "3.2.3",
|
||||
"oh-my-opencode-darwin-arm64": "3.5.3",
|
||||
"oh-my-opencode-darwin-x64": "3.5.3",
|
||||
"oh-my-opencode-linux-arm64": "3.5.3",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.5.3",
|
||||
"oh-my-opencode-linux-x64": "3.5.3",
|
||||
"oh-my-opencode-linux-x64-musl": "3.5.3",
|
||||
"oh-my-opencode-windows-x64": "3.5.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -226,19 +226,19 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.2.3", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Doc9xQCj5Jmx3PzouBIfvDwmfWM94Y9Q9IngFqOjrVpfBef9V/WIH0PlhJU6ps4BKGey8Nf2afFq3UE06Z63Hg=="],
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.3", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Dq0+PC2dyAqG7c3DUnQmdOkKbKmOsRHwoqgLCQNKN1lTRllF8zbWqp5B+LGKxSPxPqJIPS3mKt+wIR2KvkYJVw=="],
|
||||
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.2.3", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-w7lO0Hn/AlLCHe33KPbje83Js2h5weDWVMuopEs6d3pi/1zkRDBEhCi63S4J0d0EKod9kEPQA6ojtdVJ4J39zQ=="],
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.3", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Ke45Bv/ygZm3YUSUumIyk647KZ2PFzw30tH597cOpG8MDPGbNVBCM6EKFezcukUPT+gPFVpE1IiGzEkn4JmgZA=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.2.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-m1tS1jRLO2Svm5NuetK3BAgdAR8b2GkiIfMFoIYsLJTPmzIkXaigAYkFq+BXCs5JAbRmPmvjndz9cuCddnPADQ=="],
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aP5S3DngUhFkNeqYM33Ge6zccCWLzB/O3FLXLFXy/Iws03N8xugw72pnMK6lUbIia9QQBKK7IZBoYm9C79pZ3g=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.2.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Q/0AGtOuUFGNGIX8F6iD5W8c2spbjrqVBPt0B7laQSwnScKs/BI+TvM6HRE37vhoWg+fzhAX3QYJ2H9Un9FYrg=="],
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UiD/hVKYZQyX4D5N5SnZT4M5Z/B2SDtJWBW4MibpYSAcPKNCEBKi/5E4hOPxAtTfFGR8tIXFmYZdQJDkVfvluw=="],
|
||||
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.2.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-RIAyoj2XbT8vH++5fPUkdO+D1tfqxh+iWto7CqWr1TgbABbBJljGk91HJgS9xjnxyCQJEpFhTmO7NMHKJcZOWQ=="],
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-L9kqwzElGkaQ8pgtv1ZjcHARw9LPaU4UEVjzauByTMi+/5Js/PTsNXBggxSRzZfQ8/MNBPSCiA4K10Kc0YjjvA=="],
|
||||
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.2.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nnQK3y7R4DrBvqdqRGbujL2oAAQnVVb23JHUbJPQ6YxrRRGWpLOVGvK5c16ykSFEUPl8eZDmi1ON/R4opKLOUw=="],
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Z0fVVih/b2dbNeb9DK9oca5dNYCZyPySBRtxRhDXod5d7fJNgIPrvUoEd3SNfkRGORyFB3hGBZ6nqQ6N8+8DEA=="],
|
||||
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.2.3", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mt8E/TkpaCp04pvzwntT8x8TaqXDt3zCD5X2eA8ZZMrb5ofNr5HyG5G4SFXrUh+Ez3b/3YXpNWv6f6rnAlk1Dg=="],
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.3", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-ocWPjRs2sJgN02PJnEIYtqdMVDex1YhEj1FzAU5XIicfzQbgxLh9nz1yhHZzfqGJq69QStU6ofpc5kQpfX1LMg=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Instead of delegating everything to a single AI agent, it's far more efficient t
|
||||
- **Category**: "What kind of work is this?" (determines model, temperature, prompt mindset)
|
||||
- **Skill**: "What tools and knowledge are needed?" (injects specialized knowledge, MCP tools, workflows)
|
||||
|
||||
By combining these two concepts, you can generate optimal agents through `delegate_task`.
|
||||
By combining these two concepts, you can generate optimal agents through `task`.
|
||||
|
||||
---
|
||||
|
||||
@@ -32,10 +32,10 @@ A Category is an agent configuration preset optimized for specific domains.
|
||||
|
||||
### Usage
|
||||
|
||||
Specify the `category` parameter when invoking the `delegate_task` tool.
|
||||
Specify the `category` parameter when invoking the `task` tool.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
task(
|
||||
category="visual-engineering",
|
||||
prompt="Add a responsive chart component to the dashboard page"
|
||||
)
|
||||
@@ -74,7 +74,7 @@ A Skill is a mechanism that injects **specialized knowledge (Context)** and **to
|
||||
Add desired skill names to the `load_skills` array.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
task(
|
||||
category="quick",
|
||||
load_skills=["git-master"],
|
||||
prompt="Commit current changes. Follow commit message style."
|
||||
@@ -126,7 +126,7 @@ You can create powerful specialized agents by combining Categories and Skills.
|
||||
|
||||
---
|
||||
|
||||
## 5. delegate_task Prompt Guide
|
||||
## 5. task Prompt Guide
|
||||
|
||||
When delegating, **clear and specific** prompts are essential. Include these 7 elements:
|
||||
|
||||
@@ -158,7 +158,7 @@ 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. |
|
||||
| `description` | string | Human-readable description of the category's purpose. Shown in task prompt. |
|
||||
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-6`) |
|
||||
| `variant` | string | Model variant (e.g., `max`, `xhigh`) |
|
||||
| `temperature` | number | Creativity level (0.0 ~ 2.0). Lower is more deterministic. |
|
||||
|
||||
@@ -25,7 +25,7 @@ It asks about your providers (Claude, OpenAI, Gemini, etc.) and generates optima
|
||||
"explore": { "model": "opencode/gpt-5-nano" } // Free model for grep
|
||||
},
|
||||
|
||||
// Override category models (used by delegate_task)
|
||||
// Override category models (used by task)
|
||||
"categories": {
|
||||
"quick": { "model": "opencode/gpt-5-nano" }, // Fast/cheap for trivial tasks
|
||||
"visual-engineering": { "model": "google/gemini-3-pro" } // Gemini for UI
|
||||
@@ -38,13 +38,13 @@ It asks about your providers (Claude, OpenAI, Gemini, etc.) and generates optima
|
||||
## Config File Locations
|
||||
|
||||
Config file locations (priority order):
|
||||
1. `.opencode/oh-my-opencode.json` (project)
|
||||
2. User config (platform-specific):
|
||||
1. `.opencode/oh-my-opencode.jsonc` or `.opencode/oh-my-opencode.json` (project; prefers `.jsonc` when both exist)
|
||||
2. User config (platform-specific; prefers `.jsonc` when both exist):
|
||||
|
||||
| Platform | User Config Path |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| **Windows** | `~/.config/opencode/oh-my-opencode.json` (preferred) or `%APPDATA%\opencode\oh-my-opencode.json` (fallback) |
|
||||
| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.json` |
|
||||
| Platform | User Config Path |
|
||||
| --------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Windows** | `~/.config/opencode/oh-my-opencode.jsonc` (preferred) or `~/.config/opencode/oh-my-opencode.json` (fallback); `%APPDATA%\opencode\oh-my-opencode.jsonc` / `%APPDATA%\opencode\oh-my-opencode.json` (fallback) |
|
||||
| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.jsonc` (preferred) or `~/.config/opencode/oh-my-opencode.json` (fallback) |
|
||||
|
||||
Schema autocomplete supported:
|
||||
|
||||
@@ -83,7 +83,7 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
|
||||
## Google Auth
|
||||
|
||||
**Recommended**: For Google Gemini authentication, install the [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin (`@latest`). It provides multi-account load balancing, variant-based thinking levels, dual quota system (Antigravity + Gemini CLI), and active maintenance. See [Installation > Google Gemini](docs/guide/installation.md#google-gemini-antigravity-oauth).
|
||||
**Recommended**: For Google Gemini authentication, install the [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin (`@latest`). It provides multi-account load balancing, variant-based thinking levels, dual quota system (Antigravity + Gemini CLI), and active maintenance. See [Installation > Google Gemini](guide/installation.md#google-gemini-antigravity-oauth).
|
||||
|
||||
## Ollama Provider
|
||||
|
||||
@@ -252,7 +252,7 @@ Available agents: `sisyphus`, `prometheus`, `oracle`, `librarian`, `explore`, `m
|
||||
Oh My OpenCode includes built-in skills that provide additional capabilities:
|
||||
|
||||
- **playwright** (default) / **agent-browser**: Browser automation for web scraping, testing, screenshots, and browser interactions. See [Browser Automation](#browser-automation) for switching between providers.
|
||||
- **git-master**: Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with `delegate_task(category='quick', load_skills=['git-master'], ...)` to save context.
|
||||
- **git-master**: Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with `task(category='quick', load_skills=['git-master'], ...)` to save context.
|
||||
|
||||
Disable built-in skills via `disabled_skills` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
@@ -455,7 +455,7 @@ Run background subagents in separate tmux panes for **visual multi-agent executi
|
||||
### How It Works
|
||||
|
||||
When `tmux.enabled` is `true` and you're inside a tmux session:
|
||||
- Background agents (via `delegate_task(run_in_background=true)`) spawn in new tmux panes
|
||||
- Background agents (via `task(run_in_background=true)`) spawn in new tmux panes
|
||||
- Each pane shows the subagent's real-time output
|
||||
- Panes are automatically closed when the subagent completes
|
||||
- Layout is automatically adjusted based on your configuration
|
||||
@@ -716,7 +716,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
|
||||
## Categories
|
||||
|
||||
Categories enable domain-specific task delegation via the `delegate_task` tool. Each category applies runtime presets (model, temperature, prompt additions) when calling the `Sisyphus-Junior` agent.
|
||||
Categories enable domain-specific task delegation via the `task` tool. Each category applies runtime presets (model, temperature, prompt additions) when calling the `Sisyphus-Junior` agent.
|
||||
|
||||
### Built-in Categories
|
||||
|
||||
@@ -797,12 +797,12 @@ All 7 categories come with optimal model defaults, but **you must configure them
|
||||
### Usage
|
||||
|
||||
```javascript
|
||||
// Via delegate_task tool
|
||||
delegate_task(category="visual-engineering", prompt="Create a responsive dashboard component")
|
||||
delegate_task(category="ultrabrain", prompt="Design the payment processing flow")
|
||||
// Via task tool
|
||||
task(category="visual-engineering", prompt="Create a responsive dashboard component")
|
||||
task(category="ultrabrain", prompt="Design the payment processing flow")
|
||||
|
||||
// Or target a specific agent directly (bypasses categories)
|
||||
delegate_task(agent="oracle", prompt="Review this architecture")
|
||||
task(agent="oracle", prompt="Review this architecture")
|
||||
```
|
||||
|
||||
### Custom Categories
|
||||
@@ -831,7 +831,7 @@ Each category supports: `model`, `temperature`, `top_p`, `maxTokens`, `thinking`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------------ | ------- | ------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `description` | string | - | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
|
||||
| `description` | string | - | Human-readable description of the category's purpose. Shown in task prompt. |
|
||||
| `is_unstable_agent`| boolean | `false` | Mark agent as unstable - forces background mode for monitoring. Auto-enabled for gemini models. |
|
||||
|
||||
## Model Resolution System
|
||||
@@ -1061,9 +1061,10 @@ Don't want them? Disable via `disabled_mcps` in `~/.config/opencode/oh-my-openco
|
||||
|
||||
OpenCode provides LSP tools for analysis.
|
||||
Oh My OpenCode adds refactoring tools (rename, code actions).
|
||||
All OpenCode LSP configs and custom settings (from opencode.json) are supported, plus additional Oh My OpenCode-specific settings.
|
||||
All OpenCode LSP configs and custom settings (from `opencode.jsonc` / `opencode.json`) are supported, plus additional Oh My OpenCode-specific settings.
|
||||
For config discovery, `.jsonc` takes precedence over `.json` when both exist (applies to both `opencode.*` and `oh-my-opencode.*`).
|
||||
|
||||
Add LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
Add LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.jsonc` / `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.jsonc` / `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ Run agents in the background and continue working:
|
||||
|
||||
```
|
||||
# Launch in background
|
||||
delegate_task(subagent_type="explore", load_skills=[], prompt="Find auth implementations", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="Find auth implementations", run_in_background=true)
|
||||
|
||||
# Continue working...
|
||||
# System notifies on completion
|
||||
@@ -374,7 +374,7 @@ Hooks intercept and modify behavior at key points in the agent lifecycle.
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **task-resume-info** | PostToolUse | Provides task resume information for continuity. |
|
||||
| **delegate-task-retry** | PostToolUse | Retries failed delegate_task calls. |
|
||||
| **delegate-task-retry** | PostToolUse | Retries failed task calls. |
|
||||
|
||||
#### Integration
|
||||
|
||||
@@ -454,7 +454,7 @@ Disable specific hooks in config:
|
||||
| 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. |
|
||||
| **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 |
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ flowchart TB
|
||||
User -->|"/start-work"| Orchestrator
|
||||
Plan -->|"Read"| Orchestrator
|
||||
|
||||
Orchestrator -->|"delegate_task(category)"| Junior
|
||||
Orchestrator -->|"delegate_task(agent)"| Oracle
|
||||
Orchestrator -->|"delegate_task(agent)"| Explore
|
||||
Orchestrator -->|"delegate_task(agent)"| Librarian
|
||||
Orchestrator -->|"delegate_task(agent)"| Frontend
|
||||
Orchestrator -->|"task(category)"| Junior
|
||||
Orchestrator -->|"task(agent)"| Oracle
|
||||
Orchestrator -->|"task(agent)"| Explore
|
||||
Orchestrator -->|"task(agent)"| Librarian
|
||||
Orchestrator -->|"task(agent)"| Frontend
|
||||
|
||||
Junior -->|"Results + Learnings"| Orchestrator
|
||||
Oracle -->|"Advice"| Orchestrator
|
||||
@@ -220,9 +220,9 @@ Independent tasks run in parallel:
|
||||
```typescript
|
||||
// Orchestrator identifies parallelizable groups from plan
|
||||
// Group A: Tasks 2, 3, 4 (no file conflicts)
|
||||
delegate_task(category="ultrabrain", prompt="Task 2...")
|
||||
delegate_task(category="visual-engineering", prompt="Task 3...")
|
||||
delegate_task(category="general", prompt="Task 4...")
|
||||
task(category="ultrabrain", prompt="Task 2...")
|
||||
task(category="visual-engineering", prompt="Task 3...")
|
||||
task(category="general", prompt="Task 4...")
|
||||
// All run simultaneously
|
||||
```
|
||||
|
||||
@@ -234,7 +234,7 @@ delegate_task(category="general", prompt="Task 4...")
|
||||
|
||||
Junior is the **workhorse** that actually writes code. Key characteristics:
|
||||
|
||||
- **Focused**: Cannot delegate (blocked from task/delegate_task tools)
|
||||
- **Focused**: Cannot delegate (blocked from task tool)
|
||||
- **Disciplined**: Obsessive todo tracking
|
||||
- **Verified**: Must pass lsp_diagnostics before completion
|
||||
- **Constrained**: Cannot modify plan files (READ-ONLY)
|
||||
@@ -268,7 +268,7 @@ This "boulder pushing" mechanism is why the system is named after Sisyphus.
|
||||
|
||||
---
|
||||
|
||||
## The delegate_task Tool: Category + Skill System
|
||||
## The task Tool: Category + Skill System
|
||||
|
||||
### Why Categories are Revolutionary
|
||||
|
||||
@@ -276,17 +276,17 @@ This "boulder pushing" mechanism is why the system is named after Sisyphus.
|
||||
|
||||
```typescript
|
||||
// OLD: Model name creates distributional bias
|
||||
delegate_task(agent="gpt-5.2", prompt="...") // Model knows its limitations
|
||||
delegate_task(agent="claude-opus-4.6", prompt="...") // Different self-perception
|
||||
task(agent="gpt-5.2", prompt="...") // Model knows its limitations
|
||||
task(agent="claude-opus-4.6", prompt="...") // Different self-perception
|
||||
```
|
||||
|
||||
**The Solution: Semantic Categories:**
|
||||
|
||||
```typescript
|
||||
// NEW: Category describes INTENT, not implementation
|
||||
delegate_task(category="ultrabrain", prompt="...") // "Think strategically"
|
||||
delegate_task(category="visual-engineering", prompt="...") // "Design beautifully"
|
||||
delegate_task(category="quick", prompt="...") // "Just get it done fast"
|
||||
task(category="ultrabrain", prompt="...") // "Think strategically"
|
||||
task(category="visual-engineering", prompt="...") // "Design beautifully"
|
||||
task(category="quick", prompt="...") // "Just get it done fast"
|
||||
```
|
||||
|
||||
### Built-in Categories
|
||||
@@ -324,13 +324,13 @@ Skills prepend specialized instructions to subagent prompts:
|
||||
|
||||
```typescript
|
||||
// Category + Skill combination
|
||||
delegate_task(
|
||||
task(
|
||||
category="visual-engineering",
|
||||
load_skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
prompt="..."
|
||||
)
|
||||
|
||||
delegate_task(
|
||||
task(
|
||||
category="general",
|
||||
load_skills=["playwright"], // Adds browser automation expertise
|
||||
prompt="..."
|
||||
@@ -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, load_skills, prompt)
|
||||
Orchestrator->>Junior: task(category, load_skills, prompt)
|
||||
|
||||
Junior->>Junior: Create todos, execute
|
||||
Junior->>Junior: Verify (lsp_diagnostics, tests)
|
||||
|
||||
@@ -387,7 +387,7 @@ You can control related features in `oh-my-opencode.json`.
|
||||
|
||||
2. **Single Plan Principle**: No matter how large the task, contain all TODOs in one plan file (`.md`). This prevents context fragmentation.
|
||||
|
||||
3. **Active Delegation**: During execution, delegate to specialized agents via `delegate_task` rather than modifying code directly.
|
||||
3. **Active Delegation**: During execution, delegate to specialized agents via `task` rather than modifying code directly.
|
||||
|
||||
4. **Trust /start-work Continuity**: Don't worry about session interruptions. `/start-work` will always resume your work from boulder.json.
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ src/tools/delegate-task/constants.ts
|
||||
```
|
||||
Sisyphus (ULW mode)
|
||||
↓
|
||||
delegate_task(category="deep", ...)
|
||||
task(category="deep", ...)
|
||||
↓
|
||||
executor.ts: executeBackgroundContinuation()
|
||||
↓
|
||||
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "3.2.4",
|
||||
"version": "3.5.5",
|
||||
"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",
|
||||
@@ -74,13 +74,13 @@
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.2.4",
|
||||
"oh-my-opencode-darwin-x64": "3.2.4",
|
||||
"oh-my-opencode-linux-arm64": "3.2.4",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.2.4",
|
||||
"oh-my-opencode-linux-x64": "3.2.4",
|
||||
"oh-my-opencode-linux-x64-musl": "3.2.4",
|
||||
"oh-my-opencode-windows-x64": "3.2.4"
|
||||
"oh-my-opencode-darwin-arm64": "3.5.5",
|
||||
"oh-my-opencode-darwin-x64": "3.5.5",
|
||||
"oh-my-opencode-linux-arm64": "3.5.5",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.5.5",
|
||||
"oh-my-opencode-linux-x64": "3.5.5",
|
||||
"oh-my-opencode-linux-x64-musl": "3.5.5",
|
||||
"oh-my-opencode-windows-x64": "3.5.5"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.2.4",
|
||||
"version": "3.5.5",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64",
|
||||
"version": "3.2.4",
|
||||
"version": "3.5.5",
|
||||
"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.2.4",
|
||||
"version": "3.5.5",
|
||||
"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.2.4",
|
||||
"version": "3.5.5",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl",
|
||||
"version": "3.2.4",
|
||||
"version": "3.5.5",
|
||||
"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.2.4",
|
||||
"version": "3.5.5",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64",
|
||||
"version": "3.2.4",
|
||||
"version": "3.5.5",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
17
script/build-schema-document.ts
Normal file
17
script/build-schema-document.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as z from "zod"
|
||||
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
||||
|
||||
export function createOhMyOpenCodeJsonSchema(): Record<string, unknown> {
|
||||
const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, {
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
})
|
||||
|
||||
return {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
$id: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
title: "Oh My OpenCode Configuration",
|
||||
description: "Configuration schema for oh-my-opencode plugin",
|
||||
...jsonSchema,
|
||||
}
|
||||
}
|
||||
18
script/build-schema.test.ts
Normal file
18
script/build-schema.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createOhMyOpenCodeJsonSchema } from "./build-schema-document"
|
||||
|
||||
describe("build-schema-document", () => {
|
||||
test("generates schema with skills property", () => {
|
||||
// given
|
||||
const expectedDraft = "http://json-schema.org/draft-07/schema#"
|
||||
|
||||
// when
|
||||
const schema = createOhMyOpenCodeJsonSchema()
|
||||
|
||||
// then
|
||||
expect(schema.$schema).toBe(expectedDraft)
|
||||
expect(schema.title).toBe("Oh My OpenCode Configuration")
|
||||
expect(schema.properties).toBeDefined()
|
||||
expect(schema.properties.skills).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -1,24 +1,12 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as z from "zod"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
||||
import { createOhMyOpenCodeJsonSchema } from "./build-schema-document"
|
||||
|
||||
const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json"
|
||||
|
||||
async function main() {
|
||||
console.log("Generating JSON Schema...")
|
||||
|
||||
const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, {
|
||||
target: "draft7",
|
||||
})
|
||||
|
||||
const finalSchema = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
$id: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
title: "Oh My OpenCode Configuration",
|
||||
description: "Configuration schema for oh-my-opencode plugin",
|
||||
...jsonSchema,
|
||||
}
|
||||
const finalSchema = createOhMyOpenCodeJsonSchema()
|
||||
|
||||
await Bun.write(SCHEMA_OUTPUT_PATH, JSON.stringify(finalSchema, null, 2))
|
||||
|
||||
|
||||
@@ -1207,6 +1207,302 @@
|
||||
"created_at": "2026-02-06T06:23:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1541
|
||||
},
|
||||
{
|
||||
"name": "itsnebulalol",
|
||||
"id": 18669106,
|
||||
"comment_id": 3864672624,
|
||||
"created_at": "2026-02-07T15:10:54Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1622
|
||||
},
|
||||
{
|
||||
"name": "mkusaka",
|
||||
"id": 24956031,
|
||||
"comment_id": 3864822328,
|
||||
"created_at": "2026-02-07T16:54:36Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1629
|
||||
},
|
||||
{
|
||||
"name": "quantmind-br",
|
||||
"id": 170503374,
|
||||
"comment_id": 3865064441,
|
||||
"created_at": "2026-02-07T18:38:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1634
|
||||
},
|
||||
{
|
||||
"name": "QiRaining",
|
||||
"id": 13825001,
|
||||
"comment_id": 3865979224,
|
||||
"created_at": "2026-02-08T02:34:46Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1641
|
||||
},
|
||||
{
|
||||
"name": "JunyeongChoi0",
|
||||
"id": 99778164,
|
||||
"comment_id": 3867461224,
|
||||
"created_at": "2026-02-08T16:02:31Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1674
|
||||
},
|
||||
{
|
||||
"name": "aliozdenisik",
|
||||
"id": 106994209,
|
||||
"comment_id": 3867619266,
|
||||
"created_at": "2026-02-08T17:12:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1676
|
||||
},
|
||||
{
|
||||
"name": "mrm007",
|
||||
"id": 3297808,
|
||||
"comment_id": 3868350953,
|
||||
"created_at": "2026-02-08T21:41:35Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1680
|
||||
},
|
||||
{
|
||||
"name": "nianyi778",
|
||||
"id": 23355645,
|
||||
"comment_id": 3874840250,
|
||||
"created_at": "2026-02-10T01:41:08Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1703
|
||||
},
|
||||
{
|
||||
"name": "lxia1220",
|
||||
"id": 43934024,
|
||||
"comment_id": 3875675071,
|
||||
"created_at": "2026-02-10T06:43:35Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1713
|
||||
},
|
||||
{
|
||||
"name": "cyberprophet",
|
||||
"id": 48705422,
|
||||
"comment_id": 3877193956,
|
||||
"created_at": "2026-02-10T12:06:03Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1717
|
||||
},
|
||||
{
|
||||
"name": "materializerx",
|
||||
"id": 96932157,
|
||||
"comment_id": 3878329143,
|
||||
"created_at": "2026-02-10T15:07:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1724
|
||||
},
|
||||
{
|
||||
"name": "materializerx",
|
||||
"id": 96932157,
|
||||
"comment_id": 3878458939,
|
||||
"created_at": "2026-02-10T15:21:04Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1724
|
||||
},
|
||||
{
|
||||
"name": "RobertWsp",
|
||||
"id": 67512895,
|
||||
"comment_id": 3878518426,
|
||||
"created_at": "2026-02-10T15:27:01Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1723
|
||||
},
|
||||
{
|
||||
"name": "RobertWsp",
|
||||
"id": 67512895,
|
||||
"comment_id": 3878575833,
|
||||
"created_at": "2026-02-10T15:32:31Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1723
|
||||
},
|
||||
{
|
||||
"name": "sjawhar",
|
||||
"id": 5074378,
|
||||
"comment_id": 3879746658,
|
||||
"created_at": "2026-02-10T17:43:47Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1727
|
||||
},
|
||||
{
|
||||
"name": "marlon-costa-dc",
|
||||
"id": 128386606,
|
||||
"comment_id": 3879827362,
|
||||
"created_at": "2026-02-10T17:59:06Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1726
|
||||
},
|
||||
{
|
||||
"name": "marlon-costa-dc",
|
||||
"id": 128386606,
|
||||
"comment_id": 3879847814,
|
||||
"created_at": "2026-02-10T18:03:41Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1726
|
||||
},
|
||||
{
|
||||
"name": "danpung2",
|
||||
"id": 75434746,
|
||||
"comment_id": 3881834946,
|
||||
"created_at": "2026-02-11T02:52:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1741
|
||||
},
|
||||
{
|
||||
"name": "ojh102",
|
||||
"id": 14901903,
|
||||
"comment_id": 3882254163,
|
||||
"created_at": "2026-02-11T05:29:51Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1750
|
||||
},
|
||||
{
|
||||
"name": "uyu423",
|
||||
"id": 8033320,
|
||||
"comment_id": 3884127858,
|
||||
"created_at": "2026-02-11T12:30:37Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1762
|
||||
},
|
||||
{
|
||||
"name": "WietRob",
|
||||
"id": 203506602,
|
||||
"comment_id": 3859280254,
|
||||
"created_at": "2026-02-06T10:00:03Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1529
|
||||
},
|
||||
{
|
||||
"name": "COLDTURNIP",
|
||||
"id": 46220,
|
||||
"comment_id": 3884966424,
|
||||
"created_at": "2026-02-11T14:54:46Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1765
|
||||
},
|
||||
{
|
||||
"name": "tcarac",
|
||||
"id": 64477810,
|
||||
"comment_id": 3885026481,
|
||||
"created_at": "2026-02-11T15:03:25Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1766
|
||||
},
|
||||
{
|
||||
"name": "youngbinkim0",
|
||||
"id": 64558592,
|
||||
"comment_id": 3887466814,
|
||||
"created_at": "2026-02-11T22:03:00Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1777
|
||||
},
|
||||
{
|
||||
"name": "raki-1203",
|
||||
"id": 52475378,
|
||||
"comment_id": 3889111683,
|
||||
"created_at": "2026-02-12T07:27:39Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1790
|
||||
},
|
||||
{
|
||||
"name": "G36maid",
|
||||
"id": 53391375,
|
||||
"comment_id": 3889208379,
|
||||
"created_at": "2026-02-12T07:56:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1791
|
||||
},
|
||||
{
|
||||
"name": "solssak",
|
||||
"id": 107416133,
|
||||
"comment_id": 3889740003,
|
||||
"created_at": "2026-02-12T09:28:09Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1794
|
||||
},
|
||||
{
|
||||
"name": "bvanderhorn",
|
||||
"id": 9591412,
|
||||
"comment_id": 3890297580,
|
||||
"created_at": "2026-02-12T11:17:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1799
|
||||
},
|
||||
{
|
||||
"name": "jardo5",
|
||||
"id": 22041729,
|
||||
"comment_id": 3890810423,
|
||||
"created_at": "2026-02-12T12:57:06Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1802
|
||||
},
|
||||
{
|
||||
"name": "willy-scr",
|
||||
"id": 187001140,
|
||||
"comment_id": 3894534811,
|
||||
"created_at": "2026-02-13T02:56:20Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1809
|
||||
},
|
||||
{
|
||||
"name": "professional-ALFIE",
|
||||
"id": 219141081,
|
||||
"comment_id": 3897671676,
|
||||
"created_at": "2026-02-13T15:00:01Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1820
|
||||
},
|
||||
{
|
||||
"name": "Strocs",
|
||||
"id": 71996940,
|
||||
"comment_id": 3898248552,
|
||||
"created_at": "2026-02-13T16:56:54Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1822
|
||||
},
|
||||
{
|
||||
"name": "cloudwaddie-agent",
|
||||
"id": 261346076,
|
||||
"comment_id": 3900805128,
|
||||
"created_at": "2026-02-14T04:15:19Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1827
|
||||
},
|
||||
{
|
||||
"name": "morphaxl",
|
||||
"id": 57144942,
|
||||
"comment_id": 3872741516,
|
||||
"created_at": "2026-02-09T16:21:56Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1699
|
||||
},
|
||||
{
|
||||
"name": "morphaxl",
|
||||
"id": 57144942,
|
||||
"comment_id": 3872742242,
|
||||
"created_at": "2026-02-09T16:22:04Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1699
|
||||
},
|
||||
{
|
||||
"name": "liu-qingyuan",
|
||||
"id": 57737268,
|
||||
"comment_id": 3902402078,
|
||||
"created_at": "2026-02-14T19:39:58Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1844
|
||||
},
|
||||
{
|
||||
"name": "iyoda",
|
||||
"id": 31020,
|
||||
"comment_id": 3902426789,
|
||||
"created_at": "2026-02-14T19:58:19Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1845
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -212,7 +212,7 @@ Search **external references** (docs, OSS, web). Fire proactively when unfamilia
|
||||
- "Working with unfamiliar npm/pip/cargo packages"
|
||||
### Pre-Delegation Planning (MANDATORY)
|
||||
|
||||
**BEFORE every `delegate_task` call, EXPLICITLY declare your reasoning.**
|
||||
**BEFORE every `task` call, EXPLICITLY declare your reasoning.**
|
||||
|
||||
#### Step 1: Identify Task Requirements
|
||||
|
||||
@@ -236,7 +236,7 @@ Ask yourself:
|
||||
**MANDATORY FORMAT:**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
I will use task with:
|
||||
- **Category**: [selected-category-name]
|
||||
- **Why this category**: [how category description matches task domain]
|
||||
- **load_skills**: [list of selected skills]
|
||||
@@ -246,14 +246,14 @@ I will use delegate_task with:
|
||||
- **Expected Outcome**: [what success looks like]
|
||||
```
|
||||
|
||||
**Then** make the delegate_task call.
|
||||
**Then** make the task call.
|
||||
|
||||
#### Examples
|
||||
|
||||
**CORRECT: Full Evaluation**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
I will use 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"]
|
||||
@@ -263,9 +263,11 @@ I will use delegate_task with:
|
||||
- skill-c: OMITTED - description says "[quote]" which doesn't apply because [reason]
|
||||
- **Expected Outcome**: [concrete deliverable]
|
||||
|
||||
delegate_task(
|
||||
task(
|
||||
category="[category-name]",
|
||||
load_skills=["skill-a", "skill-b"],
|
||||
description="[short task description]",
|
||||
run_in_background=false,
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -273,14 +275,16 @@ delegate_task(
|
||||
**CORRECT: Agent-Specific (for exploration/consultation)**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
I will use 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(
|
||||
task(
|
||||
subagent_type="[agent-name]",
|
||||
description="[short task description]",
|
||||
run_in_background=false,
|
||||
load_skills=[],
|
||||
prompt="..."
|
||||
)
|
||||
@@ -289,14 +293,15 @@ delegate_task(
|
||||
**CORRECT: Background Exploration**
|
||||
|
||||
```
|
||||
I will use delegate_task with:
|
||||
I will use 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(
|
||||
task(
|
||||
subagent_type="explore",
|
||||
description="Find auth implementations",
|
||||
run_in_background=true,
|
||||
load_skills=[],
|
||||
prompt="Find all authentication implementations in the codebase"
|
||||
@@ -306,7 +311,7 @@ delegate_task(
|
||||
**WRONG: No Skill Evaluation**
|
||||
|
||||
```
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Where's the justification?
|
||||
task(category="...", load_skills=[], prompt="...") // Where's the justification?
|
||||
```
|
||||
|
||||
**WRONG: Vague Category Selection**
|
||||
@@ -317,7 +322,7 @@ I'll use this category because it seems right.
|
||||
|
||||
#### Enforcement
|
||||
|
||||
**BLOCKING VIOLATION**: If you call `delegate_task` without:
|
||||
**BLOCKING VIOLATION**: If you call `task` without:
|
||||
1. Explaining WHY category was selected (based on description)
|
||||
2. Evaluating EACH available skill for relevance
|
||||
|
||||
@@ -329,15 +334,15 @@ I'll use this category because it seems right.
|
||||
```typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, 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...")
|
||||
task(subagent_type="explore", description="Find auth implementations", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
task(subagent_type="explore", description="Find error handling patterns", 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...")
|
||||
task(subagent_type="librarian", description="Find JWT best practices", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
task(subagent_type="librarian", description="Find Express auth patterns", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
result = delegate_task(...) // Never wait synchronously for explore/librarian
|
||||
result = task(...) // Never wait synchronously for explore/librarian
|
||||
```
|
||||
|
||||
### Background Result Collection:
|
||||
@@ -347,16 +352,16 @@ result = delegate_task(...) // Never wait synchronously for explore/librarian
|
||||
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.
|
||||
Pass `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!)
|
||||
**ALWAYS use session_id when:**
|
||||
- Previous task failed → `session_id="ses_xxx", prompt="fix: [specific error]"`
|
||||
- Need follow-up on result → `session_id="ses_xxx", prompt="also check [additional query]"`
|
||||
- Multi-turn with same agent → session_id instead of new task (saves tokens!)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
delegate_task(resume="ses_abc123", prompt="The previous search missed X. Also look for Y.")
|
||||
task(session_id="ses_abc123", description="Follow-up search", run_in_background=false, load_skills=[], prompt="The previous search missed X. Also look for Y.")
|
||||
```
|
||||
|
||||
### Search Stop Conditions
|
||||
@@ -377,7 +382,7 @@ STOP searching when:
|
||||
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.**
|
||||
**task() combines categories and skills for optimal task execution.**
|
||||
|
||||
#### Available Categories (Domain-Optimized Models)
|
||||
|
||||
@@ -442,7 +447,7 @@ SKILL EVALUATION for "[skill-name]":
|
||||
### Delegation Pattern
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
task(
|
||||
category="[selected-category]",
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
prompt="..."
|
||||
@@ -451,7 +456,7 @@ delegate_task(
|
||||
|
||||
**ANTI-PATTERN (will produce poor results):**
|
||||
```typescript
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Empty load_skills without justification
|
||||
task(category="...", load_skills=[], prompt="...") // Empty load_skills without justification
|
||||
```
|
||||
### Delegation Table:
|
||||
|
||||
|
||||
80
src/AGENTS.md
Normal file
80
src/AGENTS.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# SRC KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Main plugin entry point and orchestration layer. Plugin initialization, hook registration, tool composition, and lifecycle management.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
src/
|
||||
├── index.ts # Main plugin entry (88 lines) — OhMyOpenCodePlugin factory
|
||||
├── create-hooks.ts # Hook coordination: core, continuation, skill (62 lines)
|
||||
├── create-managers.ts # Manager initialization: Tmux, Background, SkillMcp, Config (80 lines)
|
||||
├── create-tools.ts # Tool registry + skill context composition (54 lines)
|
||||
├── plugin-interface.ts # Plugin interface assembly — 7 OpenCode hooks (66 lines)
|
||||
├── plugin-config.ts # Config loading orchestration (user + project merge)
|
||||
├── plugin-state.ts # Model cache state (context limits, anthropic 1M flag)
|
||||
├── agents/ # 11 AI agents (32 files) - see agents/AGENTS.md
|
||||
├── cli/ # CLI installer, doctor (107+ files) - see cli/AGENTS.md
|
||||
├── config/ # Zod schema (21 component files) - see config/AGENTS.md
|
||||
├── features/ # Background agents, skills, commands (18 dirs) - see features/AGENTS.md
|
||||
├── hooks/ # 41 lifecycle hooks (36 dirs) - see hooks/AGENTS.md
|
||||
├── mcp/ # Built-in MCPs (6 files) - see mcp/AGENTS.md
|
||||
├── plugin/ # Plugin interface composition (21 files)
|
||||
├── plugin-handlers/ # Config loading, plan inheritance (15 files) - see plugin-handlers/AGENTS.md
|
||||
├── shared/ # Cross-cutting utilities (84 files) - see shared/AGENTS.md
|
||||
└── tools/ # 25+ tools (14 dirs) - see tools/AGENTS.md
|
||||
```
|
||||
|
||||
## PLUGIN INITIALIZATION (10 steps)
|
||||
|
||||
1. `injectServerAuthIntoClient(ctx.client)` — Auth injection
|
||||
2. `startTmuxCheck()` — Tmux availability
|
||||
3. `loadPluginConfig(ctx.directory, ctx)` — User + project config merge → Zod validation
|
||||
4. `createFirstMessageVariantGate()` — First message variant override gate
|
||||
5. `createModelCacheState()` — Model context limits cache
|
||||
6. `createManagers(...)` → 4 managers:
|
||||
- `TmuxSessionManager` — Multi-pane tmux sessions
|
||||
- `BackgroundManager` — Parallel subagent execution
|
||||
- `SkillMcpManager` — MCP server lifecycle
|
||||
- `ConfigHandler` — Plugin config API to OpenCode
|
||||
7. `createTools(...)` → `createSkillContext()` + `createAvailableCategories()` + `createToolRegistry()`
|
||||
8. `createHooks(...)` → `createCoreHooks()` + `createContinuationHooks()` + `createSkillHooks()`
|
||||
9. `createPluginInterface(...)` → 7 OpenCode hook handlers
|
||||
10. Return plugin with `experimental.session.compacting`
|
||||
|
||||
## HOOK REGISTRATION (3 tiers)
|
||||
|
||||
**Core Hooks** (`create-core-hooks.ts`):
|
||||
- Session (20): context-window-monitor, session-recovery, think-mode, ralph-loop, anthropic-effort, ...
|
||||
- Tool Guard (8): comment-checker, tool-output-truncator, rules-injector, write-existing-file-guard, ...
|
||||
- Transform (4): claude-code-hooks, keyword-detector, context-injector, thinking-block-validator
|
||||
|
||||
**Continuation Hooks** (`create-continuation-hooks.ts`):
|
||||
- 7 hooks: stop-continuation-guard, compaction-context-injector, todo-continuation-enforcer, atlas, ...
|
||||
|
||||
**Skill Hooks** (`create-skill-hooks.ts`):
|
||||
- 2 hooks: category-skill-reminder, auto-slash-command
|
||||
|
||||
## PLUGIN INTERFACE (7 OpenCode handlers)
|
||||
|
||||
| Handler | Source | Purpose |
|
||||
|---------|--------|---------|
|
||||
| `tool` | filteredTools | All registered tools |
|
||||
| `chat.params` | createChatParamsHandler | Anthropic effort level |
|
||||
| `chat.message` | createChatMessageHandler | First message variant, session setup |
|
||||
| `experimental.chat.messages.transform` | createMessagesTransformHandler | Context injection, keyword detection |
|
||||
| `config` | configHandler | Agent/MCP/command registration |
|
||||
| `event` | createEventHandler | Session lifecycle |
|
||||
| `tool.execute.before` | createToolExecuteBeforeHandler | Pre-tool hooks |
|
||||
| `tool.execute.after` | createToolExecuteAfterHandler | Post-tool hooks |
|
||||
|
||||
## SAFE HOOK CREATION PATTERN
|
||||
|
||||
```typescript
|
||||
const hook = isHookEnabled("hook-name")
|
||||
? safeCreateHook("hook-name", () => createHookFactory(ctx), { enabled: safeHookEnabled })
|
||||
: null;
|
||||
```
|
||||
|
||||
All hooks use this pattern for graceful degradation on failure.
|
||||
@@ -2,88 +2,99 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
11 AI agents for multi-model orchestration. Each agent has factory function + metadata + fallback chains.
|
||||
|
||||
**Primary Agents** (respect UI model selection):
|
||||
- Sisyphus, Atlas, Prometheus
|
||||
|
||||
**Subagents** (use own fallback chains):
|
||||
- Hephaestus, Oracle, Librarian, Explore, Multimodal-Looker, Metis, Momus, Sisyphus-Junior
|
||||
11 AI agents with factory functions, fallback chains, and model-specific prompt variants. Each agent has metadata (category, cost, triggers) and configurable tool restrictions.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
agents/
|
||||
├── atlas/ # Master Orchestrator (holds todo list)
|
||||
│ ├── index.ts
|
||||
│ ├── default.ts # Claude-optimized prompt (390 lines)
|
||||
│ ├── gpt.ts # GPT-optimized prompt (330 lines)
|
||||
├── sisyphus.ts # Main orchestrator (530 lines)
|
||||
├── hephaestus.ts # Autonomous deep worker (624 lines)
|
||||
├── oracle.ts # Strategic advisor (170 lines)
|
||||
├── librarian.ts # Multi-repo research (328 lines)
|
||||
├── explore.ts # Fast codebase grep (124 lines)
|
||||
├── multimodal-looker.ts # Media analyzer (58 lines)
|
||||
├── metis.ts # Pre-planning analysis (347 lines)
|
||||
├── momus.ts # Plan validator (244 lines)
|
||||
├── atlas/ # Master orchestrator
|
||||
│ ├── agent.ts # Atlas factory
|
||||
│ ├── default.ts # Claude-optimized prompt
|
||||
│ ├── gpt.ts # GPT-optimized prompt
|
||||
│ └── utils.ts
|
||||
├── prometheus/ # Planning Agent (Interview/Consultant mode)
|
||||
├── prometheus/ # Planning agent
|
||||
│ ├── index.ts
|
||||
│ ├── system-prompt.ts # 6-section prompt assembly
|
||||
│ ├── plan-template.ts # Work plan structure (423 lines)
|
||||
│ ├── interview-mode.ts # Interview flow (335 lines)
|
||||
│ ├── plan-generation.ts
|
||||
│ ├── high-accuracy-mode.ts
|
||||
│ ├── identity-constraints.ts # Identity rules (301 lines)
|
||||
│ └── behavioral-summary.ts
|
||||
├── sisyphus-junior/ # Delegated task executor (category-spawned)
|
||||
│ ├── index.ts
|
||||
│ ├── default.ts
|
||||
│ └── gpt.ts
|
||||
├── sisyphus.ts # Main orchestrator prompt (530 lines)
|
||||
├── hephaestus.ts # Autonomous deep worker (618 lines, GPT 5.3 Codex)
|
||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||
├── librarian.ts # Multi-repo research (328 lines)
|
||||
├── explore.ts # Fast contextual grep
|
||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||
├── metis.ts # Pre-planning analysis (347 lines)
|
||||
├── momus.ts # Plan reviewer
|
||||
├── sisyphus-junior/ # Delegated task executor
|
||||
│ ├── agent.ts
|
||||
│ ├── default.ts # Claude prompt
|
||||
│ └── gpt.ts # GPT prompt
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation (431 lines)
|
||||
├── builtin-agents/ # Agent registry (8 files)
|
||||
├── utils.ts # Agent creation, model fallback resolution (571 lines)
|
||||
├── types.ts # AgentModelConfig, AgentPromptMetadata
|
||||
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback() (485 lines)
|
||||
└── index.ts # builtinAgents export
|
||||
└── index.ts # Exports
|
||||
```
|
||||
|
||||
## AGENT MODELS
|
||||
| Agent | Model | Temp | Purpose |
|
||||
|-------|-------|------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-6 | 0.1 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.3-codex → gemini-3-pro) |
|
||||
| Hephaestus | openai/gpt-5.3-codex | 0.1 | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.3-codex, no fallback) |
|
||||
| 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 | xai/grok-code-fast-1 | 0.1 | Fast contextual grep (fallback: claude-haiku-4-5 → gpt-5-mini → gpt-5-nano) |
|
||||
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-6 | 0.1 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Metis | anthropic/claude-opus-4-6 | 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-6) |
|
||||
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
|
||||
|
||||
## HOW TO ADD
|
||||
1. Create `src/agents/my-agent.ts` exporting factory + metadata.
|
||||
2. Add to `agentSources` in `src/agents/utils.ts`.
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`.
|
||||
4. Register in `src/index.ts` initialization.
|
||||
| Agent | Model | Temp | Fallback Chain | Cost |
|
||||
|-------|-------|------|----------------|------|
|
||||
| Sisyphus | claude-opus-4-6 | 0.1 | kimi-k2.5 → glm-4.7 → gpt-5.3-codex → gemini-3-pro | EXPENSIVE |
|
||||
| Hephaestus | gpt-5.3-codex | 0.1 | NONE (required) | EXPENSIVE |
|
||||
| Atlas | claude-sonnet-4-5 | 0.1 | kimi-k2.5 → gpt-5.2 | EXPENSIVE |
|
||||
| Prometheus | claude-opus-4-6 | 0.1 | kimi-k2.5 → gpt-5.2 | EXPENSIVE |
|
||||
| oracle | gpt-5.2 | 0.1 | claude-opus-4-6 | EXPENSIVE |
|
||||
| librarian | glm-4.7 | 0.1 | glm-4.7-free | CHEAP |
|
||||
| explore | grok-code-fast-1 | 0.1 | claude-haiku-4-5 → gpt-5-mini → gpt-5-nano | FREE |
|
||||
| multimodal-looker | gemini-3-flash | 0.1 | NONE | CHEAP |
|
||||
| Metis | claude-opus-4-6 | 0.3 | kimi-k2.5 → gpt-5.2 | EXPENSIVE |
|
||||
| Momus | gpt-5.2 | 0.1 | claude-opus-4-6 | EXPENSIVE |
|
||||
| Sisyphus-Junior | claude-sonnet-4-5 | 0.1 | (user-configurable) | EXPENSIVE |
|
||||
|
||||
## 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 only |
|
||||
| Sisyphus-Junior | task, delegate_task |
|
||||
| Atlas | task, call_omo_agent |
|
||||
|
||||
## PATTERNS
|
||||
- **Factory**: `createXXXAgent(model: string): AgentConfig`
|
||||
| Agent | Denied | Allowed |
|
||||
|-------|--------|---------|
|
||||
| oracle | write, edit, task, call_omo_agent | Read-only consultation |
|
||||
| librarian | write, edit, task, call_omo_agent | Research tools only |
|
||||
| explore | write, edit, task, call_omo_agent | Search tools only |
|
||||
| multimodal-looker | ALL except `read` | Vision-only |
|
||||
| Sisyphus-Junior | task | No delegation |
|
||||
| Atlas | task, call_omo_agent | Orchestration only |
|
||||
|
||||
## THINKING / REASONING
|
||||
|
||||
| Agent | Claude | GPT |
|
||||
|-------|--------|-----|
|
||||
| Sisyphus | 32k budget tokens | reasoningEffort: "medium" |
|
||||
| Hephaestus | — | reasoningEffort: "medium" |
|
||||
| Oracle | 32k budget tokens | reasoningEffort: "medium" |
|
||||
| Metis | 32k budget tokens | — |
|
||||
| Momus | 32k budget tokens | reasoningEffort: "medium" |
|
||||
| Sisyphus-Junior | 32k budget tokens | reasoningEffort: "medium" |
|
||||
|
||||
## HOW TO ADD
|
||||
|
||||
1. Create `src/agents/my-agent.ts` exporting factory + metadata
|
||||
2. Add to `agentSources` in `src/agents/builtin-agents/`
|
||||
3. Update `AgentNameSchema` in `src/config/schema/agent-names.ts`
|
||||
4. Register in `src/plugin-handlers/agent-config-handler.ts`
|
||||
|
||||
## KEY PATTERNS
|
||||
|
||||
- **Factory**: `createXXXAgent(model): 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
|
||||
- **Model-specific routing**: Atlas, Sisyphus-Junior have GPT vs Claude prompt variants
|
||||
- **Model-specific prompts**: Atlas, Sisyphus-Junior have GPT vs Claude variants
|
||||
- **Dynamic prompts**: Sisyphus, Hephaestus use `dynamic-agent-prompt-builder.ts` to inject available tools/skills/categories
|
||||
|
||||
## ANTI-PATTERNS
|
||||
- **Trust reports**: NEVER trust "I'm done" - verify outputs
|
||||
- **High temp**: Don't use >0.3 for code agents
|
||||
- **Sequential calls**: Use `delegate_task` with `run_in_background` for exploration
|
||||
- **Prometheus writing code**: Planner only - never implements
|
||||
|
||||
- **Trust agent self-reports**: NEVER — always verify outputs
|
||||
- **High temperature**: Don't use >0.3 for code agents
|
||||
- **Sequential calls**: Use `task` with `run_in_background` for exploration
|
||||
- **Prometheus writing code**: Planner only — never implements
|
||||
|
||||
50
src/agents/agent-builder.ts
Normal file
50
src/agents/agent-builder.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentFactory } from "./types"
|
||||
import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
|
||||
import type { BrowserAutomationProvider } from "../config/schema"
|
||||
import { mergeCategories } from "../shared/merge-categories"
|
||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||
|
||||
export type AgentSource = AgentFactory | AgentConfig
|
||||
|
||||
export function isFactory(source: AgentSource): source is AgentFactory {
|
||||
return typeof source === "function"
|
||||
}
|
||||
|
||||
export function buildAgent(
|
||||
source: AgentSource,
|
||||
model: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
disabledSkills?: Set<string>
|
||||
): AgentConfig {
|
||||
const base = isFactory(source) ? source(model) : { ...source }
|
||||
const categoryConfigs: Record<string, CategoryConfig> = mergeCategories(categories)
|
||||
|
||||
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
|
||||
if (agentWithCategory.category) {
|
||||
const categoryConfig = categoryConfigs[agentWithCategory.category]
|
||||
if (categoryConfig) {
|
||||
if (!base.model) {
|
||||
base.model = categoryConfig.model
|
||||
}
|
||||
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
||||
base.temperature = categoryConfig.temperature
|
||||
}
|
||||
if (base.variant === undefined && categoryConfig.variant !== undefined) {
|
||||
base.variant = categoryConfig.variant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (agentWithCategory.skills?.length) {
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider, disabledSkills })
|
||||
if (resolved.size > 0) {
|
||||
const skillContent = Array.from(resolved.values()).join("\n\n")
|
||||
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
142
src/agents/atlas/agent.ts
Normal file
142
src/agents/atlas/agent.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Atlas - Master Orchestrator Agent
|
||||
*
|
||||
* Orchestrates work via task() to complete ALL tasks in a todo list until fully done.
|
||||
* You are the conductor of a symphony of specialized agents.
|
||||
*
|
||||
* Routing:
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) → gpt.ts (GPT-5.2 optimized)
|
||||
* 2. Default (Claude, etc.) → default.ts (Claude-optimized)
|
||||
*/
|
||||
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode, AgentPromptMetadata } from "../types"
|
||||
import { isGptModel } from "../types"
|
||||
import type { AvailableAgent, AvailableSkill, AvailableCategory } from "../dynamic-agent-prompt-builder"
|
||||
import { buildCategorySkillsDelegationGuide } from "../dynamic-agent-prompt-builder"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { mergeCategories } from "../../shared/merge-categories"
|
||||
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
||||
|
||||
import { getDefaultAtlasPrompt } from "./default"
|
||||
import { getGptAtlasPrompt } from "./gpt"
|
||||
import {
|
||||
getCategoryDescription,
|
||||
buildAgentSelectionSection,
|
||||
buildCategorySection,
|
||||
buildSkillsSection,
|
||||
buildDecisionMatrix,
|
||||
} from "./prompt-section-builder"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
|
||||
export type AtlasPromptSource = "default" | "gpt"
|
||||
|
||||
/**
|
||||
* Determines which Atlas prompt to use based on model.
|
||||
*/
|
||||
export function getAtlasPromptSource(model?: string): AtlasPromptSource {
|
||||
if (model && isGptModel(model)) {
|
||||
return "gpt"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
export interface OrchestratorContext {
|
||||
model?: string
|
||||
availableAgents?: AvailableAgent[]
|
||||
availableSkills?: AvailableSkill[]
|
||||
userCategories?: Record<string, CategoryConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate Atlas prompt based on model.
|
||||
*/
|
||||
export function getAtlasPrompt(model?: string): string {
|
||||
const source = getAtlasPromptSource(model)
|
||||
|
||||
switch (source) {
|
||||
case "gpt":
|
||||
return getGptAtlasPrompt()
|
||||
case "default":
|
||||
default:
|
||||
return getDefaultAtlasPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
|
||||
const agents = ctx?.availableAgents ?? []
|
||||
const skills = ctx?.availableSkills ?? []
|
||||
const userCategories = ctx?.userCategories
|
||||
const model = ctx?.model
|
||||
|
||||
const allCategories = mergeCategories(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)
|
||||
|
||||
const basePrompt = getAtlasPrompt(model)
|
||||
|
||||
return basePrompt
|
||||
.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",
|
||||
])
|
||||
|
||||
const baseConfig = {
|
||||
description:
|
||||
"Orchestrates work via 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),
|
||||
color: "#10B981",
|
||||
...restrictions,
|
||||
}
|
||||
|
||||
return baseConfig 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",
|
||||
}
|
||||
@@ -19,18 +19,18 @@ 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.
|
||||
Complete ALL tasks in a work plan via \`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):
|
||||
Use \`task()\` with EITHER category OR agent (mutually exclusive):
|
||||
|
||||
\`\`\`typescript
|
||||
// Option A: Category + Skills (spawns Sisyphus-Junior with domain config)
|
||||
delegate_task(
|
||||
task(
|
||||
category="[category-name]",
|
||||
load_skills=["skill-1", "skill-2"],
|
||||
run_in_background=false,
|
||||
@@ -38,7 +38,7 @@ delegate_task(
|
||||
)
|
||||
|
||||
// Option B: Specialized Agent (for specific expert tasks)
|
||||
delegate_task(
|
||||
task(
|
||||
subagent_type="[agent-name]",
|
||||
load_skills=[],
|
||||
run_in_background=false,
|
||||
@@ -58,7 +58,7 @@ delegate_task(
|
||||
|
||||
## 6-Section Prompt Structure (MANDATORY)
|
||||
|
||||
Every \`delegate_task()\` prompt MUST include ALL 6 sections:
|
||||
Every \`task()\` prompt MUST include ALL 6 sections:
|
||||
|
||||
\`\`\`markdown
|
||||
## 1. TASK
|
||||
@@ -149,7 +149,7 @@ Structure:
|
||||
### 3.1 Check Parallelization
|
||||
If tasks can run in parallel:
|
||||
- Prepare prompts for ALL parallelizable tasks
|
||||
- Invoke multiple \`delegate_task()\` in ONE message
|
||||
- Invoke multiple \`task()\` in ONE message
|
||||
- Wait for all to complete
|
||||
- Verify all, then continue
|
||||
|
||||
@@ -167,10 +167,10 @@ Read(".sisyphus/notepads/{plan-name}/issues.md")
|
||||
|
||||
Extract wisdom and include in prompt.
|
||||
|
||||
### 3.3 Invoke delegate_task()
|
||||
### 3.3 Invoke task()
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
task(
|
||||
category="[category]",
|
||||
load_skills=["[relevant-skills]"],
|
||||
run_in_background=false,
|
||||
@@ -178,39 +178,59 @@ delegate_task(
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
### 3.4 Verify (PROJECT-LEVEL QA)
|
||||
### 3.4 Verify (MANDATORY — EVERY SINGLE DELEGATION)
|
||||
|
||||
**After EVERY delegation, YOU must verify:**
|
||||
**You are the QA gate. Subagents lie. Automated checks alone are NOT enough.**
|
||||
|
||||
1. **Project-level diagnostics**:
|
||||
\`lsp_diagnostics(filePath="src/")\` or \`lsp_diagnostics(filePath=".")\`
|
||||
MUST return ZERO errors
|
||||
After EVERY delegation, complete ALL of these steps — no shortcuts:
|
||||
|
||||
2. **Build verification**:
|
||||
\`bun run build\` or \`bun run typecheck\`
|
||||
Exit code MUST be 0
|
||||
#### A. Automated Verification
|
||||
1. \`lsp_diagnostics(filePath=".")\` → ZERO errors at project level
|
||||
2. \`bun run build\` or \`bun run typecheck\` → exit code 0
|
||||
3. \`bun test\` → ALL tests pass
|
||||
|
||||
3. **Test verification**:
|
||||
\`bun test\`
|
||||
ALL tests MUST pass
|
||||
#### B. Manual Code Review (NON-NEGOTIABLE — DO NOT SKIP)
|
||||
|
||||
4. **Manual inspection**:
|
||||
- Read changed files
|
||||
- Confirm changes match requirements
|
||||
- Check for regressions
|
||||
**This is the step you are most tempted to skip. DO NOT SKIP IT.**
|
||||
|
||||
**Checklist:**
|
||||
1. \`Read\` EVERY file the subagent created or modified — no exceptions
|
||||
2. For EACH file, check line by line:
|
||||
- Does the logic actually implement the task requirement?
|
||||
- Are there stubs, TODOs, placeholders, or hardcoded values?
|
||||
- Are there logic errors or missing edge cases?
|
||||
- Does it follow the existing codebase patterns?
|
||||
- Are imports correct and complete?
|
||||
3. Cross-reference: compare what subagent CLAIMED vs what the code ACTUALLY does
|
||||
4. If anything doesn't match → resume session and fix immediately
|
||||
|
||||
**If you cannot explain what the changed code does, you have not reviewed it.**
|
||||
|
||||
#### C. Hands-On QA (if applicable)
|
||||
| Deliverable | Method | Tool |
|
||||
|-------------|--------|------|
|
||||
| Frontend/UI | Browser | \`/playwright\` |
|
||||
| TUI/CLI | Interactive | \`interactive_bash\` |
|
||||
| API/Backend | Real requests | curl |
|
||||
|
||||
#### D. Check Boulder State Directly
|
||||
|
||||
After verification, READ the plan file directly — every time, no exceptions:
|
||||
\`\`\`
|
||||
[ ] lsp_diagnostics at project level - ZERO errors
|
||||
[ ] Build command - exit 0
|
||||
[ ] Test suite - all pass
|
||||
[ ] Files exist and match requirements
|
||||
[ ] No regressions
|
||||
Read(".sisyphus/tasks/{plan-name}.yaml")
|
||||
\`\`\`
|
||||
Count remaining \`- [ ]\` tasks. This is your ground truth for what comes next.
|
||||
|
||||
**Checklist (ALL must be checked):**
|
||||
\`\`\`
|
||||
[ ] Automated: lsp_diagnostics clean, build passes, tests pass
|
||||
[ ] Manual: Read EVERY changed file, verified logic matches requirements
|
||||
[ ] Cross-check: Subagent claims match actual code
|
||||
[ ] Boulder: Read plan file, confirmed current progress
|
||||
\`\`\`
|
||||
|
||||
**If verification fails**: Resume the SAME session with the ACTUAL error output:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
task(
|
||||
session_id="ses_xyz789", // ALWAYS use the session from the failed task
|
||||
load_skills=[...],
|
||||
prompt="Verification failed: {actual error}. Fix."
|
||||
@@ -221,13 +241,13 @@ delegate_task(
|
||||
|
||||
**CRITICAL: When re-delegating, ALWAYS use \`session_id\` parameter.**
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. STORE IT.
|
||||
Every \`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(
|
||||
task(
|
||||
session_id="ses_xyz789", // Session from failed task
|
||||
load_skills=[...],
|
||||
prompt="FAILED: {error}. Fix by: {specific instruction}"
|
||||
@@ -274,21 +294,21 @@ ACCUMULATED WISDOM:
|
||||
|
||||
**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, ...)
|
||||
task(subagent_type="explore", load_skills=[], run_in_background=true, ...)
|
||||
task(subagent_type="librarian", load_skills=[], run_in_background=true, ...)
|
||||
\`\`\`
|
||||
|
||||
**For task execution**: NEVER background
|
||||
\`\`\`typescript
|
||||
delegate_task(category="...", run_in_background=false, ...)
|
||||
task(category="...", load_skills=[...], 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", load_skills=[], run_in_background=false, prompt="Task 2...")
|
||||
delegate_task(category="quick", load_skills=[], run_in_background=false, prompt="Task 3...")
|
||||
delegate_task(category="quick", load_skills=[], run_in_background=false, prompt="Task 4...")
|
||||
task(category="quick", load_skills=[], run_in_background=false, prompt="Task 2...")
|
||||
task(category="quick", load_skills=[], run_in_background=false, prompt="Task 3...")
|
||||
task(category="quick", load_skills=[], run_in_background=false, prompt="Task 4...")
|
||||
\`\`\`
|
||||
|
||||
**Background management**:
|
||||
@@ -325,22 +345,25 @@ delegate_task(category="quick", load_skills=[], run_in_background=false, prompt=
|
||||
|
||||
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
|
||||
**After each delegation — BOTH automated AND manual verification are MANDATORY:**
|
||||
|
||||
1. \`lsp_diagnostics\` at PROJECT level → ZERO errors
|
||||
2. Run build command → exit 0
|
||||
3. Run test suite → ALL pass
|
||||
4. **\`Read\` EVERY changed file line by line** → logic matches requirements
|
||||
5. **Cross-check**: subagent's claims vs actual code — do they match?
|
||||
6. **Check boulder state**: Read the plan file directly, count remaining tasks
|
||||
|
||||
**Evidence required**:
|
||||
| Action | Evidence |
|
||||
|--------|----------|
|
||||
| Code change | lsp_diagnostics clean at project level |
|
||||
| Code change | lsp_diagnostics clean + manual Read of every changed file |
|
||||
| Build | Exit code 0 |
|
||||
| Tests | All pass |
|
||||
| Delegation | Verified independently |
|
||||
| Logic correct | You read the code and can explain what it does |
|
||||
| Boulder state | Read plan file, confirmed progress |
|
||||
|
||||
**No evidence = not complete.**
|
||||
**No evidence = not complete. Skipping manual review = rubber-stamping broken work.**
|
||||
</verification_rules>
|
||||
|
||||
<boundaries>
|
||||
|
||||
@@ -24,7 +24,7 @@ You DELEGATE, COORDINATE, and VERIFY. You NEVER write code yourself.
|
||||
</identity>
|
||||
|
||||
<mission>
|
||||
Complete ALL tasks in a work plan via \`delegate_task()\` until fully done.
|
||||
Complete ALL tasks in a work plan via \`task()\` until fully done.
|
||||
- One task per delegation
|
||||
- Parallel when independent
|
||||
- Verify everything
|
||||
@@ -71,14 +71,14 @@ Complete ALL tasks in a work plan via \`delegate_task()\` until fully done.
|
||||
<delegation_system>
|
||||
## Delegation API
|
||||
|
||||
Use \`delegate_task()\` with EITHER category OR agent (mutually exclusive):
|
||||
Use \`task()\` with EITHER category OR agent (mutually exclusive):
|
||||
|
||||
\`\`\`typescript
|
||||
// Category + Skills (spawns Sisyphus-Junior)
|
||||
delegate_task(category="[name]", load_skills=["skill-1"], run_in_background=false, prompt="...")
|
||||
task(category="[name]", load_skills=["skill-1"], run_in_background=false, prompt="...")
|
||||
|
||||
// Specialized Agent
|
||||
delegate_task(subagent_type="[agent]", load_skills=[], run_in_background=false, prompt="...")
|
||||
task(subagent_type="[agent]", load_skills=[], run_in_background=false, prompt="...")
|
||||
\`\`\`
|
||||
|
||||
{CATEGORY_SECTION}
|
||||
@@ -93,7 +93,7 @@ delegate_task(subagent_type="[agent]", load_skills=[], run_in_background=false,
|
||||
|
||||
## 6-Section Prompt Structure (MANDATORY)
|
||||
|
||||
Every \`delegate_task()\` prompt MUST include ALL 6 sections:
|
||||
Every \`task()\` prompt MUST include ALL 6 sections:
|
||||
|
||||
\`\`\`markdown
|
||||
## 1. TASK
|
||||
@@ -166,7 +166,7 @@ Structure: learnings.md, decisions.md, issues.md, problems.md
|
||||
## Step 3: Execute Tasks
|
||||
|
||||
### 3.1 Parallelization Check
|
||||
- Parallel tasks → invoke multiple \`delegate_task()\` in ONE message
|
||||
- Parallel tasks → invoke multiple \`task()\` in ONE message
|
||||
- Sequential → process one at a time
|
||||
|
||||
### 3.2 Pre-Delegation (MANDATORY)
|
||||
@@ -176,32 +176,64 @@ Read(".sisyphus/notepads/{plan-name}/issues.md")
|
||||
\`\`\`
|
||||
Extract wisdom → include in prompt.
|
||||
|
||||
### 3.3 Invoke delegate_task()
|
||||
### 3.3 Invoke task()
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[cat]", load_skills=["[skills]"], run_in_background=false, prompt=\`[6-SECTION PROMPT]\`)
|
||||
task(category="[cat]", load_skills=["[skills]"], run_in_background=false, prompt=\`[6-SECTION PROMPT]\`)
|
||||
\`\`\`
|
||||
|
||||
### 3.4 Verify (PROJECT-LEVEL QA)
|
||||
### 3.4 Verify (MANDATORY — EVERY SINGLE DELEGATION)
|
||||
|
||||
After EVERY delegation:
|
||||
After EVERY delegation, complete ALL steps — no shortcuts:
|
||||
|
||||
#### A. Automated Verification
|
||||
1. \`lsp_diagnostics(filePath=".")\` → ZERO errors
|
||||
2. \`Bash("bun run build")\` → exit 0
|
||||
3. \`Bash("bun test")\` → all pass
|
||||
4. \`Read\` changed files → confirm requirements met
|
||||
|
||||
Checklist:
|
||||
- [ ] lsp_diagnostics clean
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass
|
||||
- [ ] Files match requirements
|
||||
#### B. Manual Code Review (NON-NEGOTIABLE)
|
||||
1. \`Read\` EVERY file the subagent touched — no exceptions
|
||||
2. For each file, verify line by line:
|
||||
|
||||
| Check | What to Look For |
|
||||
|-------|------------------|
|
||||
| Logic correctness | Does implementation match task requirements? |
|
||||
| Completeness | No stubs, TODOs, placeholders, hardcoded values? |
|
||||
| Edge cases | Off-by-one, null checks, error paths handled? |
|
||||
| Patterns | Follows existing codebase conventions? |
|
||||
| Imports | Correct, complete, no unused? |
|
||||
|
||||
3. Cross-check: subagent's claims vs actual code — do they match?
|
||||
4. If mismatch found → resume session with \`session_id\` and fix
|
||||
|
||||
**If you cannot explain what the changed code does, you have not reviewed it.**
|
||||
|
||||
#### C. Hands-On QA (if applicable)
|
||||
| Deliverable | Method | Tool |
|
||||
|-------------|--------|------|
|
||||
| Frontend/UI | Browser | \`/playwright\` |
|
||||
| TUI/CLI | Interactive | \`interactive_bash\` |
|
||||
| API/Backend | Real requests | curl |
|
||||
|
||||
#### D. Check Boulder State Directly
|
||||
After verification, READ the plan file — every time:
|
||||
\`\`\`
|
||||
Read(".sisyphus/tasks/{plan-name}.yaml")
|
||||
\`\`\`
|
||||
Count remaining \`- [ ]\` tasks. This is your ground truth.
|
||||
|
||||
Checklist (ALL required):
|
||||
- [ ] Automated: diagnostics clean, build passes, tests pass
|
||||
- [ ] Manual: Read EVERY changed file, logic matches requirements
|
||||
- [ ] Cross-check: subagent claims match actual code
|
||||
- [ ] Boulder: Read plan file, confirmed current progress
|
||||
|
||||
### 3.5 Handle Failures
|
||||
|
||||
**CRITICAL: Use \`session_id\` for retries.**
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(session_id="ses_xyz789", load_skills=[...], prompt="FAILED: {error}. Fix by: {instruction}")
|
||||
task(session_id="ses_xyz789", load_skills=[...], prompt="FAILED: {error}. Fix by: {instruction}")
|
||||
\`\`\`
|
||||
|
||||
- Maximum 3 retries per task
|
||||
@@ -231,18 +263,18 @@ ACCUMULATED WISDOM: [from notepad]
|
||||
<parallel_execution>
|
||||
**Exploration (explore/librarian)**: ALWAYS background
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="explore", run_in_background=true, ...)
|
||||
task(subagent_type="explore", load_skills=[], run_in_background=true, ...)
|
||||
\`\`\`
|
||||
|
||||
**Task execution**: NEVER background
|
||||
\`\`\`typescript
|
||||
delegate_task(category="...", run_in_background=false, ...)
|
||||
task(category="...", load_skills=[...], run_in_background=false, ...)
|
||||
\`\`\`
|
||||
|
||||
**Parallel task groups**: Invoke multiple in ONE message
|
||||
\`\`\`typescript
|
||||
delegate_task(category="quick", load_skills=[], run_in_background=false, prompt="Task 2...")
|
||||
delegate_task(category="quick", load_skills=[], run_in_background=false, prompt="Task 3...")
|
||||
task(category="quick", load_skills=[], run_in_background=false, prompt="Task 2...")
|
||||
task(category="quick", load_skills=[], run_in_background=false, prompt="Task 3...")
|
||||
\`\`\`
|
||||
|
||||
**Background management**:
|
||||
@@ -269,15 +301,23 @@ delegate_task(category="quick", load_skills=[], run_in_background=false, prompt=
|
||||
<verification_rules>
|
||||
You are the QA gate. Subagents lie. Verify EVERYTHING.
|
||||
|
||||
**After each delegation**:
|
||||
**After each delegation — BOTH automated AND manual verification are MANDATORY**:
|
||||
|
||||
| Step | Tool | Expected |
|
||||
|------|------|----------|
|
||||
| 1 | \`lsp_diagnostics(".")\` | ZERO errors |
|
||||
| 2 | \`Bash("bun run build")\` | exit 0 |
|
||||
| 3 | \`Bash("bun test")\` | all pass |
|
||||
| 4 | \`Read\` changed files | matches requirements |
|
||||
| 4 | \`Read\` EVERY changed file | logic matches requirements |
|
||||
| 5 | Cross-check claims vs code | subagent's report matches reality |
|
||||
| 6 | \`Read\` plan file | boulder state confirmed |
|
||||
|
||||
**No evidence = not complete.**
|
||||
**Manual code review (Step 4) is NON-NEGOTIABLE:**
|
||||
- Read every line of every changed file
|
||||
- Verify logic correctness, completeness, edge cases
|
||||
- If you can't explain what the code does, you haven't reviewed it
|
||||
|
||||
**No evidence = not complete. Skipping manual review = rubber-stamping broken work.**
|
||||
</verification_rules>
|
||||
|
||||
<boundaries>
|
||||
|
||||
@@ -1,33 +1,3 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Routing:
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) → gpt.ts (GPT-5.2 optimized)
|
||||
* 2. Default (Claude, etc.) → default.ts (Claude-optimized)
|
||||
*/
|
||||
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode, AgentPromptMetadata } from "../types"
|
||||
import { isGptModel } from "../types"
|
||||
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 } from "../../tools/delegate-task/constants"
|
||||
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
||||
|
||||
import { ATLAS_SYSTEM_PROMPT, getDefaultAtlasPrompt } from "./default"
|
||||
import { ATLAS_GPT_SYSTEM_PROMPT, getGptAtlasPrompt } from "./gpt"
|
||||
import {
|
||||
getCategoryDescription,
|
||||
buildAgentSelectionSection,
|
||||
buildCategorySection,
|
||||
buildSkillsSection,
|
||||
buildDecisionMatrix,
|
||||
} from "./utils"
|
||||
|
||||
export { ATLAS_SYSTEM_PROMPT, getDefaultAtlasPrompt } from "./default"
|
||||
export { ATLAS_GPT_SYSTEM_PROMPT, getGptAtlasPrompt } from "./gpt"
|
||||
export {
|
||||
@@ -36,118 +6,9 @@ export {
|
||||
buildCategorySection,
|
||||
buildSkillsSection,
|
||||
buildDecisionMatrix,
|
||||
} from "./utils"
|
||||
export { isGptModel }
|
||||
} from "./prompt-section-builder"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
export { createAtlasAgent, getAtlasPromptSource, getAtlasPrompt, atlasPromptMetadata } from "./agent"
|
||||
export type { AtlasPromptSource, OrchestratorContext } from "./agent"
|
||||
|
||||
export type AtlasPromptSource = "default" | "gpt"
|
||||
|
||||
/**
|
||||
* Determines which Atlas prompt to use based on model.
|
||||
*/
|
||||
export function getAtlasPromptSource(model?: string): AtlasPromptSource {
|
||||
if (model && isGptModel(model)) {
|
||||
return "gpt"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
export interface OrchestratorContext {
|
||||
model?: string
|
||||
availableAgents?: AvailableAgent[]
|
||||
availableSkills?: AvailableSkill[]
|
||||
userCategories?: Record<string, CategoryConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate Atlas prompt based on model.
|
||||
*/
|
||||
export function getAtlasPrompt(model?: string): string {
|
||||
const source = getAtlasPromptSource(model)
|
||||
|
||||
switch (source) {
|
||||
case "gpt":
|
||||
return getGptAtlasPrompt()
|
||||
case "default":
|
||||
default:
|
||||
return getDefaultAtlasPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
|
||||
const agents = ctx?.availableAgents ?? []
|
||||
const skills = ctx?.availableSkills ?? []
|
||||
const userCategories = ctx?.userCategories
|
||||
const model = ctx?.model
|
||||
|
||||
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)
|
||||
|
||||
const basePrompt = getAtlasPrompt(model)
|
||||
|
||||
return basePrompt
|
||||
.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",
|
||||
])
|
||||
|
||||
const baseConfig = {
|
||||
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),
|
||||
color: "#10B981",
|
||||
...restrictions,
|
||||
}
|
||||
|
||||
return baseConfig 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",
|
||||
}
|
||||
export { isGptModel } from "../types"
|
||||
|
||||
@@ -7,22 +7,24 @@
|
||||
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { formatCustomSkillsBlock, type AvailableAgent, type AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
||||
import { CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
||||
import { mergeCategories } from "../../shared/merge-categories"
|
||||
import { truncateDescription } from "../../shared/truncate-description"
|
||||
|
||||
export const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
||||
userCategories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks"
|
||||
|
||||
export function buildAgentSelectionSection(agents: AvailableAgent[]): string {
|
||||
if (agents.length === 0) {
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
if (agents.length === 0) {
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
|
||||
No agents available.`
|
||||
}
|
||||
No agents available.`
|
||||
}
|
||||
|
||||
const rows = agents.map((a) => {
|
||||
const shortDesc = a.description.split(".")[0] || a.description
|
||||
return `| \`${a.name}\` | ${shortDesc} |`
|
||||
})
|
||||
const rows = agents.map((a) => {
|
||||
const shortDesc = truncateDescription(a.description)
|
||||
return `| \`${a.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
|
||||
@@ -32,7 +34,7 @@ ${rows.join("\n")}`
|
||||
}
|
||||
|
||||
export function buildCategorySection(userCategories?: Record<string, CategoryConfig>): string {
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
const allCategories = mergeCategories(userCategories)
|
||||
const categoryRows = Object.entries(allCategories).map(([name, config]) => {
|
||||
const temp = config.temperature ?? 0.5
|
||||
return `| \`${name}\` | ${temp} | ${getCategoryDescription(name, userCategories)} |`
|
||||
@@ -47,7 +49,7 @@ Categories spawn \`Sisyphus-Junior-{category}\` with optimized settings:
|
||||
${categoryRows.join("\n")}
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category-name]", load_skills=[...], run_in_background=false, prompt="...")
|
||||
task(category="[category-name]", load_skills=[...], run_in_background=false, prompt="...")
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -59,16 +61,16 @@ export function buildSkillsSection(skills: AvailableSkill[]): string {
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const shortDesc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const shortDesc = truncateDescription(s.description)
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
const customRows = customSkills.map((s) => {
|
||||
const shortDesc = s.description.split(".")[0] || s.description
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${shortDesc} | ${source} |`
|
||||
})
|
||||
const customRows = customSkills.map((s) => {
|
||||
const shortDesc = truncateDescription(s.description)
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${shortDesc} | ${source} |`
|
||||
})
|
||||
|
||||
const customSkillBlock = formatCustomSkillsBlock(customRows, customSkills, "**")
|
||||
|
||||
@@ -105,7 +107,7 @@ Read each skill's description and ask: "Does this skill's domain overlap with my
|
||||
|
||||
**Usage:**
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category]", load_skills=["skill-1", "skill-2"], run_in_background=false, prompt="...")
|
||||
task(category="[category]", load_skills=["skill-1", "skill-2"], run_in_background=false, prompt="...")
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT:**
|
||||
@@ -115,16 +117,16 @@ delegate_task(category="[category]", load_skills=["skill-1", "skill-2"], run_in_
|
||||
}
|
||||
|
||||
export function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: Record<string, CategoryConfig>): string {
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
const allCategories = mergeCategories(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}"\` |`
|
||||
})
|
||||
const agentRows = agents.map((a) => {
|
||||
const shortDesc = truncateDescription(a.description)
|
||||
return `| ${shortDesc} | \`agent="${a.name}"\` |`
|
||||
})
|
||||
|
||||
return `##### Decision Matrix
|
||||
|
||||
182
src/agents/builtin-agents.ts
Normal file
182
src/agents/builtin-agents.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { BuiltinAgentName, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
|
||||
import type { CategoriesConfig, GitMasterConfig } from "../config/schema"
|
||||
import type { LoadedSkill } from "../features/opencode-skill-loader/types"
|
||||
import type { BrowserAutomationProvider } from "../config/schema"
|
||||
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 { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
|
||||
import { createMetisAgent, metisPromptMetadata } from "./metis"
|
||||
import { createAtlasAgent, atlasPromptMetadata } from "./atlas"
|
||||
import { createMomusAgent, momusPromptMetadata } from "./momus"
|
||||
import { createHephaestusAgent } from "./hephaestus"
|
||||
import type { AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
import { fetchAvailableModels, readConnectedProvidersCache } from "../shared"
|
||||
import { CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||
import { mergeCategories } from "../shared/merge-categories"
|
||||
import { buildAvailableSkills } from "./builtin-agents/available-skills"
|
||||
import { collectPendingBuiltinAgents } from "./builtin-agents/general-agents"
|
||||
import { maybeCreateSisyphusConfig } from "./builtin-agents/sisyphus-agent"
|
||||
import { maybeCreateHephaestusConfig } from "./builtin-agents/hephaestus-agent"
|
||||
import { maybeCreateAtlasConfig } from "./builtin-agents/atlas-agent"
|
||||
import { buildCustomAgentMetadata, parseRegisteredAgentSummaries } from "./custom-agent-summaries"
|
||||
|
||||
type AgentSource = AgentFactory | AgentConfig
|
||||
|
||||
const agentSources: Record<BuiltinAgentName, AgentSource> = {
|
||||
sisyphus: createSisyphusAgent,
|
||||
hephaestus: createHephaestusAgent,
|
||||
oracle: createOracleAgent,
|
||||
librarian: createLibrarianAgent,
|
||||
explore: createExploreAgent,
|
||||
"multimodal-looker": createMultimodalLookerAgent,
|
||||
metis: createMetisAgent,
|
||||
momus: createMomusAgent,
|
||||
// Note: Atlas is handled specially in createBuiltinAgents()
|
||||
// because it needs OrchestratorContext, not just a model string
|
||||
atlas: createAtlasAgent as AgentFactory,
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for each agent, used to build Sisyphus's dynamic prompt sections
|
||||
* (Delegation Table, Tool Selection, Key Triggers, etc.)
|
||||
*/
|
||||
const agentMetadata: Partial<Record<BuiltinAgentName, AgentPromptMetadata>> = {
|
||||
oracle: ORACLE_PROMPT_METADATA,
|
||||
librarian: LIBRARIAN_PROMPT_METADATA,
|
||||
explore: EXPLORE_PROMPT_METADATA,
|
||||
"multimodal-looker": MULTIMODAL_LOOKER_PROMPT_METADATA,
|
||||
metis: metisPromptMetadata,
|
||||
momus: momusPromptMetadata,
|
||||
atlas: atlasPromptMetadata,
|
||||
}
|
||||
|
||||
export async function createBuiltinAgents(
|
||||
disabledAgents: string[] = [],
|
||||
agentOverrides: AgentOverrides = {},
|
||||
directory?: string,
|
||||
systemDefaultModel?: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
discoveredSkills: LoadedSkill[] = [],
|
||||
customAgentSummaries?: unknown,
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
uiSelectedModel?: string,
|
||||
disabledSkills?: Set<string>,
|
||||
useTaskSystem = false
|
||||
): Promise<Record<string, AgentConfig>> {
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
// IMPORTANT: Do NOT call OpenCode client APIs 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 isFirstRunNoCache =
|
||||
availableModels.size === 0 && (!connectedProviders || connectedProviders.length === 0)
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
|
||||
const mergedCategories = mergeCategories(categories)
|
||||
|
||||
const availableCategories: AvailableCategory[] = Object.entries(mergedCategories).map(([name]) => ({
|
||||
name,
|
||||
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
|
||||
}))
|
||||
|
||||
const availableSkills = buildAvailableSkills(discoveredSkills, browserProvider, disabledSkills)
|
||||
|
||||
// Collect general agents first (for availableAgents), but don't add to result yet
|
||||
const { pendingAgentConfigs, availableAgents } = collectPendingBuiltinAgents({
|
||||
agentSources,
|
||||
agentMetadata,
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
directory,
|
||||
systemDefaultModel,
|
||||
mergedCategories,
|
||||
gitMasterConfig,
|
||||
browserProvider,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
disabledSkills,
|
||||
})
|
||||
|
||||
const registeredAgents = parseRegisteredAgentSummaries(customAgentSummaries)
|
||||
const builtinAgentNames = new Set(Object.keys(agentSources).map((name) => name.toLowerCase()))
|
||||
const disabledAgentNames = new Set(disabledAgents.map((name) => name.toLowerCase()))
|
||||
|
||||
for (const agent of registeredAgents) {
|
||||
const lowerName = agent.name.toLowerCase()
|
||||
if (builtinAgentNames.has(lowerName)) continue
|
||||
if (disabledAgentNames.has(lowerName)) continue
|
||||
if (availableAgents.some((availableAgent) => availableAgent.name.toLowerCase() === lowerName)) continue
|
||||
|
||||
availableAgents.push({
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
metadata: buildCustomAgentMetadata(agent.name, agent.description),
|
||||
})
|
||||
}
|
||||
|
||||
const sisyphusConfig = maybeCreateSisyphusConfig({
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
isFirstRunNoCache,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
availableCategories,
|
||||
mergedCategories,
|
||||
directory,
|
||||
userCategories: categories,
|
||||
useTaskSystem,
|
||||
})
|
||||
if (sisyphusConfig) {
|
||||
result["sisyphus"] = sisyphusConfig
|
||||
}
|
||||
|
||||
const hephaestusConfig = maybeCreateHephaestusConfig({
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
isFirstRunNoCache,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
availableCategories,
|
||||
mergedCategories,
|
||||
directory,
|
||||
useTaskSystem,
|
||||
})
|
||||
if (hephaestusConfig) {
|
||||
result["hephaestus"] = hephaestusConfig
|
||||
}
|
||||
|
||||
// Add pending agents after sisyphus and hephaestus to maintain order
|
||||
for (const [name, config] of pendingAgentConfigs) {
|
||||
result[name] = config
|
||||
}
|
||||
|
||||
const atlasConfig = maybeCreateAtlasConfig({
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
mergedCategories,
|
||||
directory,
|
||||
userCategories: categories,
|
||||
})
|
||||
if (atlasConfig) {
|
||||
result["atlas"] = atlasConfig
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
71
src/agents/builtin-agents/agent-overrides.ts
Normal file
71
src/agents/builtin-agents/agent-overrides.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentOverrideConfig } from "../types"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { deepMerge, migrateAgentConfig } from "../../shared"
|
||||
import { resolvePromptAppend } from "./resolve-file-uri"
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export 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
|
||||
|
||||
if (categoryConfig.prompt_append && typeof result.prompt === "string") {
|
||||
result.prompt = result.prompt + "\n" + resolvePromptAppend(categoryConfig.prompt_append)
|
||||
}
|
||||
|
||||
return result as AgentConfig
|
||||
}
|
||||
|
||||
export function mergeAgentConfig(
|
||||
base: AgentConfig,
|
||||
override: AgentOverrideConfig,
|
||||
directory?: string
|
||||
): AgentConfig {
|
||||
const migratedOverride = migrateAgentConfig(override as Record<string, unknown>) as AgentOverrideConfig
|
||||
const { prompt_append, ...rest } = migratedOverride
|
||||
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
||||
|
||||
if (prompt_append && merged.prompt) {
|
||||
merged.prompt = merged.prompt + "\n" + resolvePromptAppend(prompt_append, directory)
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
export function applyOverrides(
|
||||
config: AgentConfig,
|
||||
override: AgentOverrideConfig | undefined,
|
||||
mergedCategories: Record<string, CategoryConfig>,
|
||||
directory?: string
|
||||
): AgentConfig {
|
||||
let result = config
|
||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (overrideCategory) {
|
||||
result = applyCategoryOverride(result, overrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (override) {
|
||||
result = mergeAgentConfig(result, override, directory)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
66
src/agents/builtin-agents/atlas-agent.ts
Normal file
66
src/agents/builtin-agents/atlas-agent.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentOverrides } from "../types"
|
||||
import type { CategoriesConfig, CategoryConfig } from "../../config/schema"
|
||||
import type { AvailableAgent, AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { AGENT_MODEL_REQUIREMENTS } from "../../shared"
|
||||
import { applyOverrides } from "./agent-overrides"
|
||||
import { applyModelResolution } from "./model-resolution"
|
||||
import { createAtlasAgent } from "../atlas"
|
||||
|
||||
export function maybeCreateAtlasConfig(input: {
|
||||
disabledAgents: string[]
|
||||
agentOverrides: AgentOverrides
|
||||
uiSelectedModel?: string
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel?: string
|
||||
availableAgents: AvailableAgent[]
|
||||
availableSkills: AvailableSkill[]
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
directory?: string
|
||||
userCategories?: CategoriesConfig
|
||||
useTaskSystem?: boolean
|
||||
}): AgentConfig | undefined {
|
||||
const {
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
mergedCategories,
|
||||
directory,
|
||||
userCategories,
|
||||
} = input
|
||||
|
||||
if (disabledAgents.includes("atlas")) return undefined
|
||||
|
||||
const orchestratorOverride = agentOverrides["atlas"]
|
||||
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
const atlasResolution = applyModelResolution({
|
||||
uiSelectedModel: orchestratorOverride?.model ? undefined : uiSelectedModel,
|
||||
userModel: orchestratorOverride?.model,
|
||||
requirement: atlasRequirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (!atlasResolution) return undefined
|
||||
const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution
|
||||
|
||||
let orchestratorConfig = createAtlasAgent({
|
||||
model: atlasModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
userCategories,
|
||||
})
|
||||
|
||||
if (atlasResolvedVariant) {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
|
||||
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories, directory)
|
||||
|
||||
return orchestratorConfig
|
||||
}
|
||||
35
src/agents/builtin-agents/available-skills.ts
Normal file
35
src/agents/builtin-agents/available-skills.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import type { BrowserAutomationProvider } from "../../config/schema"
|
||||
import type { LoadedSkill, SkillScope } from "../../features/opencode-skill-loader/types"
|
||||
import { createBuiltinSkills } from "../../features/builtin-skills"
|
||||
|
||||
function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
|
||||
if (scope === "user" || scope === "opencode") return "user"
|
||||
if (scope === "project" || scope === "opencode-project") return "project"
|
||||
return "plugin"
|
||||
}
|
||||
|
||||
export function buildAvailableSkills(
|
||||
discoveredSkills: LoadedSkill[],
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
disabledSkills?: Set<string>
|
||||
): AvailableSkill[] {
|
||||
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills })
|
||||
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
|
||||
|
||||
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
location: "plugin" as const,
|
||||
}))
|
||||
|
||||
const discoveredAvailable: AvailableSkill[] = discoveredSkills
|
||||
.filter(s => !builtinSkillNames.has(s.name) && !disabledSkills?.has(s.name))
|
||||
.map((skill) => ({
|
||||
name: skill.name,
|
||||
description: skill.definition.description ?? "",
|
||||
location: mapScopeToLocation(skill.scope),
|
||||
}))
|
||||
|
||||
return [...builtinAvailable, ...discoveredAvailable]
|
||||
}
|
||||
8
src/agents/builtin-agents/environment-context.ts
Normal file
8
src/agents/builtin-agents/environment-context.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { createEnvContext } from "../env-context"
|
||||
|
||||
export function applyEnvironmentContext(config: AgentConfig, directory?: string): AgentConfig {
|
||||
if (!directory || !config.prompt) return config
|
||||
const envContext = createEnvContext()
|
||||
return { ...config, prompt: config.prompt + envContext }
|
||||
}
|
||||
103
src/agents/builtin-agents/general-agents.ts
Normal file
103
src/agents/builtin-agents/general-agents.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { BuiltinAgentName, AgentOverrides, AgentPromptMetadata } from "../types"
|
||||
import type { CategoryConfig, GitMasterConfig } from "../../config/schema"
|
||||
import type { BrowserAutomationProvider } from "../../config/schema"
|
||||
import type { AvailableAgent } from "../dynamic-agent-prompt-builder"
|
||||
import { AGENT_MODEL_REQUIREMENTS, isModelAvailable } from "../../shared"
|
||||
import { buildAgent, isFactory } from "../agent-builder"
|
||||
import { applyOverrides } from "./agent-overrides"
|
||||
import { applyEnvironmentContext } from "./environment-context"
|
||||
import { applyModelResolution } from "./model-resolution"
|
||||
|
||||
export function collectPendingBuiltinAgents(input: {
|
||||
agentSources: Record<BuiltinAgentName, import("../agent-builder").AgentSource>
|
||||
agentMetadata: Partial<Record<BuiltinAgentName, AgentPromptMetadata>>
|
||||
disabledAgents: string[]
|
||||
agentOverrides: AgentOverrides
|
||||
directory?: string
|
||||
systemDefaultModel?: string
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
gitMasterConfig?: GitMasterConfig
|
||||
browserProvider?: BrowserAutomationProvider
|
||||
uiSelectedModel?: string
|
||||
availableModels: Set<string>
|
||||
disabledSkills?: Set<string>
|
||||
useTaskSystem?: boolean
|
||||
}): { pendingAgentConfigs: Map<string, AgentConfig>; availableAgents: AvailableAgent[] } {
|
||||
const {
|
||||
agentSources,
|
||||
agentMetadata,
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
directory,
|
||||
systemDefaultModel,
|
||||
mergedCategories,
|
||||
gitMasterConfig,
|
||||
browserProvider,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
disabledSkills,
|
||||
} = input
|
||||
|
||||
const availableAgents: AvailableAgent[] = []
|
||||
const pendingAgentConfigs: Map<string, AgentConfig> = new Map()
|
||||
|
||||
for (const [name, source] of Object.entries(agentSources)) {
|
||||
const agentName = name as BuiltinAgentName
|
||||
|
||||
if (agentName === "sisyphus") continue
|
||||
if (agentName === "hephaestus") continue
|
||||
if (agentName === "atlas") continue
|
||||
if (disabledAgents.some((name) => name.toLowerCase() === agentName.toLowerCase())) continue
|
||||
|
||||
const override = agentOverrides[agentName]
|
||||
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||
|
||||
// Check if agent requires a specific model
|
||||
if (requirement?.requiresModel && availableModels) {
|
||||
if (!isModelAvailable(requirement.requiresModel, availableModels)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
|
||||
|
||||
const resolution = applyModelResolution({
|
||||
uiSelectedModel: (isPrimaryAgent && !override?.model) ? uiSelectedModel : undefined,
|
||||
userModel: override?.model,
|
||||
requirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
if (!resolution) continue
|
||||
const { model, variant: resolvedVariant } = resolution
|
||||
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills)
|
||||
|
||||
// Apply resolved variant from model fallback chain
|
||||
if (resolvedVariant) {
|
||||
config = { ...config, variant: resolvedVariant }
|
||||
}
|
||||
|
||||
if (agentName === "librarian") {
|
||||
config = applyEnvironmentContext(config, directory)
|
||||
}
|
||||
|
||||
config = applyOverrides(config, override, mergedCategories, directory)
|
||||
|
||||
// Store for later - will be added after sisyphus and hephaestus
|
||||
pendingAgentConfigs.set(name, config)
|
||||
|
||||
const metadata = agentMetadata[agentName]
|
||||
if (metadata) {
|
||||
availableAgents.push({
|
||||
name: agentName,
|
||||
description: config.description ?? "",
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { pendingAgentConfigs, availableAgents }
|
||||
}
|
||||
91
src/agents/builtin-agents/hephaestus-agent.ts
Normal file
91
src/agents/builtin-agents/hephaestus-agent.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentOverrides } from "../types"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { AGENT_MODEL_REQUIREMENTS, isAnyProviderConnected } from "../../shared"
|
||||
import { createHephaestusAgent } from "../hephaestus"
|
||||
import { createEnvContext } from "../env-context"
|
||||
import { applyCategoryOverride, mergeAgentConfig } from "./agent-overrides"
|
||||
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
|
||||
|
||||
export function maybeCreateHephaestusConfig(input: {
|
||||
disabledAgents: string[]
|
||||
agentOverrides: AgentOverrides
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel?: string
|
||||
isFirstRunNoCache: boolean
|
||||
availableAgents: AvailableAgent[]
|
||||
availableSkills: AvailableSkill[]
|
||||
availableCategories: AvailableCategory[]
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
directory?: string
|
||||
useTaskSystem: boolean
|
||||
}): AgentConfig | undefined {
|
||||
const {
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
isFirstRunNoCache,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
availableCategories,
|
||||
mergedCategories,
|
||||
directory,
|
||||
useTaskSystem,
|
||||
} = input
|
||||
|
||||
if (disabledAgents.includes("hephaestus")) return undefined
|
||||
|
||||
const hephaestusOverride = agentOverrides["hephaestus"]
|
||||
const hephaestusRequirement = AGENT_MODEL_REQUIREMENTS["hephaestus"]
|
||||
const hasHephaestusExplicitConfig = hephaestusOverride !== undefined
|
||||
|
||||
const hasRequiredProvider =
|
||||
!hephaestusRequirement?.requiresProvider ||
|
||||
hasHephaestusExplicitConfig ||
|
||||
isFirstRunNoCache ||
|
||||
isAnyProviderConnected(hephaestusRequirement.requiresProvider, availableModels)
|
||||
|
||||
if (!hasRequiredProvider) return undefined
|
||||
|
||||
let hephaestusResolution = applyModelResolution({
|
||||
userModel: hephaestusOverride?.model,
|
||||
requirement: hephaestusRequirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (isFirstRunNoCache && !hephaestusOverride?.model) {
|
||||
hephaestusResolution = getFirstFallbackModel(hephaestusRequirement)
|
||||
}
|
||||
|
||||
if (!hephaestusResolution) return undefined
|
||||
const { model: hephaestusModel, variant: hephaestusResolvedVariant } = hephaestusResolution
|
||||
|
||||
let hephaestusConfig = createHephaestusAgent(
|
||||
hephaestusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories,
|
||||
useTaskSystem
|
||||
)
|
||||
|
||||
hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" }
|
||||
|
||||
const hepOverrideCategory = (hephaestusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (hepOverrideCategory) {
|
||||
hephaestusConfig = applyCategoryOverride(hephaestusConfig, hepOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (directory && hephaestusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
hephaestusConfig = { ...hephaestusConfig, prompt: hephaestusConfig.prompt + envContext }
|
||||
}
|
||||
|
||||
if (hephaestusOverride) {
|
||||
hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride, directory)
|
||||
}
|
||||
return hephaestusConfig
|
||||
}
|
||||
28
src/agents/builtin-agents/model-resolution.ts
Normal file
28
src/agents/builtin-agents/model-resolution.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { resolveModelPipeline } from "../../shared"
|
||||
|
||||
export function applyModelResolution(input: {
|
||||
uiSelectedModel?: string
|
||||
userModel?: string
|
||||
requirement?: { fallbackChain?: { providers: string[]; model: string; variant?: string }[] }
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel?: string
|
||||
}) {
|
||||
const { uiSelectedModel, userModel, requirement, availableModels, systemDefaultModel } = input
|
||||
return resolveModelPipeline({
|
||||
intent: { uiSelectedModel, userModel },
|
||||
constraints: { availableModels },
|
||||
policy: { fallbackChain: requirement?.fallbackChain, systemDefaultModel },
|
||||
})
|
||||
}
|
||||
|
||||
export function getFirstFallbackModel(requirement?: {
|
||||
fallbackChain?: { providers: string[]; model: string; variant?: string }[]
|
||||
}) {
|
||||
const entry = requirement?.fallbackChain?.[0]
|
||||
if (!entry || entry.providers.length === 0) return undefined
|
||||
return {
|
||||
model: `${entry.providers[0]}/${entry.model}`,
|
||||
provenance: "provider-fallback" as const,
|
||||
variant: entry.variant,
|
||||
}
|
||||
}
|
||||
109
src/agents/builtin-agents/resolve-file-uri.test.ts
Normal file
109
src/agents/builtin-agents/resolve-file-uri.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { homedir, tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { resolvePromptAppend } from "./resolve-file-uri"
|
||||
|
||||
describe("resolvePromptAppend", () => {
|
||||
const fixtureRoot = join(tmpdir(), `resolve-file-uri-${Date.now()}`)
|
||||
const configDir = join(fixtureRoot, "config")
|
||||
const homeFixtureDir = join(homedir(), `.resolve-file-uri-home-${Date.now()}`)
|
||||
|
||||
const absoluteFilePath = join(fixtureRoot, "absolute.txt")
|
||||
const relativeFilePath = join(configDir, "relative.txt")
|
||||
const spacedFilePath = join(fixtureRoot, "with space.txt")
|
||||
const homeFilePath = join(homeFixtureDir, "home.txt")
|
||||
|
||||
beforeAll(() => {
|
||||
mkdirSync(fixtureRoot, { recursive: true })
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
mkdirSync(homeFixtureDir, { recursive: true })
|
||||
|
||||
writeFileSync(absoluteFilePath, "absolute-content", "utf8")
|
||||
writeFileSync(relativeFilePath, "relative-content", "utf8")
|
||||
writeFileSync(spacedFilePath, "encoded-content", "utf8")
|
||||
writeFileSync(homeFilePath, "home-content", "utf8")
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true })
|
||||
rmSync(homeFixtureDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test("returns non-file URI strings unchanged", () => {
|
||||
//#given
|
||||
const input = "append this text"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe(input)
|
||||
})
|
||||
|
||||
test("resolves absolute file URI to file contents", () => {
|
||||
//#given
|
||||
const input = `file://${absoluteFilePath}`
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("absolute-content")
|
||||
})
|
||||
|
||||
test("resolves relative file URI using configDir", () => {
|
||||
//#given
|
||||
const input = "file://./relative.txt"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input, configDir)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("relative-content")
|
||||
})
|
||||
|
||||
test("resolves home directory URI path", () => {
|
||||
//#given
|
||||
const input = `file://~/${homeFixtureDir.split("/").pop()}/home.txt`
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("home-content")
|
||||
})
|
||||
|
||||
test("resolves percent-encoded URI path", () => {
|
||||
//#given
|
||||
const input = `file://${encodeURIComponent(spacedFilePath)}`
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toBe("encoded-content")
|
||||
})
|
||||
|
||||
test("returns warning for malformed percent-encoding", () => {
|
||||
//#given
|
||||
const input = "file://%E0%A4%A"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toContain("[WARNING: Malformed file URI")
|
||||
})
|
||||
|
||||
test("returns warning when file does not exist", () => {
|
||||
//#given
|
||||
const input = "file:///path/does/not/exist.txt"
|
||||
|
||||
//#when
|
||||
const resolved = resolvePromptAppend(input)
|
||||
|
||||
//#then
|
||||
expect(resolved).toContain("[WARNING: Could not resolve file URI")
|
||||
})
|
||||
})
|
||||
30
src/agents/builtin-agents/resolve-file-uri.ts
Normal file
30
src/agents/builtin-agents/resolve-file-uri.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { isAbsolute, resolve } from "node:path"
|
||||
|
||||
export function resolvePromptAppend(promptAppend: string, configDir?: string): string {
|
||||
if (!promptAppend.startsWith("file://")) return promptAppend
|
||||
|
||||
const encoded = promptAppend.slice(7)
|
||||
|
||||
let filePath: string
|
||||
try {
|
||||
const decoded = decodeURIComponent(encoded)
|
||||
const expanded = decoded.startsWith("~/") ? decoded.replace(/^~\//, `${homedir()}/`) : decoded
|
||||
filePath = isAbsolute(expanded)
|
||||
? expanded
|
||||
: resolve(configDir ?? process.cwd(), expanded)
|
||||
} catch {
|
||||
return `[WARNING: Malformed file URI (invalid percent-encoding): ${promptAppend}]`
|
||||
}
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
return `[WARNING: Could not resolve file URI: ${promptAppend}]`
|
||||
}
|
||||
|
||||
try {
|
||||
return readFileSync(filePath, "utf8")
|
||||
} catch {
|
||||
return `[WARNING: Could not read file: ${promptAppend}]`
|
||||
}
|
||||
}
|
||||
84
src/agents/builtin-agents/sisyphus-agent.ts
Normal file
84
src/agents/builtin-agents/sisyphus-agent.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentOverrides } from "../types"
|
||||
import type { CategoriesConfig, CategoryConfig } from "../../config/schema"
|
||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { AGENT_MODEL_REQUIREMENTS, isAnyFallbackModelAvailable } from "../../shared"
|
||||
import { applyEnvironmentContext } from "./environment-context"
|
||||
import { applyOverrides } from "./agent-overrides"
|
||||
import { applyModelResolution, getFirstFallbackModel } from "./model-resolution"
|
||||
import { createSisyphusAgent } from "../sisyphus"
|
||||
|
||||
export function maybeCreateSisyphusConfig(input: {
|
||||
disabledAgents: string[]
|
||||
agentOverrides: AgentOverrides
|
||||
uiSelectedModel?: string
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel?: string
|
||||
isFirstRunNoCache: boolean
|
||||
availableAgents: AvailableAgent[]
|
||||
availableSkills: AvailableSkill[]
|
||||
availableCategories: AvailableCategory[]
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
directory?: string
|
||||
userCategories?: CategoriesConfig
|
||||
useTaskSystem: boolean
|
||||
}): AgentConfig | undefined {
|
||||
const {
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
isFirstRunNoCache,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
availableCategories,
|
||||
mergedCategories,
|
||||
directory,
|
||||
useTaskSystem,
|
||||
} = input
|
||||
|
||||
const sisyphusOverride = agentOverrides["sisyphus"]
|
||||
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||
const hasSisyphusExplicitConfig = sisyphusOverride !== undefined
|
||||
const meetsSisyphusAnyModelRequirement =
|
||||
!sisyphusRequirement?.requiresAnyModel ||
|
||||
hasSisyphusExplicitConfig ||
|
||||
isFirstRunNoCache ||
|
||||
isAnyFallbackModelAvailable(sisyphusRequirement.fallbackChain, availableModels)
|
||||
|
||||
if (disabledAgents.includes("sisyphus") || !meetsSisyphusAnyModelRequirement) return undefined
|
||||
|
||||
let sisyphusResolution = applyModelResolution({
|
||||
uiSelectedModel: sisyphusOverride?.model ? undefined : uiSelectedModel,
|
||||
userModel: sisyphusOverride?.model,
|
||||
requirement: sisyphusRequirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (isFirstRunNoCache && !sisyphusOverride?.model && !uiSelectedModel) {
|
||||
sisyphusResolution = getFirstFallbackModel(sisyphusRequirement)
|
||||
}
|
||||
|
||||
if (!sisyphusResolution) return undefined
|
||||
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
|
||||
|
||||
let sisyphusConfig = createSisyphusAgent(
|
||||
sisyphusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories,
|
||||
useTaskSystem
|
||||
)
|
||||
|
||||
if (sisyphusResolvedVariant) {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
|
||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories, directory)
|
||||
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
|
||||
|
||||
return sisyphusConfig
|
||||
}
|
||||
61
src/agents/custom-agent-summaries.ts
Normal file
61
src/agents/custom-agent-summaries.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { truncateDescription } from "../shared/truncate-description"
|
||||
|
||||
type RegisteredAgentSummary = {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
function sanitizeMarkdownTableCell(value: string): string {
|
||||
return value
|
||||
.replace(/\r?\n/g, " ")
|
||||
.replace(/\|/g, "\\|")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
export function parseRegisteredAgentSummaries(input: unknown): RegisteredAgentSummary[] {
|
||||
if (!Array.isArray(input)) return []
|
||||
|
||||
const result: RegisteredAgentSummary[] = []
|
||||
for (const item of input) {
|
||||
if (!isRecord(item)) continue
|
||||
|
||||
const name = typeof item.name === "string" ? item.name : undefined
|
||||
if (!name) continue
|
||||
|
||||
const hidden = item.hidden
|
||||
if (hidden === true) continue
|
||||
|
||||
const disabled = item.disabled
|
||||
if (disabled === true) continue
|
||||
|
||||
const enabled = item.enabled
|
||||
if (enabled === false) continue
|
||||
|
||||
const description = typeof item.description === "string" ? item.description : ""
|
||||
result.push({ name: sanitizeMarkdownTableCell(name), description: sanitizeMarkdownTableCell(description) })
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function buildCustomAgentMetadata(agentName: string, description: string): AgentPromptMetadata {
|
||||
const shortDescription = sanitizeMarkdownTableCell(truncateDescription(description))
|
||||
const safeAgentName = sanitizeMarkdownTableCell(agentName)
|
||||
|
||||
return {
|
||||
category: "specialist",
|
||||
cost: "CHEAP",
|
||||
triggers: [
|
||||
{
|
||||
domain: `Custom agent: ${safeAgentName}`,
|
||||
trigger: shortDescription || "Use when this agent's description matches the task",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { AgentPromptMetadata, BuiltinAgentName } from "./types"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { truncateDescription } from "../shared/truncate-description"
|
||||
|
||||
export interface AvailableAgent {
|
||||
name: BuiltinAgentName
|
||||
name: string
|
||||
description: string
|
||||
metadata: AgentPromptMetadata
|
||||
}
|
||||
@@ -205,16 +206,16 @@ export function buildCategorySkillsDelegationGuide(categories: AvailableCategory
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const desc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const desc = truncateDescription(s.description)
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
|
||||
const customRows = customSkills.map((s) => {
|
||||
const desc = s.description.split(".")[0] || s.description
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${desc} | ${source} |`
|
||||
})
|
||||
const customRows = customSkills.map((s) => {
|
||||
const desc = truncateDescription(s.description)
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${desc} | ${source} |`
|
||||
})
|
||||
|
||||
const customSkillBlock = formatCustomSkillsBlock(customRows, customSkills)
|
||||
|
||||
@@ -242,7 +243,7 @@ ${builtinRows.join("\n")}`
|
||||
|
||||
return `### Category + Skills Delegation System
|
||||
|
||||
**delegate_task() combines categories and skills for optimal task execution.**
|
||||
**task() combines categories and skills for optimal task execution.**
|
||||
|
||||
#### Available Categories (Domain-Optimized Models)
|
||||
|
||||
@@ -296,7 +297,7 @@ SKILL EVALUATION for "[skill-name]":
|
||||
### Delegation Pattern
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
task(
|
||||
category="[selected-category]",
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills — ESPECIALLY user-installed ones
|
||||
prompt="..."
|
||||
@@ -305,7 +306,7 @@ delegate_task(
|
||||
|
||||
**ANTI-PATTERN (will produce poor results):**
|
||||
\`\`\`typescript
|
||||
delegate_task(category="...", load_skills=[], run_in_background=false, prompt="...") // Empty load_skills without justification
|
||||
task(category="...", load_skills=[], run_in_background=false, prompt="...") // Empty load_skills without justification
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -421,7 +422,7 @@ export function buildUltraworkSection(
|
||||
|
||||
lines.push("**Agents** (for specialized consultation/exploration):")
|
||||
for (const agent of sortedAgents) {
|
||||
const shortDesc = agent.description.split(".")[0] || agent.description
|
||||
const shortDesc = agent.description.length > 120 ? agent.description.slice(0, 120) + "..." : agent.description
|
||||
const suffix = agent.name === "explore" || agent.name === "librarian" ? " (multiple)" : ""
|
||||
lines.push(`- \`${agent.name}${suffix}\`: ${shortDesc}`)
|
||||
}
|
||||
|
||||
33
src/agents/env-context.ts
Normal file
33
src/agents/env-context.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Creates OmO-specific environment context (time, timezone, locale).
|
||||
* Note: Working directory, platform, and date are already provided by OpenCode's system.ts,
|
||||
* so we only include fields that OpenCode doesn't provide to avoid duplication.
|
||||
* See: https://github.com/code-yeongyu/oh-my-opencode/issues/379
|
||||
*/
|
||||
export function createEnvContext(): string {
|
||||
const now = new Date()
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
||||
|
||||
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",
|
||||
hour12: true,
|
||||
})
|
||||
|
||||
return `
|
||||
<omo-env>
|
||||
Current date: ${dateStr}
|
||||
Current time: ${timeStr}
|
||||
Timezone: ${timezone}
|
||||
Locale: ${locale}
|
||||
</omo-env>`
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function createExploreAgent(model: string): AgentConfig {
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
"task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode } from "./types"
|
||||
import type { AvailableAgent, AvailableTool, AvailableSkill, AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk";
|
||||
import type { AgentMode } from "./types";
|
||||
import type {
|
||||
AvailableAgent,
|
||||
AvailableTool,
|
||||
AvailableSkill,
|
||||
AvailableCategory,
|
||||
} from "./dynamic-agent-prompt-builder";
|
||||
import {
|
||||
buildKeyTriggersSection,
|
||||
buildToolSelectionTable,
|
||||
@@ -12,9 +17,9 @@ import {
|
||||
buildHardBlocksSection,
|
||||
buildAntiPatternsSection,
|
||||
categorizeTools,
|
||||
} from "./dynamic-agent-prompt-builder"
|
||||
} from "./dynamic-agent-prompt-builder";
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
const MODE: AgentMode = "primary";
|
||||
|
||||
function buildTodoDisciplineSection(useTaskSystem: boolean): string {
|
||||
if (useTaskSystem) {
|
||||
@@ -52,7 +57,7 @@ function buildTodoDisciplineSection(useTaskSystem: boolean): string {
|
||||
| Proceeding without \`in_progress\` | No indication of current work |
|
||||
| Finishing without completing tasks | Task appears incomplete |
|
||||
|
||||
**NO TASKS ON MULTI-STEP WORK = INCOMPLETE WORK.**`
|
||||
**NO TASKS ON MULTI-STEP WORK = INCOMPLETE WORK.**`;
|
||||
}
|
||||
|
||||
return `## Todo Discipline (NON-NEGOTIABLE)
|
||||
@@ -89,7 +94,7 @@ function buildTodoDisciplineSection(useTaskSystem: boolean): string {
|
||||
| Proceeding without \`in_progress\` | No indication of current work |
|
||||
| Finishing without completing todos | Task appears incomplete |
|
||||
|
||||
**NO TODOS ON MULTI-STEP WORK = INCOMPLETE WORK.**`
|
||||
**NO TODOS ON MULTI-STEP WORK = INCOMPLETE WORK.**`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,18 +116,25 @@ function buildHephaestusPrompt(
|
||||
availableTools: AvailableTool[] = [],
|
||||
availableSkills: AvailableSkill[] = [],
|
||||
availableCategories: AvailableCategory[] = [],
|
||||
useTaskSystem = false
|
||||
useTaskSystem = false,
|
||||
): 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 todoDiscipline = buildTodoDisciplineSection(useTaskSystem)
|
||||
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 todoDiscipline = buildTodoDisciplineSection(useTaskSystem);
|
||||
|
||||
return `You are Hephaestus, an autonomous deep worker for software engineering.
|
||||
|
||||
@@ -226,9 +238,10 @@ Agent: *runs gh pr list, gh pr view, searches recent commits*
|
||||
### Step 3: Validate Before Acting
|
||||
|
||||
**Delegation Check (MANDATORY before acting directly):**
|
||||
0. Find relevant skills that you can load, and load them IMMEDIATELY.
|
||||
1. Is there a specialized agent that perfectly matches this request?
|
||||
2. If not, is there a \`delegate_task\` category that best describes this task? What skills are available to equip the agent with?
|
||||
- MUST FIND skills to use: \`delegate_task(load_skills=[{skill1}, ...])\`
|
||||
2. If not, is there a \`task\` category that best describes this task? What skills are available to equip the agent with?
|
||||
- MUST FIND skills to use: \`task(load_skills=[{skill1}, ...])\`
|
||||
3. Can I do it myself for the best result, FOR SURE?
|
||||
|
||||
**Default Bias: DELEGATE for complex tasks. Work yourself ONLY when trivial.**
|
||||
@@ -278,17 +291,23 @@ ${librarianSection}
|
||||
|
||||
\`\`\`typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Prompt structure: [CONTEXT: what I'm doing] + [GOAL: what I'm trying to achieve] + [QUESTION: what I need to know] + [REQUEST: what to find]
|
||||
// Prompt structure (each field should be substantive, not a single sentence):
|
||||
// [CONTEXT]: What task I'm working on, which files/modules are involved, and what approach I'm taking
|
||||
// [GOAL]: The specific outcome I need — what decision or action the results will unblock
|
||||
// [DOWNSTREAM]: How I will use the results — what I'll build/decide based on what's found
|
||||
// [REQUEST]: Concrete search instructions — what to find, what format to return, and what to SKIP
|
||||
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="I'm implementing user authentication for our API. I need to understand how auth is currently structured in this codebase. Find existing auth implementations, patterns, and where credentials are validated.")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="I'm adding error handling to the auth flow. I want to follow existing project conventions for consistency. Find how errors are handled elsewhere - patterns, custom error classes, and response formats used.")
|
||||
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find auth implementations", prompt="I'm implementing JWT auth for the REST API in src/api/routes/. I need to match existing auth conventions so my code fits seamlessly. I'll use this to decide middleware structure and token flow. Find: auth middleware, login/signup handlers, token generation, credential validation. Focus on src/ — skip tests. Return file paths with pattern descriptions.")
|
||||
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find error handling patterns", prompt="I'm adding error handling to the auth flow and need to follow existing error conventions exactly. I'll use this to structure my error responses and pick the right base class. Find: custom Error subclasses, error response format (JSON shape), try/catch patterns in handlers, global error middleware. Skip test files. Return the error class hierarchy and response format.")
|
||||
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="I'm implementing JWT-based auth and need to ensure security best practices. Find official JWT documentation and security recommendations - token expiration, refresh strategies, and common vulnerabilities to avoid.")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="I'm building Express middleware for auth and want production-quality patterns. Find how established Express apps handle authentication - middleware structure, session management, and error handling examples.")
|
||||
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find JWT security docs", prompt="I'm implementing JWT auth and need current security best practices to choose token storage (httpOnly cookies vs localStorage) and set expiration policy. Find: OWASP auth guidelines, recommended token lifetimes, refresh token rotation strategies, common JWT vulnerabilities. Skip 'what is JWT' tutorials — production security guidance only.")
|
||||
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find Express auth patterns", prompt="I'm building Express auth middleware and need production-quality patterns to structure my middleware chain. Find how established Express apps (1000+ stars) handle: middleware ordering, token refresh, role-based access control, auth error propagation. Skip basic tutorials — I need battle-tested patterns with proper error handling.")
|
||||
// Continue immediately - collect results when needed
|
||||
|
||||
// WRONG: Sequential or blocking - NEVER DO THIS
|
||||
result = delegate_task(..., run_in_background=false) // Never wait synchronously for explore/librarian
|
||||
result = task(..., run_in_background=false) // Never wait synchronously for explore/librarian
|
||||
\`\`\`
|
||||
|
||||
**Rules:**
|
||||
@@ -393,7 +412,7 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
|
||||
### Session Continuity (MANDATORY)
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
Every \`task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
**ALWAYS continue when:**
|
||||
| Scenario | Action |
|
||||
@@ -405,9 +424,13 @@ Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
**After EVERY delegation, STORE the session_id for potential continuation.**
|
||||
|
||||
${oracleSection ? `
|
||||
${
|
||||
oracleSection
|
||||
? `
|
||||
${oracleSection}
|
||||
` : ""}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
## Role & Agency (CRITICAL - READ CAREFULLY)
|
||||
|
||||
@@ -585,7 +608,7 @@ When working on long sessions or complex multi-file tasks:
|
||||
## Soft Guidelines
|
||||
|
||||
- Prefer existing libraries over new dependencies
|
||||
- Prefer small, focused changes over large refactors`
|
||||
- Prefer small, focused changes over large refactors`;
|
||||
}
|
||||
|
||||
export function createHephaestusAgent(
|
||||
@@ -594,14 +617,20 @@ export function createHephaestusAgent(
|
||||
availableToolNames?: string[],
|
||||
availableSkills?: AvailableSkill[],
|
||||
availableCategories?: AvailableCategory[],
|
||||
useTaskSystem = false
|
||||
useTaskSystem = false,
|
||||
): AgentConfig {
|
||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : []
|
||||
const skills = availableSkills ?? []
|
||||
const categories = availableCategories ?? []
|
||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : [];
|
||||
const skills = availableSkills ?? [];
|
||||
const categories = availableCategories ?? [];
|
||||
const prompt = availableAgents
|
||||
? buildHephaestusPrompt(availableAgents, tools, skills, categories, useTaskSystem)
|
||||
: buildHephaestusPrompt([], tools, skills, categories, useTaskSystem)
|
||||
? buildHephaestusPrompt(
|
||||
availableAgents,
|
||||
tools,
|
||||
skills,
|
||||
categories,
|
||||
useTaskSystem,
|
||||
)
|
||||
: buildHephaestusPrompt([], tools, skills, categories, useTaskSystem);
|
||||
|
||||
return {
|
||||
description:
|
||||
@@ -611,8 +640,11 @@ export function createHephaestusAgent(
|
||||
maxTokens: 32000,
|
||||
prompt,
|
||||
color: "#D97706", // Forged Amber - Golden heated metal, divine craftsman
|
||||
permission: { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"],
|
||||
permission: {
|
||||
question: "allow",
|
||||
call_omo_agent: "deny",
|
||||
} as AgentConfig["permission"],
|
||||
reasoningEffort: "medium",
|
||||
}
|
||||
};
|
||||
}
|
||||
createHephaestusAgent.mode = MODE
|
||||
createHephaestusAgent.mode = MODE;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from "./types"
|
||||
export { createBuiltinAgents } from "./utils"
|
||||
export { createBuiltinAgents } from "./builtin-agents"
|
||||
export type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||
export { createSisyphusAgent } from "./sisyphus"
|
||||
export { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||
|
||||
@@ -26,7 +26,7 @@ export function createLibrarianAgent(model: string): AgentConfig {
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
"task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
|
||||
|
||||
@@ -307,7 +307,6 @@ const metisRestrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
])
|
||||
|
||||
export function createMetisAgent(model: string): AgentConfig {
|
||||
|
||||
@@ -193,7 +193,7 @@ export function createMomusAgent(model: string): AgentConfig {
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
"task",
|
||||
])
|
||||
|
||||
const base = {
|
||||
|
||||
@@ -147,7 +147,7 @@ export function createOracleAgent(model: string): AgentConfig {
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
"task",
|
||||
])
|
||||
|
||||
const base = {
|
||||
|
||||
@@ -15,8 +15,9 @@ export const PROMETHEUS_HIGH_ACCURACY_MODE = `# PHASE 3: PLAN GENERATION
|
||||
\`\`\`typescript
|
||||
// After generating initial plan
|
||||
while (true) {
|
||||
const result = delegate_task(
|
||||
const result = task(
|
||||
subagent_type="momus",
|
||||
load_skills=[],
|
||||
prompt=".sisyphus/plans/{name}.md",
|
||||
run_in_background=false
|
||||
)
|
||||
|
||||
@@ -1,50 +1,4 @@
|
||||
/**
|
||||
* Prometheus Planner System Prompt
|
||||
*
|
||||
* Named after the Titan who gave fire (knowledge/foresight) to humanity.
|
||||
* Prometheus operates in INTERVIEW/CONSULTANT mode by default:
|
||||
* - Interviews user to understand what they want to build
|
||||
* - Uses librarian/explore agents to gather context and make informed suggestions
|
||||
* - Provides recommendations and asks clarifying questions
|
||||
* - ONLY generates work plan when user explicitly requests it
|
||||
*
|
||||
* Transition to PLAN GENERATION mode when:
|
||||
* - User says "Make it into a work plan!" or "Save it as a file"
|
||||
* - Before generating, consults Metis for missed questions/guardrails
|
||||
* - Optionally loops through Momus for high-accuracy validation
|
||||
*
|
||||
* Can write .md files only (enforced by prometheus-md-only hook).
|
||||
*/
|
||||
|
||||
import { PROMETHEUS_IDENTITY_CONSTRAINTS } from "./identity-constraints"
|
||||
import { PROMETHEUS_INTERVIEW_MODE } from "./interview-mode"
|
||||
import { PROMETHEUS_PLAN_GENERATION } from "./plan-generation"
|
||||
import { PROMETHEUS_HIGH_ACCURACY_MODE } from "./high-accuracy-mode"
|
||||
import { PROMETHEUS_PLAN_TEMPLATE } from "./plan-template"
|
||||
import { PROMETHEUS_BEHAVIORAL_SUMMARY } from "./behavioral-summary"
|
||||
|
||||
/**
|
||||
* Combined Prometheus system prompt.
|
||||
* Assembled from modular sections for maintainability.
|
||||
*/
|
||||
export const PROMETHEUS_SYSTEM_PROMPT = `${PROMETHEUS_IDENTITY_CONSTRAINTS}
|
||||
${PROMETHEUS_INTERVIEW_MODE}
|
||||
${PROMETHEUS_PLAN_GENERATION}
|
||||
${PROMETHEUS_HIGH_ACCURACY_MODE}
|
||||
${PROMETHEUS_PLAN_TEMPLATE}
|
||||
${PROMETHEUS_BEHAVIORAL_SUMMARY}`
|
||||
|
||||
/**
|
||||
* Prometheus planner permission configuration.
|
||||
* Allows write/edit for plan files (.md only, enforced by prometheus-md-only hook).
|
||||
* Question permission allows agent to ask user questions via OpenCode's QuestionTool.
|
||||
*/
|
||||
export const PROMETHEUS_PERMISSION = {
|
||||
edit: "allow" as const,
|
||||
bash: "allow" as const,
|
||||
webfetch: "allow" as const,
|
||||
question: "allow" as const,
|
||||
}
|
||||
export { PROMETHEUS_SYSTEM_PROMPT, PROMETHEUS_PERMISSION } from "./system-prompt"
|
||||
|
||||
// Re-export individual sections for granular access
|
||||
export { PROMETHEUS_IDENTITY_CONSTRAINTS } from "./identity-constraints"
|
||||
|
||||
@@ -65,9 +65,13 @@ Or should I just note down this single fix?"
|
||||
|
||||
**Research First:**
|
||||
\`\`\`typescript
|
||||
// Prompt structure: CONTEXT (what I'm doing) + GOAL (what I'm trying to achieve) + QUESTION (what I need to know) + REQUEST (what to find)
|
||||
delegate_task(subagent_type="explore", prompt="I'm refactoring [target] and need to understand its impact scope before making changes. Find all usages via lsp_find_references - show calling code, patterns of use, and potential breaking points.", run_in_background=true)
|
||||
delegate_task(subagent_type="explore", prompt="I'm about to modify [affected code] and need to ensure behavior preservation. Find existing test coverage - which tests exercise this code, what assertions exist, and any gaps in coverage.", run_in_background=true)
|
||||
// Prompt structure (each field substantive):
|
||||
// [CONTEXT]: Task, files/modules involved, approach
|
||||
// [GOAL]: Specific outcome needed — what decision/action results will unblock
|
||||
// [DOWNSTREAM]: How results will be used
|
||||
// [REQUEST]: What to find, return format, what to SKIP
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm refactoring [target] and need to map its full impact scope before making changes. I'll use this to build a safe refactoring plan. Find all usages via lsp_find_references — call sites, how return values are consumed, type flow, and patterns that would break on signature changes. Also check for dynamic access that lsp_find_references might miss. Return: file path, usage pattern, risk level (high/medium/low) per call site.", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm about to modify [affected code] and need to understand test coverage for behavior preservation. I'll use this to decide whether to add tests first. Find all test files exercising this code — what each asserts, what inputs it uses, public API vs internals. Identify coverage gaps: behaviors used in production but untested. Return a coverage map: tested vs untested behaviors.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus:**
|
||||
@@ -90,10 +94,10 @@ delegate_task(subagent_type="explore", prompt="I'm about to modify [affected cod
|
||||
**Pre-Interview Research (MANDATORY):**
|
||||
\`\`\`typescript
|
||||
// Launch BEFORE asking user questions
|
||||
// Prompt structure: CONTEXT + GOAL + QUESTION + REQUEST
|
||||
delegate_task(subagent_type="explore", prompt="I'm building a new [feature] and want to maintain codebase consistency. Find similar implementations in this project - their structure, patterns used, and conventions to follow.", run_in_background=true)
|
||||
delegate_task(subagent_type="explore", prompt="I'm adding [feature type] to the project and need to understand existing conventions. Find how similar features are organized - file structure, naming patterns, and architectural approach.", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="I'm implementing [technology] and want to follow established best practices. Find official documentation and community recommendations - setup patterns, common pitfalls, and production-ready examples.", run_in_background=true)
|
||||
// Prompt structure: [CONTEXT] + [GOAL] + [DOWNSTREAM] + [REQUEST]
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm building a new [feature] from scratch and need to match existing codebase conventions exactly. I'll use this to copy the right file structure and patterns. Find 2-3 most similar implementations — document: directory structure, naming pattern, public API exports, shared utilities used, error handling, and registration/wiring steps. Return concrete file paths and patterns, not abstract descriptions.", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm adding [feature type] and need to understand organizational conventions to match them. I'll use this to determine directory layout and naming scheme. Find how similar features are organized: nesting depth, index.ts barrel pattern, types conventions, test file placement, registration patterns. Compare 2-3 feature directories. Return the canonical structure as a file tree.", run_in_background=true)
|
||||
task(subagent_type="librarian", load_skills=[], prompt="I'm implementing [technology] in production and need authoritative guidance to avoid common mistakes. I'll use this for setup and configuration decisions. Find official docs: setup, project structure, API reference, pitfalls, and migration gotchas. Also find 1-2 production-quality OSS examples (not tutorials). Skip beginner guides — I need production patterns only.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus** (AFTER research):
|
||||
@@ -132,7 +136,7 @@ Based on your stack, I'd recommend NextAuth.js - it integrates well with Next.js
|
||||
|
||||
Run this check:
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="explore", prompt="I'm assessing this project's test setup before planning work that may require TDD. I need to understand what testing capabilities exist. Find test infrastructure: package.json test scripts, config files (jest.config, vitest.config, pytest.ini), and existing test files. Report: 1) Does test infra exist? 2) What framework? 3) Example test patterns.", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm assessing test infrastructure before planning TDD work. I'll use this to decide whether to include test setup tasks. Find: 1) Test framework — package.json scripts, config files (jest/vitest/bun/pytest), test dependencies. 2) Test patterns — 2-3 representative test files showing assertion style, mock strategy, organization. 3) Coverage config and test-to-source ratio. 4) CI integration — test commands in .github/workflows. Return structured report: YES/NO per capability with examples.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
#### Step 2: Ask the Test Question (MANDATORY)
|
||||
@@ -230,13 +234,13 @@ Add to draft immediately:
|
||||
|
||||
**Research First:**
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="explore", prompt="I'm planning architectural changes and need to understand the current system design. Find existing architecture: module boundaries, dependency patterns, data flow, and key abstractions used.", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="I'm designing architecture for [domain] and want to make informed decisions. Find architectural best practices - proven patterns, trade-offs, and lessons learned from similar systems.", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm planning architectural changes and need to understand current system design. I'll use this to identify safe-to-change vs load-bearing boundaries. Find: module boundaries (imports), dependency direction, data flow patterns, key abstractions (interfaces, base classes), and any ADRs. Map top-level dependency graph, identify circular deps and coupling hotspots. Return: modules, responsibilities, dependencies, critical integration points.", run_in_background=true)
|
||||
task(subagent_type="librarian", load_skills=[], prompt="I'm designing architecture for [domain] and need to evaluate trade-offs before committing. I'll use this to present concrete options to the user. Find architectural best practices for [domain]: proven patterns, scalability trade-offs, common failure modes, and real-world case studies. Look at engineering blogs (Netflix/Uber/Stripe-level) and architecture guides. Skip generic pattern catalogs — I need domain-specific guidance.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Oracle Consultation** (recommend when stakes are high):
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="oracle", prompt="Architecture consultation needed: [context]...", run_in_background=false)
|
||||
task(subagent_type="oracle", load_skills=[], prompt="Architecture consultation needed: [context]...", run_in_background=false)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus:**
|
||||
@@ -253,9 +257,9 @@ delegate_task(subagent_type="oracle", prompt="Architecture consultation needed:
|
||||
|
||||
**Parallel Investigation:**
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="explore", prompt="I'm researching how to implement [feature] and need to understand current approach. Find how X is currently handled in this codebase - implementation details, edge cases covered, and any known limitations.", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="I'm implementing Y and need authoritative guidance. Find official documentation - API reference, configuration options, and recommended usage patterns.", run_in_background=true)
|
||||
delegate_task(subagent_type="librarian", prompt="I'm looking for battle-tested implementations of Z. Find open source projects that solve this - focus on production-quality code, how they handle edge cases, and any gotchas documented.", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm researching [feature] to decide whether to extend or replace the current approach. I'll use this to recommend a strategy. Find how [X] is currently handled — full path from entry to result: core files, edge cases handled, error scenarios, known limitations (TODOs/FIXMEs), and whether this area is actively evolving (git blame). Return: what works, what's fragile, what's missing.", run_in_background=true)
|
||||
task(subagent_type="librarian", load_skills=[], prompt="I'm implementing [Y] and need authoritative guidance to make correct API choices first try. I'll use this to follow intended patterns, not anti-patterns. Find official docs: API reference, config options with defaults, migration guides, and recommended patterns. Check for 'common mistakes' sections and GitHub issues for gotchas. Return: key API signatures, recommended config, pitfalls.", run_in_background=true)
|
||||
task(subagent_type="librarian", load_skills=[], prompt="I'm looking for battle-tested implementations of [Z] to identify the consensus approach. I'll use this to avoid reinventing the wheel. Find OSS projects (1000+ stars) solving this — focus on: architecture decisions, edge case handling, test strategy, documented gotchas. Compare 2-3 implementations for common vs project-specific patterns. Skip tutorials — production code only.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**Interview Focus:**
|
||||
@@ -281,17 +285,17 @@ delegate_task(subagent_type="librarian", prompt="I'm looking for battle-tested i
|
||||
|
||||
**For Understanding Codebase:**
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="explore", prompt="I'm working on [topic] and need to understand how it's organized in this project. Find all related files - show the structure, patterns used, and conventions I should follow.", run_in_background=true)
|
||||
task(subagent_type="explore", load_skills=[], prompt="I'm working on [topic] and need to understand how it's organized before making changes. I'll use this to match existing conventions. Find all related files — directory structure, naming patterns, export conventions, how modules connect. Compare 2-3 similar modules to identify the canonical pattern. Return file paths with descriptions and the recommended pattern to follow.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**For External Knowledge:**
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="librarian", prompt="I'm integrating [library] and need to understand [specific feature]. Find official documentation - API details, configuration options, and recommended best practices.", run_in_background=true)
|
||||
task(subagent_type="librarian", load_skills=[], prompt="I'm integrating [library] and need to understand [specific feature] for correct first-try implementation. I'll use this to follow recommended patterns. Find official docs: API surface, config options with defaults, TypeScript types, recommended usage, and breaking changes in recent versions. Check changelog if our version differs from latest. Return: API signatures, config snippets, pitfalls.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
**For Implementation Examples:**
|
||||
\`\`\`typescript
|
||||
delegate_task(subagent_type="librarian", prompt="I'm implementing [feature] and want to learn from existing solutions. Find open source implementations - focus on production-quality code, architecture decisions, and common patterns.", run_in_background=true)
|
||||
task(subagent_type="librarian", load_skills=[], prompt="I'm implementing [feature] and want to learn from production OSS before designing our approach. I'll use this to identify consensus patterns. Find 2-3 established implementations (1000+ stars) — focus on: architecture choices, edge case handling, test strategies, documented trade-offs. Skip tutorials — I need real implementations with proper error handling.", run_in_background=true)
|
||||
\`\`\`
|
||||
|
||||
## Interview Mode Anti-Patterns
|
||||
|
||||
@@ -59,8 +59,9 @@ todoWrite([
|
||||
**BEFORE generating the plan**, summon Metis to catch what you might have missed:
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
task(
|
||||
subagent_type="metis",
|
||||
load_skills=[],
|
||||
prompt=\`Review this planning session before I generate the work plan:
|
||||
|
||||
**User's Goal**: {summarize what user wants}
|
||||
|
||||
@@ -214,7 +214,7 @@ Parallel Speedup: ~40% faster than sequential
|
||||
|
||||
| Wave | Tasks | Recommended Agents |
|
||||
|------|-------|-------------------|
|
||||
| 1 | 1, 5 | delegate_task(category="...", load_skills=[...], run_in_background=false) |
|
||||
| 1 | 1, 5 | task(category="...", load_skills=[...], run_in_background=false) |
|
||||
| 2 | 2, 3, 6 | dispatch parallel after Wave 1 completes |
|
||||
| 3 | 4 | final integration task |
|
||||
|
||||
|
||||
29
src/agents/prometheus/system-prompt.ts
Normal file
29
src/agents/prometheus/system-prompt.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PROMETHEUS_IDENTITY_CONSTRAINTS } from "./identity-constraints"
|
||||
import { PROMETHEUS_INTERVIEW_MODE } from "./interview-mode"
|
||||
import { PROMETHEUS_PLAN_GENERATION } from "./plan-generation"
|
||||
import { PROMETHEUS_HIGH_ACCURACY_MODE } from "./high-accuracy-mode"
|
||||
import { PROMETHEUS_PLAN_TEMPLATE } from "./plan-template"
|
||||
import { PROMETHEUS_BEHAVIORAL_SUMMARY } from "./behavioral-summary"
|
||||
|
||||
/**
|
||||
* Combined Prometheus system prompt.
|
||||
* Assembled from modular sections for maintainability.
|
||||
*/
|
||||
export const PROMETHEUS_SYSTEM_PROMPT = `${PROMETHEUS_IDENTITY_CONSTRAINTS}
|
||||
${PROMETHEUS_INTERVIEW_MODE}
|
||||
${PROMETHEUS_PLAN_GENERATION}
|
||||
${PROMETHEUS_HIGH_ACCURACY_MODE}
|
||||
${PROMETHEUS_PLAN_TEMPLATE}
|
||||
${PROMETHEUS_BEHAVIORAL_SUMMARY}`
|
||||
|
||||
/**
|
||||
* Prometheus planner permission configuration.
|
||||
* Allows write/edit for plan files (.md only, enforced by prometheus-md-only hook).
|
||||
* Question permission allows agent to ask user questions via OpenCode's QuestionTool.
|
||||
*/
|
||||
export const PROMETHEUS_PERMISSION = {
|
||||
edit: "allow" as const,
|
||||
bash: "allow" as const,
|
||||
webfetch: "allow" as const,
|
||||
question: "allow" as const,
|
||||
}
|
||||
119
src/agents/sisyphus-junior/agent.ts
Normal file
119
src/agents/sisyphus-junior/agent.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Sisyphus-Junior - Focused Task Executor
|
||||
*
|
||||
* Executes delegated tasks directly without spawning other agents.
|
||||
* Category-spawned executor with domain-specific configurations.
|
||||
*
|
||||
* Routing:
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) -> gpt.ts (GPT-5.2 optimized)
|
||||
* 2. Default (Claude, etc.) -> default.ts (Claude-optimized)
|
||||
*/
|
||||
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode } from "../types"
|
||||
import { isGptModel } from "../types"
|
||||
import type { AgentOverrideConfig } from "../../config/schema"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
type PermissionValue,
|
||||
} from "../../shared/permission-compat"
|
||||
|
||||
import { buildDefaultSisyphusJuniorPrompt } from "./default"
|
||||
import { buildGptSisyphusJuniorPrompt } from "./gpt"
|
||||
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
// Core tools that Sisyphus-Junior must NEVER have access to
|
||||
// Note: call_omo_agent is ALLOWED so subagents can spawn explore/librarian
|
||||
const BLOCKED_TOOLS = ["task"]
|
||||
|
||||
export const SISYPHUS_JUNIOR_DEFAULTS = {
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
temperature: 0.1,
|
||||
} as const
|
||||
|
||||
export type SisyphusJuniorPromptSource = "default" | "gpt"
|
||||
|
||||
/**
|
||||
* Determines which Sisyphus-Junior prompt to use based on model.
|
||||
*/
|
||||
export function getSisyphusJuniorPromptSource(model?: string): SisyphusJuniorPromptSource {
|
||||
if (model && isGptModel(model)) {
|
||||
return "gpt"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the appropriate Sisyphus-Junior prompt based on model.
|
||||
*/
|
||||
export function buildSisyphusJuniorPrompt(
|
||||
model: string | undefined,
|
||||
useTaskSystem: boolean,
|
||||
promptAppend?: string
|
||||
): string {
|
||||
const source = getSisyphusJuniorPromptSource(model)
|
||||
|
||||
switch (source) {
|
||||
case "gpt":
|
||||
return buildGptSisyphusJuniorPrompt(useTaskSystem, promptAppend)
|
||||
case "default":
|
||||
default:
|
||||
return buildDefaultSisyphusJuniorPrompt(useTaskSystem, promptAppend)
|
||||
}
|
||||
}
|
||||
|
||||
export function createSisyphusJuniorAgentWithOverrides(
|
||||
override: AgentOverrideConfig | undefined,
|
||||
systemDefaultModel?: string,
|
||||
useTaskSystem = false
|
||||
): AgentConfig {
|
||||
if (override?.disable) {
|
||||
override = undefined
|
||||
}
|
||||
|
||||
const overrideModel = (override as { model?: string } | undefined)?.model
|
||||
const model = overrideModel ?? systemDefaultModel ?? SISYPHUS_JUNIOR_DEFAULTS.model
|
||||
const temperature = override?.temperature ?? SISYPHUS_JUNIOR_DEFAULTS.temperature
|
||||
|
||||
const promptAppend = override?.prompt_append
|
||||
const prompt = buildSisyphusJuniorPrompt(model, useTaskSystem, promptAppend)
|
||||
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
|
||||
const userPermission = (override?.permission ?? {}) as Record<string, PermissionValue>
|
||||
const basePermission = baseRestrictions.permission
|
||||
const merged: Record<string, PermissionValue> = { ...userPermission }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = "deny"
|
||||
}
|
||||
merged.call_omo_agent = "allow"
|
||||
const toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
"Focused task executor. Same discipline, no delegation. (Sisyphus-Junior - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: 64000,
|
||||
prompt,
|
||||
color: override?.color ?? "#20B2AA",
|
||||
...toolsConfig,
|
||||
}
|
||||
|
||||
if (override?.top_p !== undefined) {
|
||||
base.top_p = override.top_p
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium" } as AgentConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
createSisyphusJuniorAgentWithOverrides.mode = MODE
|
||||
@@ -7,11 +7,14 @@
|
||||
* - Extended reasoning context for complex tasks
|
||||
*/
|
||||
|
||||
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
|
||||
|
||||
export function buildDefaultSisyphusJuniorPrompt(
|
||||
useTaskSystem: boolean,
|
||||
promptAppend?: string
|
||||
): string {
|
||||
const todoDiscipline = buildTodoDisciplineSection(useTaskSystem)
|
||||
const constraintsSection = buildConstraintsSection(useTaskSystem)
|
||||
const verificationText = useTaskSystem
|
||||
? "All tasks marked completed"
|
||||
: "All todos marked completed"
|
||||
@@ -21,14 +24,7 @@ Sisyphus-Junior - Focused executor from OhMyOpenCode.
|
||||
Execute tasks directly. NEVER delegate or spawn other agents.
|
||||
</Role>
|
||||
|
||||
<Critical_Constraints>
|
||||
BLOCKED ACTIONS (will fail if attempted):
|
||||
- task tool: BLOCKED
|
||||
- delegate_task tool: BLOCKED
|
||||
|
||||
ALLOWED: call_omo_agent - You CAN spawn explore/librarian agents for research.
|
||||
You work ALONE for implementation. No delegation of implementation tasks.
|
||||
</Critical_Constraints>
|
||||
${constraintsSection}
|
||||
|
||||
${todoDiscipline}
|
||||
|
||||
@@ -46,7 +42,30 @@ Task NOT complete without:
|
||||
</Style>`
|
||||
|
||||
if (!promptAppend) return prompt
|
||||
return prompt + "\n\n" + promptAppend
|
||||
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
|
||||
}
|
||||
|
||||
function buildConstraintsSection(useTaskSystem: boolean): string {
|
||||
if (useTaskSystem) {
|
||||
return `<Critical_Constraints>
|
||||
BLOCKED ACTIONS (will fail if attempted):
|
||||
- task (agent delegation tool): BLOCKED — you cannot delegate work to other agents
|
||||
|
||||
ALLOWED tools:
|
||||
- call_omo_agent: You CAN spawn explore/librarian agents for research
|
||||
- task_create, task_update, task_list, task_get: ALLOWED — use these for tracking your work
|
||||
|
||||
You work ALONE for implementation. No delegation of implementation tasks.
|
||||
</Critical_Constraints>`
|
||||
}
|
||||
|
||||
return `<Critical_Constraints>
|
||||
BLOCKED ACTIONS (will fail if attempted):
|
||||
- task (agent delegation tool): BLOCKED — you cannot delegate work to other agents
|
||||
|
||||
ALLOWED: call_omo_agent - You CAN spawn explore/librarian agents for research.
|
||||
You work ALONE for implementation. No delegation of implementation tasks.
|
||||
</Critical_Constraints>`
|
||||
}
|
||||
|
||||
function buildTodoDisciplineSection(useTaskSystem: boolean): string {
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
* - Explicit decision criteria needed (model won't infer)
|
||||
*/
|
||||
|
||||
import { resolvePromptAppend } from "../builtin-agents/resolve-file-uri"
|
||||
|
||||
export function buildGptSisyphusJuniorPrompt(
|
||||
useTaskSystem: boolean,
|
||||
promptAppend?: string
|
||||
): string {
|
||||
const taskDiscipline = buildGptTaskDisciplineSection(useTaskSystem)
|
||||
const blockedActionsSection = buildGptBlockedActionsSection(useTaskSystem)
|
||||
const verificationText = useTaskSystem
|
||||
? "All tasks marked completed"
|
||||
: "All todos marked completed"
|
||||
@@ -45,20 +48,7 @@ Role: Execute tasks directly. You work ALONE.
|
||||
- Do NOT expand task boundaries beyond what's written.
|
||||
</scope_and_design_constraints>
|
||||
|
||||
<blocked_actions>
|
||||
BLOCKED (will fail if attempted):
|
||||
| Tool | Status |
|
||||
|------|--------|
|
||||
| task | BLOCKED |
|
||||
| delegate_task | BLOCKED |
|
||||
|
||||
ALLOWED:
|
||||
| Tool | Usage |
|
||||
|------|-------|
|
||||
| call_omo_agent | Spawn explore/librarian for research ONLY |
|
||||
|
||||
You work ALONE for implementation. No delegation.
|
||||
</blocked_actions>
|
||||
${blockedActionsSection}
|
||||
|
||||
<uncertainty_and_ambiguity>
|
||||
- If a task is ambiguous or underspecified:
|
||||
@@ -97,7 +87,43 @@ Task NOT complete without evidence:
|
||||
</style_spec>`
|
||||
|
||||
if (!promptAppend) return prompt
|
||||
return prompt + "\n\n" + promptAppend
|
||||
return prompt + "\n\n" + resolvePromptAppend(promptAppend)
|
||||
}
|
||||
|
||||
function buildGptBlockedActionsSection(useTaskSystem: boolean): string {
|
||||
if (useTaskSystem) {
|
||||
return `<blocked_actions>
|
||||
BLOCKED (will fail if attempted):
|
||||
| Tool | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| task | BLOCKED | Agent delegation tool — you cannot spawn other agents |
|
||||
|
||||
ALLOWED:
|
||||
| Tool | Usage |
|
||||
|------|-------|
|
||||
| call_omo_agent | Spawn explore/librarian for research ONLY |
|
||||
| task_create | Create tasks to track your work |
|
||||
| task_update | Update task status (in_progress, completed) |
|
||||
| task_list | List active tasks |
|
||||
| task_get | Get task details by ID |
|
||||
|
||||
You work ALONE for implementation. No delegation.
|
||||
</blocked_actions>`
|
||||
}
|
||||
|
||||
return `<blocked_actions>
|
||||
BLOCKED (will fail if attempted):
|
||||
| Tool | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| task | BLOCKED | Agent delegation tool — you cannot spawn other agents |
|
||||
|
||||
ALLOWED:
|
||||
| Tool | Usage |
|
||||
|------|-------|
|
||||
| call_omo_agent | Spawn explore/librarian for research ONLY |
|
||||
|
||||
You work ALONE for implementation. No delegation.
|
||||
</blocked_actions>`
|
||||
}
|
||||
|
||||
function buildGptTaskDisciplineSection(useTaskSystem: boolean): string {
|
||||
|
||||
@@ -143,13 +143,12 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool safety (task/delegate_task blocked, call_omo_agent allowed)", () => {
|
||||
test("task and delegate_task remain blocked, call_omo_agent is allowed via tools format", () => {
|
||||
describe("tool safety (task blocked, call_omo_agent allowed)", () => {
|
||||
test("task remains blocked, call_omo_agent is allowed via tools format", () => {
|
||||
// given
|
||||
const override = {
|
||||
tools: {
|
||||
task: true,
|
||||
delegate_task: true,
|
||||
call_omo_agent: true,
|
||||
read: true,
|
||||
},
|
||||
@@ -163,25 +162,22 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
const permission = result.permission as Record<string, string> | undefined
|
||||
if (tools) {
|
||||
expect(tools.task).toBe(false)
|
||||
expect(tools.delegate_task).toBe(false)
|
||||
// call_omo_agent is NOW ALLOWED for subagents to spawn explore/librarian
|
||||
expect(tools.call_omo_agent).toBe(true)
|
||||
expect(tools.read).toBe(true)
|
||||
}
|
||||
if (permission) {
|
||||
expect(permission.task).toBe("deny")
|
||||
expect(permission.delegate_task).toBe("deny")
|
||||
// call_omo_agent is NOW ALLOWED for subagents to spawn explore/librarian
|
||||
expect(permission.call_omo_agent).toBe("allow")
|
||||
}
|
||||
})
|
||||
|
||||
test("task and delegate_task remain blocked when using permission format override", () => {
|
||||
test("task remains blocked when using permission format override", () => {
|
||||
// given
|
||||
const override = {
|
||||
permission: {
|
||||
task: "allow",
|
||||
delegate_task: "allow",
|
||||
call_omo_agent: "allow",
|
||||
read: "allow",
|
||||
},
|
||||
@@ -190,22 +186,102 @@ describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
// when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override as Parameters<typeof createSisyphusJuniorAgentWithOverrides>[0])
|
||||
|
||||
// then - task/delegate_task blocked, but call_omo_agent allowed for explore/librarian spawning
|
||||
// then - task blocked, but call_omo_agent allowed for explore/librarian spawning
|
||||
const tools = result.tools as Record<string, boolean> | undefined
|
||||
const permission = result.permission as Record<string, string> | undefined
|
||||
if (tools) {
|
||||
expect(tools.task).toBe(false)
|
||||
expect(tools.delegate_task).toBe(false)
|
||||
expect(tools.call_omo_agent).toBe(true)
|
||||
}
|
||||
if (permission) {
|
||||
expect(permission.task).toBe("deny")
|
||||
expect(permission.delegate_task).toBe("deny")
|
||||
expect(permission.call_omo_agent).toBe("allow")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("useTaskSystem integration", () => {
|
||||
test("useTaskSystem=true produces Task_Discipline prompt for Claude", () => {
|
||||
//#given
|
||||
const override = { model: "anthropic/claude-sonnet-4-5" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
|
||||
|
||||
//#then
|
||||
expect(result.prompt).toContain("TaskCreate")
|
||||
expect(result.prompt).toContain("TaskUpdate")
|
||||
expect(result.prompt).not.toContain("todowrite")
|
||||
})
|
||||
|
||||
test("useTaskSystem=true produces task_discipline_spec prompt for GPT", () => {
|
||||
//#given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
|
||||
|
||||
//#then
|
||||
expect(result.prompt).toContain("<task_discipline_spec>")
|
||||
expect(result.prompt).toContain("TaskCreate")
|
||||
expect(result.prompt).not.toContain("<todo_discipline_spec>")
|
||||
})
|
||||
|
||||
test("useTaskSystem=false (default) produces Todo_Discipline prompt", () => {
|
||||
//#given
|
||||
const override = {}
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
//#then
|
||||
expect(result.prompt).toContain("todowrite")
|
||||
expect(result.prompt).not.toContain("TaskCreate")
|
||||
})
|
||||
|
||||
test("useTaskSystem=true explicitly lists task management tools as ALLOWED for Claude", () => {
|
||||
//#given
|
||||
const override = { model: "anthropic/claude-sonnet-4-5" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
|
||||
|
||||
//#then - prompt must disambiguate: delegation tool blocked, management tools allowed
|
||||
expect(result.prompt).toContain("task_create")
|
||||
expect(result.prompt).toContain("task_update")
|
||||
expect(result.prompt).toContain("task_list")
|
||||
expect(result.prompt).toContain("task_get")
|
||||
expect(result.prompt).toContain("agent delegation tool")
|
||||
})
|
||||
|
||||
test("useTaskSystem=true explicitly lists task management tools as ALLOWED for GPT", () => {
|
||||
//#given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, true)
|
||||
|
||||
//#then - prompt must disambiguate: delegation tool blocked, management tools allowed
|
||||
expect(result.prompt).toContain("task_create")
|
||||
expect(result.prompt).toContain("task_update")
|
||||
expect(result.prompt).toContain("task_list")
|
||||
expect(result.prompt).toContain("task_get")
|
||||
expect(result.prompt).toContain("Agent delegation tool")
|
||||
})
|
||||
|
||||
test("useTaskSystem=false does NOT list task management tools in constraints", () => {
|
||||
//#given - Claude model without task system
|
||||
const override = { model: "anthropic/claude-sonnet-4-5" }
|
||||
|
||||
//#when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override, undefined, false)
|
||||
|
||||
//#then - no task management tool references in constraints section
|
||||
expect(result.prompt).not.toContain("task_create")
|
||||
expect(result.prompt).not.toContain("task_update")
|
||||
})
|
||||
})
|
||||
|
||||
describe("prompt composition", () => {
|
||||
test("base prompt contains discipline constraints", () => {
|
||||
// given
|
||||
|
||||
@@ -1,121 +1,10 @@
|
||||
/**
|
||||
* Sisyphus-Junior - Focused Task Executor
|
||||
*
|
||||
* Executes delegated tasks directly without spawning other agents.
|
||||
* Category-spawned executor with domain-specific configurations.
|
||||
*
|
||||
* Routing:
|
||||
* 1. GPT models (openai/*, github-copilot/gpt-*) -> gpt.ts (GPT-5.2 optimized)
|
||||
* 2. Default (Claude, etc.) -> default.ts (Claude-optimized)
|
||||
*/
|
||||
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode } from "../types"
|
||||
import { isGptModel } from "../types"
|
||||
import type { AgentOverrideConfig } from "../../config/schema"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
type PermissionValue,
|
||||
} from "../../shared/permission-compat"
|
||||
|
||||
import { buildDefaultSisyphusJuniorPrompt } from "./default"
|
||||
import { buildGptSisyphusJuniorPrompt } from "./gpt"
|
||||
|
||||
export { buildDefaultSisyphusJuniorPrompt } from "./default"
|
||||
export { buildGptSisyphusJuniorPrompt } from "./gpt"
|
||||
|
||||
const MODE: AgentMode = "subagent"
|
||||
|
||||
// Core tools that Sisyphus-Junior must NEVER have access to
|
||||
// Note: call_omo_agent is ALLOWED so subagents can spawn explore/librarian
|
||||
const BLOCKED_TOOLS = ["task", "delegate_task"]
|
||||
|
||||
export const SISYPHUS_JUNIOR_DEFAULTS = {
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
temperature: 0.1,
|
||||
} as const
|
||||
|
||||
export type SisyphusJuniorPromptSource = "default" | "gpt"
|
||||
|
||||
/**
|
||||
* Determines which Sisyphus-Junior prompt to use based on model.
|
||||
*/
|
||||
export function getSisyphusJuniorPromptSource(model?: string): SisyphusJuniorPromptSource {
|
||||
if (model && isGptModel(model)) {
|
||||
return "gpt"
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the appropriate Sisyphus-Junior prompt based on model.
|
||||
*/
|
||||
export function buildSisyphusJuniorPrompt(
|
||||
model: string | undefined,
|
||||
useTaskSystem: boolean,
|
||||
promptAppend?: string
|
||||
): string {
|
||||
const source = getSisyphusJuniorPromptSource(model)
|
||||
|
||||
switch (source) {
|
||||
case "gpt":
|
||||
return buildGptSisyphusJuniorPrompt(useTaskSystem, promptAppend)
|
||||
case "default":
|
||||
default:
|
||||
return buildDefaultSisyphusJuniorPrompt(useTaskSystem, promptAppend)
|
||||
}
|
||||
}
|
||||
|
||||
export function createSisyphusJuniorAgentWithOverrides(
|
||||
override: AgentOverrideConfig | undefined,
|
||||
systemDefaultModel?: string,
|
||||
useTaskSystem = false
|
||||
): AgentConfig {
|
||||
if (override?.disable) {
|
||||
override = undefined
|
||||
}
|
||||
|
||||
const model = override?.model ?? systemDefaultModel ?? SISYPHUS_JUNIOR_DEFAULTS.model
|
||||
const temperature = override?.temperature ?? SISYPHUS_JUNIOR_DEFAULTS.temperature
|
||||
|
||||
const promptAppend = override?.prompt_append
|
||||
const prompt = buildSisyphusJuniorPrompt(model, useTaskSystem, promptAppend)
|
||||
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
|
||||
const userPermission = (override?.permission ?? {}) as Record<string, PermissionValue>
|
||||
const basePermission = baseRestrictions.permission
|
||||
const merged: Record<string, PermissionValue> = { ...userPermission }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = "deny"
|
||||
}
|
||||
merged.call_omo_agent = "allow"
|
||||
const toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
"Focused task executor. Same discipline, no delegation. (Sisyphus-Junior - OhMyOpenCode)",
|
||||
mode: MODE,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: 64000,
|
||||
prompt,
|
||||
color: override?.color ?? "#20B2AA",
|
||||
...toolsConfig,
|
||||
}
|
||||
|
||||
if (override?.top_p !== undefined) {
|
||||
base.top_p = override.top_p
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium" } as AgentConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
createSisyphusJuniorAgentWithOverrides.mode = MODE
|
||||
export {
|
||||
SISYPHUS_JUNIOR_DEFAULTS,
|
||||
getSisyphusJuniorPromptSource,
|
||||
buildSisyphusJuniorPrompt,
|
||||
createSisyphusJuniorAgentWithOverrides,
|
||||
} from "./agent"
|
||||
export type { SisyphusJuniorPromptSource } from "./agent"
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk";
|
||||
import type { AgentMode, AgentPromptMetadata } from "./types";
|
||||
import { isGptModel } from "./types";
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
const MODE: AgentMode = "primary";
|
||||
export const SISYPHUS_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "utility",
|
||||
cost: "EXPENSIVE",
|
||||
promptAlias: "Sisyphus",
|
||||
triggers: [],
|
||||
}
|
||||
import type { AvailableAgent, AvailableTool, AvailableSkill, AvailableCategory } from "./dynamic-agent-prompt-builder"
|
||||
};
|
||||
import type {
|
||||
AvailableAgent,
|
||||
AvailableTool,
|
||||
AvailableSkill,
|
||||
AvailableCategory,
|
||||
} from "./dynamic-agent-prompt-builder";
|
||||
import {
|
||||
buildKeyTriggersSection,
|
||||
buildToolSelectionTable,
|
||||
@@ -21,7 +26,7 @@ import {
|
||||
buildHardBlocksSection,
|
||||
buildAntiPatternsSection,
|
||||
categorizeTools,
|
||||
} from "./dynamic-agent-prompt-builder"
|
||||
} from "./dynamic-agent-prompt-builder";
|
||||
|
||||
function buildTaskManagementSection(useTaskSystem: boolean): string {
|
||||
if (useTaskSystem) {
|
||||
@@ -80,7 +85,7 @@ I want to make sure I understand correctly.
|
||||
|
||||
Should I proceed with [recommendation], or would you prefer differently?
|
||||
\`\`\`
|
||||
</Task_Management>`
|
||||
</Task_Management>`;
|
||||
}
|
||||
|
||||
return `<Task_Management>
|
||||
@@ -138,7 +143,7 @@ I want to make sure I understand correctly.
|
||||
|
||||
Should I proceed with [recommendation], or would you prefer differently?
|
||||
\`\`\`
|
||||
</Task_Management>`
|
||||
</Task_Management>`;
|
||||
}
|
||||
|
||||
function buildDynamicSisyphusPrompt(
|
||||
@@ -146,21 +151,28 @@ function buildDynamicSisyphusPrompt(
|
||||
availableTools: AvailableTool[] = [],
|
||||
availableSkills: AvailableSkill[] = [],
|
||||
availableCategories: AvailableCategory[] = [],
|
||||
useTaskSystem = false
|
||||
useTaskSystem = false,
|
||||
): 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 taskManagementSection = buildTaskManagementSection(useTaskSystem)
|
||||
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 taskManagementSection = buildTaskManagementSection(useTaskSystem);
|
||||
const todoHookNote = useTaskSystem
|
||||
? "YOUR TASK CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TASK CONTINUATION])"
|
||||
: "YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION])"
|
||||
: "YOUR TODO CREATION WOULD BE TRACKED BY HOOK([SYSTEM REMINDER - TODO CONTINUATION])";
|
||||
|
||||
return `<Role>
|
||||
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
|
||||
@@ -214,8 +226,8 @@ ${keyTriggers}
|
||||
|
||||
**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.
|
||||
2. If not, is there a \`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: \`task(load_skills=[{skill1}, ...])\` MUST PASS SKILL AS 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.**
|
||||
@@ -275,17 +287,23 @@ ${librarianSection}
|
||||
|
||||
\`\`\`typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Prompt structure: [CONTEXT: what I'm doing] + [GOAL: what I'm trying to achieve] + [QUESTION: what I need to know] + [REQUEST: what to find]
|
||||
// Prompt structure (each field should be substantive, not a single sentence):
|
||||
// [CONTEXT]: What task I'm working on, which files/modules are involved, and what approach I'm taking
|
||||
// [GOAL]: The specific outcome I need — what decision or action the results will unblock
|
||||
// [DOWNSTREAM]: How I will use the results — what I'll build/decide based on what's found
|
||||
// [REQUEST]: Concrete search instructions — what to find, what format to return, and what to SKIP
|
||||
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="I'm implementing user authentication for our API. I need to understand how auth is currently structured in this codebase. Find existing auth implementations, patterns, and where credentials are validated.")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="I'm adding error handling to the auth flow. I want to follow existing project conventions for consistency. Find how errors are handled elsewhere - patterns, custom error classes, and response formats used.")
|
||||
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find auth implementations", prompt="I'm implementing JWT auth for the REST API in src/api/routes/. I need to match existing auth conventions so my code fits seamlessly. I'll use this to decide middleware structure and token flow. Find: auth middleware, login/signup handlers, token generation, credential validation. Focus on src/ — skip tests. Return file paths with pattern descriptions.")
|
||||
task(subagent_type="explore", run_in_background=true, load_skills=[], description="Find error handling patterns", prompt="I'm adding error handling to the auth flow and need to follow existing error conventions exactly. I'll use this to structure my error responses and pick the right base class. Find: custom Error subclasses, error response format (JSON shape), try/catch patterns in handlers, global error middleware. Skip test files. Return the error class hierarchy and response format.")
|
||||
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="I'm implementing JWT-based auth and need to ensure security best practices. Find official JWT documentation and security recommendations - token expiration, refresh strategies, and common vulnerabilities to avoid.")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="I'm building Express middleware for auth and want production-quality patterns. Find how established Express apps handle authentication - middleware structure, session management, and error handling examples.")
|
||||
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find JWT security docs", prompt="I'm implementing JWT auth and need current security best practices to choose token storage (httpOnly cookies vs localStorage) and set expiration policy. Find: OWASP auth guidelines, recommended token lifetimes, refresh token rotation strategies, common JWT vulnerabilities. Skip 'what is JWT' tutorials — production security guidance only.")
|
||||
task(subagent_type="librarian", run_in_background=true, load_skills=[], description="Find Express auth patterns", prompt="I'm building Express auth middleware and need production-quality patterns to structure my middleware chain. Find how established Express apps (1000+ stars) handle: middleware ordering, token refresh, role-based access control, auth error propagation. Skip basic tutorials — I need battle-tested patterns with proper error handling.")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
result = delegate_task(..., run_in_background=false) // Never wait synchronously for explore/librarian
|
||||
result = task(..., run_in_background=false) // Never wait synchronously for explore/librarian
|
||||
\`\`\`
|
||||
|
||||
### Background Result Collection:
|
||||
@@ -309,6 +327,7 @@ STOP searching when:
|
||||
## Phase 2B - Implementation
|
||||
|
||||
### Pre-Implementation:
|
||||
0. Find relevant skills that you can load, and load them IMMEDIATELY.
|
||||
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
|
||||
@@ -340,7 +359,7 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
|
||||
### Session Continuity (MANDATORY)
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
Every \`task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
**ALWAYS continue when:**
|
||||
| Scenario | Action |
|
||||
@@ -358,10 +377,10 @@ Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
\`\`\`typescript
|
||||
// WRONG: Starting fresh loses all context
|
||||
delegate_task(category="quick", load_skills=[], run_in_background=false, prompt="Fix the type error in auth.ts...")
|
||||
task(category="quick", load_skills=[], run_in_background=false, description="Fix type error", prompt="Fix the type error in auth.ts...")
|
||||
|
||||
// CORRECT: Resume preserves everything
|
||||
delegate_task(session_id="ses_abc123", prompt="Fix: Type error on line 42")
|
||||
task(session_id="ses_abc123", load_skills=[], run_in_background=false, description="Fix type error", prompt="Fix: Type error on line 42")
|
||||
\`\`\`
|
||||
|
||||
**After EVERY delegation, STORE the session_id for potential continuation.**
|
||||
@@ -491,7 +510,7 @@ ${antiPatterns}
|
||||
- Prefer small, focused changes over large refactors
|
||||
- When uncertain about scope, ask
|
||||
</Constraints>
|
||||
`
|
||||
`;
|
||||
}
|
||||
|
||||
export function createSisyphusAgent(
|
||||
@@ -500,16 +519,25 @@ export function createSisyphusAgent(
|
||||
availableToolNames?: string[],
|
||||
availableSkills?: AvailableSkill[],
|
||||
availableCategories?: AvailableCategory[],
|
||||
useTaskSystem = false
|
||||
useTaskSystem = false,
|
||||
): AgentConfig {
|
||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : []
|
||||
const skills = availableSkills ?? []
|
||||
const categories = availableCategories ?? []
|
||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : [];
|
||||
const skills = availableSkills ?? [];
|
||||
const categories = availableCategories ?? [];
|
||||
const prompt = availableAgents
|
||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills, categories, useTaskSystem)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills, categories, useTaskSystem)
|
||||
? buildDynamicSisyphusPrompt(
|
||||
availableAgents,
|
||||
tools,
|
||||
skills,
|
||||
categories,
|
||||
useTaskSystem,
|
||||
)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills, categories, useTaskSystem);
|
||||
|
||||
const permission = { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"]
|
||||
const permission = {
|
||||
question: "allow",
|
||||
call_omo_agent: "deny",
|
||||
} as AgentConfig["permission"];
|
||||
const base = {
|
||||
description:
|
||||
"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)",
|
||||
@@ -519,12 +547,12 @@ export function createSisyphusAgent(
|
||||
prompt,
|
||||
color: "#00CED1",
|
||||
permission,
|
||||
}
|
||||
};
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium" }
|
||||
return { ...base, reasoningEffort: "medium" };
|
||||
}
|
||||
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } };
|
||||
}
|
||||
createSisyphusAgent.mode = MODE
|
||||
createSisyphusAgent.mode = MODE;
|
||||
|
||||
49
src/agents/types.test.ts
Normal file
49
src/agents/types.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { isGptModel } from "./types";
|
||||
|
||||
describe("isGptModel", () => {
|
||||
test("standard openai provider models", () => {
|
||||
expect(isGptModel("openai/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("openai/gpt-4o")).toBe(true);
|
||||
expect(isGptModel("openai/o1")).toBe(true);
|
||||
expect(isGptModel("openai/o3-mini")).toBe(true);
|
||||
});
|
||||
|
||||
test("github copilot gpt models", () => {
|
||||
expect(isGptModel("github-copilot/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("github-copilot/gpt-4o")).toBe(true);
|
||||
});
|
||||
|
||||
test("litellm proxied gpt models", () => {
|
||||
expect(isGptModel("litellm/gpt-5.2")).toBe(true);
|
||||
expect(isGptModel("litellm/gpt-4o")).toBe(true);
|
||||
expect(isGptModel("litellm/o1")).toBe(true);
|
||||
expect(isGptModel("litellm/o3-mini")).toBe(true);
|
||||
expect(isGptModel("litellm/o4-mini")).toBe(true);
|
||||
});
|
||||
|
||||
test("other proxied gpt models", () => {
|
||||
expect(isGptModel("ollama/gpt-4o")).toBe(true);
|
||||
expect(isGptModel("custom-provider/gpt-5.2")).toBe(true);
|
||||
});
|
||||
|
||||
test("gpt4 prefix without hyphen (legacy naming)", () => {
|
||||
expect(isGptModel("litellm/gpt4o")).toBe(true);
|
||||
expect(isGptModel("ollama/gpt4")).toBe(true);
|
||||
});
|
||||
|
||||
test("claude models are not gpt", () => {
|
||||
expect(isGptModel("anthropic/claude-opus-4-6")).toBe(false);
|
||||
expect(isGptModel("anthropic/claude-sonnet-4-5")).toBe(false);
|
||||
expect(isGptModel("litellm/anthropic.claude-opus-4-5")).toBe(false);
|
||||
});
|
||||
|
||||
test("gemini models are not gpt", () => {
|
||||
expect(isGptModel("google/gemini-3-pro")).toBe(false);
|
||||
expect(isGptModel("litellm/gemini-3-pro")).toBe(false);
|
||||
});
|
||||
|
||||
test("opencode provider is not gpt", () => {
|
||||
expect(isGptModel("opencode/claude-opus-4-6")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -66,8 +66,18 @@ export interface AgentPromptMetadata {
|
||||
keyTrigger?: string
|
||||
}
|
||||
|
||||
function extractModelName(model: string): string {
|
||||
return model.includes("/") ? model.split("/").pop() ?? model : model
|
||||
}
|
||||
|
||||
const GPT_MODEL_PREFIXES = ["gpt-", "gpt4", "o1", "o3", "o4"]
|
||||
|
||||
export function isGptModel(model: string): boolean {
|
||||
return model.startsWith("openai/") || model.startsWith("github-copilot/gpt-")
|
||||
if (model.startsWith("openai/") || model.startsWith("github-copilot/gpt-"))
|
||||
return true
|
||||
|
||||
const modelName = extractModelName(model).toLowerCase()
|
||||
return GPT_MODEL_PREFIXES.some((prefix) => modelName.startsWith(prefix))
|
||||
}
|
||||
|
||||
export type BuiltinAgentName =
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import { createBuiltinAgents } from "./utils"
|
||||
import { createBuiltinAgents } from "./builtin-agents"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { clearSkillCache } from "../features/opencode-skill-loader/skill-content"
|
||||
import * as connectedProvidersCache from "../shared/connected-providers-cache"
|
||||
@@ -79,6 +81,72 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("user config model takes priority over uiSelectedModel for sisyphus", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-5"])
|
||||
)
|
||||
const uiSelectedModel = "openai/gpt-5.2"
|
||||
const overrides = {
|
||||
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||
}
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
overrides,
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
uiSelectedModel
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("google/antigravity-claude-opus-4-5-thinking")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("user config model takes priority over uiSelectedModel for atlas", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2", "anthropic/claude-sonnet-4-5"])
|
||||
)
|
||||
const uiSelectedModel = "openai/gpt-5.2"
|
||||
const overrides = {
|
||||
atlas: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||
}
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
overrides,
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
uiSelectedModel
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.atlas).toBeDefined()
|
||||
expect(agents.atlas.model).toBe("google/antigravity-claude-opus-4-5-thinking")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("Sisyphus is created on first run when no availableModels or cache exist", async () => {
|
||||
// #given
|
||||
const systemDefaultModel = "anthropic/claude-opus-4-6"
|
||||
@@ -183,6 +251,222 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
expect(agents.sisyphus.prompt).toContain("frontend-ui-ux")
|
||||
expect(agents.sisyphus.prompt).toContain("git-master")
|
||||
})
|
||||
|
||||
test("includes custom agents in orchestrator prompts when provided via config", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set([
|
||||
"anthropic/claude-opus-4-6",
|
||||
"kimi-for-coding/k2p5",
|
||||
"opencode/kimi-k2.5-free",
|
||||
"zai-coding-plan/glm-4.7",
|
||||
"opencode/glm-4.7-free",
|
||||
"openai/gpt-5.2",
|
||||
])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
{
|
||||
name: "researcher",
|
||||
description: "Research agent for deep analysis",
|
||||
hidden: false,
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
customAgentSummaries
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).toContain("researcher")
|
||||
expect(agents.hephaestus.prompt).toContain("researcher")
|
||||
expect(agents.atlas.prompt).toContain("researcher")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("excludes hidden custom agents from orchestrator prompts", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
{
|
||||
name: "hidden-agent",
|
||||
description: "Should never show",
|
||||
hidden: true,
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
customAgentSummaries
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).not.toContain("hidden-agent")
|
||||
expect(agents.hephaestus.prompt).not.toContain("hidden-agent")
|
||||
expect(agents.atlas.prompt).not.toContain("hidden-agent")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("excludes disabled custom agents from orchestrator prompts", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
{
|
||||
name: "disabled-agent",
|
||||
description: "Should never show",
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
customAgentSummaries
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).not.toContain("disabled-agent")
|
||||
expect(agents.hephaestus.prompt).not.toContain("disabled-agent")
|
||||
expect(agents.atlas.prompt).not.toContain("disabled-agent")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("excludes custom agents when disabledAgents contains their name (case-insensitive)", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
)
|
||||
|
||||
const disabledAgents = ["ReSeArChEr"]
|
||||
const customAgentSummaries = [
|
||||
{
|
||||
name: "researcher",
|
||||
description: "Should never show",
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
disabledAgents,
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
customAgentSummaries
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).not.toContain("researcher")
|
||||
expect(agents.hephaestus.prompt).not.toContain("researcher")
|
||||
expect(agents.atlas.prompt).not.toContain("researcher")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("deduplicates custom agents case-insensitively", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
{ name: "Researcher", description: "First" },
|
||||
{ name: "researcher", description: "Second" },
|
||||
]
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
customAgentSummaries
|
||||
)
|
||||
|
||||
// #then
|
||||
const matches = agents.sisyphus.prompt.match(/Custom agent: researcher/gi) ?? []
|
||||
expect(matches.length).toBe(1)
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("sanitizes custom agent strings for markdown tables", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-6", "openai/gpt-5.2"])
|
||||
)
|
||||
|
||||
const customAgentSummaries = [
|
||||
{
|
||||
name: "table-agent",
|
||||
description: "Line1\nAlpha | Beta",
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
TEST_DEFAULT_MODEL,
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
customAgentSummaries
|
||||
)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).toContain("Line1 Alpha \\| Beta")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
@@ -422,10 +706,62 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
cacheSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("sisyphus uses user-configured plugin model even when not in cache or fallback chain", async () => {
|
||||
// #given - user configures a model from a plugin provider (like antigravity)
|
||||
// that is NOT in the availableModels cache and NOT in the fallback chain
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2"])
|
||||
)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(
|
||||
["openai"]
|
||||
)
|
||||
const overrides = {
|
||||
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||
}
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("google/antigravity-claude-opus-4-5-thinking")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
cacheSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("sisyphus uses user-configured plugin model when availableModels is empty but cache exists", async () => {
|
||||
// #given - connected providers cache exists but models cache is empty
|
||||
// This reproduces the exact scenario where provider-models.json has models: {}
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set()
|
||||
)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(
|
||||
["google", "openai", "opencode"]
|
||||
)
|
||||
const overrides = {
|
||||
sisyphus: { model: "google/antigravity-claude-opus-4-5-thinking" },
|
||||
}
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("google/antigravity-claude-opus-4-5-thinking")
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
cacheSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildAgent with category and skills", () => {
|
||||
const { buildAgent } = require("./utils")
|
||||
const { buildAgent } = require("./agent-builder")
|
||||
const TEST_MODEL = "anthropic/claude-opus-4-6"
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -873,4 +1209,29 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", (
|
||||
fetchSpy.mockRestore?.()
|
||||
cacheSpy.mockRestore?.()
|
||||
})
|
||||
test("Hephaestus variant override respects user config over hardcoded default", async () => {
|
||||
// #given - user provides variant in config
|
||||
const overrides = {
|
||||
hephaestus: { variant: "high" },
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - user variant takes precedence over hardcoded "medium"
|
||||
expect(agents.hephaestus).toBeDefined()
|
||||
expect(agents.hephaestus.variant).toBe("high")
|
||||
})
|
||||
|
||||
test("Hephaestus uses default variant when no user override provided", async () => {
|
||||
// #given - no variant override in config
|
||||
const overrides = {}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - default "medium" variant is applied
|
||||
expect(agents.hephaestus).toBeDefined()
|
||||
expect(agents.hephaestus.variant).toBe("medium")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,485 +0,0 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
|
||||
import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
|
||||
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 { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
|
||||
import { createMetisAgent, metisPromptMetadata } from "./metis"
|
||||
import { createAtlasAgent, atlasPromptMetadata } from "./atlas"
|
||||
import { createMomusAgent, momusPromptMetadata } from "./momus"
|
||||
import { createHephaestusAgent } from "./hephaestus"
|
||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||
import { deepMerge, fetchAvailableModels, resolveModelPipeline, AGENT_MODEL_REQUIREMENTS, readConnectedProvidersCache, isModelAvailable, isAnyFallbackModelAvailable, isAnyProviderConnected, migrateAgentConfig } 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,
|
||||
hephaestus: createHephaestusAgent,
|
||||
oracle: createOracleAgent,
|
||||
librarian: createLibrarianAgent,
|
||||
explore: createExploreAgent,
|
||||
"multimodal-looker": createMultimodalLookerAgent,
|
||||
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,
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for each agent, used to build Sisyphus's dynamic prompt sections
|
||||
* (Delegation Table, Tool Selection, Key Triggers, etc.)
|
||||
*/
|
||||
const agentMetadata: Partial<Record<BuiltinAgentName, AgentPromptMetadata>> = {
|
||||
oracle: ORACLE_PROMPT_METADATA,
|
||||
librarian: LIBRARIAN_PROMPT_METADATA,
|
||||
explore: EXPLORE_PROMPT_METADATA,
|
||||
"multimodal-looker": MULTIMODAL_LOOKER_PROMPT_METADATA,
|
||||
metis: metisPromptMetadata,
|
||||
momus: momusPromptMetadata,
|
||||
atlas: atlasPromptMetadata,
|
||||
}
|
||||
|
||||
function isFactory(source: AgentSource): source is AgentFactory {
|
||||
return typeof source === "function"
|
||||
}
|
||||
|
||||
export function buildAgent(
|
||||
source: AgentSource,
|
||||
model: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
disabledSkills?: Set<string>
|
||||
): AgentConfig {
|
||||
const base = isFactory(source) ? source(model) : source
|
||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
|
||||
if (agentWithCategory.category) {
|
||||
const categoryConfig = categoryConfigs[agentWithCategory.category]
|
||||
if (categoryConfig) {
|
||||
if (!base.model) {
|
||||
base.model = categoryConfig.model
|
||||
}
|
||||
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
||||
base.temperature = categoryConfig.temperature
|
||||
}
|
||||
if (base.variant === undefined && categoryConfig.variant !== undefined) {
|
||||
base.variant = categoryConfig.variant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (agentWithCategory.skills?.length) {
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider, disabledSkills })
|
||||
if (resolved.size > 0) {
|
||||
const skillContent = Array.from(resolved.values()).join("\n\n")
|
||||
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates OmO-specific environment context (time, timezone, locale).
|
||||
* Note: Working directory, platform, and date are already provided by OpenCode's system.ts,
|
||||
* so we only include fields that OpenCode doesn't provide to avoid duplication.
|
||||
* See: https://github.com/code-yeongyu/oh-my-opencode/issues/379
|
||||
*/
|
||||
export function createEnvContext(): string {
|
||||
const now = new Date()
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
||||
|
||||
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",
|
||||
hour12: true,
|
||||
})
|
||||
|
||||
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 applyModelResolution(input: {
|
||||
uiSelectedModel?: string
|
||||
userModel?: string
|
||||
requirement?: { fallbackChain?: { providers: string[]; model: string; variant?: string }[] }
|
||||
availableModels: Set<string>
|
||||
systemDefaultModel?: string
|
||||
}) {
|
||||
const { uiSelectedModel, userModel, requirement, availableModels, systemDefaultModel } = input
|
||||
return resolveModelPipeline({
|
||||
intent: { uiSelectedModel, userModel },
|
||||
constraints: { availableModels },
|
||||
policy: { fallbackChain: requirement?.fallbackChain, systemDefaultModel },
|
||||
})
|
||||
}
|
||||
|
||||
function getFirstFallbackModel(requirement?: {
|
||||
fallbackChain?: { providers: string[]; model: string; variant?: string }[]
|
||||
}) {
|
||||
const entry = requirement?.fallbackChain?.[0]
|
||||
if (!entry || entry.providers.length === 0) return undefined
|
||||
return {
|
||||
model: `${entry.providers[0]}/${entry.model}`,
|
||||
provenance: "provider-fallback" as const,
|
||||
variant: entry.variant,
|
||||
}
|
||||
}
|
||||
|
||||
function applyEnvironmentContext(config: AgentConfig, directory?: string): AgentConfig {
|
||||
if (!directory || !config.prompt) return config
|
||||
const envContext = createEnvContext()
|
||||
return { ...config, prompt: config.prompt + envContext }
|
||||
}
|
||||
|
||||
function applyOverrides(
|
||||
config: AgentConfig,
|
||||
override: AgentOverrideConfig | undefined,
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
): AgentConfig {
|
||||
let result = config
|
||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (overrideCategory) {
|
||||
result = applyCategoryOverride(result, overrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (override) {
|
||||
result = mergeAgentConfig(result, override)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function mergeAgentConfig(
|
||||
base: AgentConfig,
|
||||
override: AgentOverrideConfig
|
||||
): AgentConfig {
|
||||
const migratedOverride = migrateAgentConfig(override as Record<string, unknown>) as AgentOverrideConfig
|
||||
const { prompt_append, ...rest } = migratedOverride
|
||||
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
||||
|
||||
if (prompt_append && merged.prompt) {
|
||||
merged.prompt = merged.prompt + "\n" + prompt_append
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
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,
|
||||
discoveredSkills: LoadedSkill[] = [],
|
||||
client?: any,
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
uiSelectedModel?: string,
|
||||
disabledSkills?: Set<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 isFirstRunNoCache =
|
||||
availableModels.size === 0 && (!connectedProviders || connectedProviders.length === 0)
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const availableAgents: AvailableAgent[] = []
|
||||
|
||||
const mergedCategories = categories
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
const availableCategories: AvailableCategory[] = Object.entries(mergedCategories).map(([name]) => ({
|
||||
name,
|
||||
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
|
||||
}))
|
||||
|
||||
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills })
|
||||
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
|
||||
|
||||
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
location: "plugin" as const,
|
||||
}))
|
||||
|
||||
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]
|
||||
|
||||
// Collect general agents first (for availableAgents), but don't add to result yet
|
||||
const pendingAgentConfigs: Map<string, AgentConfig> = new Map()
|
||||
|
||||
for (const [name, source] of Object.entries(agentSources)) {
|
||||
const agentName = name as BuiltinAgentName
|
||||
|
||||
if (agentName === "sisyphus") continue
|
||||
if (agentName === "hephaestus") continue
|
||||
if (agentName === "atlas") continue
|
||||
if (disabledAgents.some((name) => name.toLowerCase() === agentName.toLowerCase())) continue
|
||||
|
||||
const override = agentOverrides[agentName]
|
||||
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||
|
||||
// Check if agent requires a specific model
|
||||
if (requirement?.requiresModel && availableModels) {
|
||||
if (!isModelAvailable(requirement.requiresModel, availableModels)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
|
||||
|
||||
const resolution = applyModelResolution({
|
||||
uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined,
|
||||
userModel: override?.model,
|
||||
requirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
if (!resolution) continue
|
||||
const { model, variant: resolvedVariant } = resolution
|
||||
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills)
|
||||
|
||||
// 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") {
|
||||
config = applyEnvironmentContext(config, directory)
|
||||
}
|
||||
|
||||
config = applyOverrides(config, override, mergedCategories)
|
||||
|
||||
// Store for later - will be added after sisyphus and hephaestus
|
||||
pendingAgentConfigs.set(name, config)
|
||||
|
||||
const metadata = agentMetadata[agentName]
|
||||
if (metadata) {
|
||||
availableAgents.push({
|
||||
name: agentName,
|
||||
description: config.description ?? "",
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sisyphusOverride = agentOverrides["sisyphus"]
|
||||
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||
const hasSisyphusExplicitConfig = sisyphusOverride !== undefined
|
||||
const meetsSisyphusAnyModelRequirement =
|
||||
!sisyphusRequirement?.requiresAnyModel ||
|
||||
hasSisyphusExplicitConfig ||
|
||||
isFirstRunNoCache ||
|
||||
isAnyFallbackModelAvailable(sisyphusRequirement.fallbackChain, availableModels)
|
||||
|
||||
if (!disabledAgents.includes("sisyphus") && meetsSisyphusAnyModelRequirement) {
|
||||
let sisyphusResolution = applyModelResolution({
|
||||
uiSelectedModel,
|
||||
userModel: sisyphusOverride?.model,
|
||||
requirement: sisyphusRequirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (isFirstRunNoCache && !sisyphusOverride?.model && !uiSelectedModel) {
|
||||
sisyphusResolution = getFirstFallbackModel(sisyphusRequirement)
|
||||
}
|
||||
|
||||
if (sisyphusResolution) {
|
||||
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
|
||||
|
||||
let sisyphusConfig = createSisyphusAgent(
|
||||
sisyphusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories
|
||||
)
|
||||
|
||||
if (sisyphusResolvedVariant) {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
|
||||
sisyphusConfig = applyOverrides(sisyphusConfig, sisyphusOverride, mergedCategories)
|
||||
sisyphusConfig = applyEnvironmentContext(sisyphusConfig, directory)
|
||||
|
||||
result["sisyphus"] = sisyphusConfig
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabledAgents.includes("hephaestus")) {
|
||||
const hephaestusOverride = agentOverrides["hephaestus"]
|
||||
const hephaestusRequirement = AGENT_MODEL_REQUIREMENTS["hephaestus"]
|
||||
const hasHephaestusExplicitConfig = hephaestusOverride !== undefined
|
||||
|
||||
const hasRequiredProvider =
|
||||
!hephaestusRequirement?.requiresProvider ||
|
||||
hasHephaestusExplicitConfig ||
|
||||
isFirstRunNoCache ||
|
||||
isAnyProviderConnected(hephaestusRequirement.requiresProvider, availableModels)
|
||||
|
||||
if (hasRequiredProvider) {
|
||||
let hephaestusResolution = applyModelResolution({
|
||||
userModel: hephaestusOverride?.model,
|
||||
requirement: hephaestusRequirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (isFirstRunNoCache && !hephaestusOverride?.model) {
|
||||
hephaestusResolution = getFirstFallbackModel(hephaestusRequirement)
|
||||
}
|
||||
|
||||
if (hephaestusResolution) {
|
||||
const { model: hephaestusModel, variant: hephaestusResolvedVariant } = hephaestusResolution
|
||||
|
||||
let hephaestusConfig = createHephaestusAgent(
|
||||
hephaestusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories
|
||||
)
|
||||
|
||||
hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" }
|
||||
|
||||
const hepOverrideCategory = (hephaestusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (hepOverrideCategory) {
|
||||
hephaestusConfig = applyCategoryOverride(hephaestusConfig, hepOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (directory && hephaestusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
hephaestusConfig = { ...hephaestusConfig, prompt: hephaestusConfig.prompt + envContext }
|
||||
}
|
||||
|
||||
if (hephaestusOverride) {
|
||||
hephaestusConfig = mergeAgentConfig(hephaestusConfig, hephaestusOverride)
|
||||
}
|
||||
|
||||
result["hephaestus"] = hephaestusConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add pending agents after sisyphus and hephaestus to maintain order
|
||||
for (const [name, config] of pendingAgentConfigs) {
|
||||
result[name] = config
|
||||
}
|
||||
|
||||
if (!disabledAgents.includes("atlas")) {
|
||||
const orchestratorOverride = agentOverrides["atlas"]
|
||||
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
const atlasResolution = applyModelResolution({
|
||||
uiSelectedModel,
|
||||
userModel: orchestratorOverride?.model,
|
||||
requirement: atlasRequirement,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
if (atlasResolution) {
|
||||
const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution
|
||||
|
||||
let orchestratorConfig = createAtlasAgent({
|
||||
model: atlasModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
userCategories: categories,
|
||||
})
|
||||
|
||||
if (atlasResolvedVariant) {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
|
||||
orchestratorConfig = applyOverrides(orchestratorConfig, orchestratorOverride, mergedCategories)
|
||||
|
||||
result["atlas"] = orchestratorConfig
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -2,79 +2,71 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
CLI entry: `bunx oh-my-opencode`. 5 commands with Commander.js + @clack/prompts TUI.
|
||||
CLI entry: `bunx oh-my-opencode`. 107+ files with Commander.js + @clack/prompts TUI.
|
||||
|
||||
**Commands**: install (interactive setup), doctor (14 health checks), run (session launcher), get-local-version, mcp-oauth
|
||||
**Commands**: install, run, doctor, get-local-version, mcp-oauth
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry (5 commands)
|
||||
├── install.ts # Interactive TUI (542 lines)
|
||||
├── config-manager.ts # JSONC parsing (667 lines)
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
├── types.ts # InstallArgs, InstallConfig
|
||||
├── doctor/
|
||||
│ ├── index.ts # Doctor entry
|
||||
│ ├── runner.ts # Check orchestration
|
||||
│ ├── formatter.ts # Colored output
|
||||
│ ├── constants.ts # Check IDs, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition
|
||||
│ └── checks/ # 14 checks, 23 files
|
||||
│ ├── version.ts # OpenCode + plugin version
|
||||
│ ├── config.ts # JSONC validity, Zod
|
||||
│ ├── auth.ts # Anthropic, OpenAI, Google
|
||||
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
||||
│ ├── lsp.ts # LSP connectivity
|
||||
│ ├── mcp.ts # MCP validation
|
||||
│ ├── model-resolution.ts # Model resolution check (323 lines)
|
||||
│ └── gh.ts # GitHub CLI
|
||||
├── run/
|
||||
│ ├── index.ts # Session launcher
|
||||
│ └── events.ts # CLI run events (325 lines)
|
||||
├── mcp-oauth/
|
||||
│ └── index.ts # MCP OAuth flow
|
||||
└── get-local-version/
|
||||
└── index.ts # Version detection
|
||||
├── index.ts # Entry point (5 lines)
|
||||
├── cli-program.ts # Commander.js program (150+ lines, 5 commands)
|
||||
├── install.ts # TTY routing (TUI or CLI installer)
|
||||
├── cli-installer.ts # Non-interactive installer (164 lines)
|
||||
├── tui-installer.ts # Interactive TUI with @clack/prompts (140 lines)
|
||||
├── config-manager/ # 17 config utilities
|
||||
│ ├── add-plugin-to-opencode-config.ts # Plugin registration
|
||||
│ ├── add-provider-config.ts # Provider setup
|
||||
│ ├── detect-current-config.ts # Project vs user config
|
||||
│ ├── write-omo-config.ts # JSONC writing
|
||||
│ └── ...
|
||||
├── doctor/ # 14 health checks
|
||||
│ ├── runner.ts # Check orchestration
|
||||
│ ├── formatter.ts # Colored output
|
||||
│ └── checks/ # 29 files: auth, config, dependencies, gh, lsp, mcp, opencode, plugin, version, model-resolution (6 sub-checks)
|
||||
├── run/ # Session launcher (24 files)
|
||||
│ ├── runner.ts # Run orchestration (126 lines)
|
||||
│ ├── agent-resolver.ts # Agent selection: flag → env → config → fallback
|
||||
│ ├── session-resolver.ts # Session creation or resume
|
||||
│ ├── event-handlers.ts # Event processing (125 lines)
|
||||
│ ├── completion.ts # Completion detection
|
||||
│ └── poll-for-completion.ts # Polling with timeout
|
||||
├── mcp-oauth/ # OAuth token management (login, logout, status)
|
||||
├── get-local-version/ # Version detection + update check
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
└── provider-availability.ts # Provider availability checks
|
||||
```
|
||||
|
||||
## COMMANDS
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `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 |
|
||||
| `mcp-oauth` | MCP OAuth authentication flow |
|
||||
| Command | Purpose | Key Logic |
|
||||
|---------|---------|-----------|
|
||||
| `install` | Interactive setup | Provider selection → config generation → plugin registration |
|
||||
| `run` | Session launcher | Agent: flag → env → config → Sisyphus. Enforces todo completion. |
|
||||
| `doctor` | 14 health checks | installation, config, auth, deps, tools, updates |
|
||||
| `get-local-version` | Version check | Detects installed, compares with npm latest |
|
||||
| `mcp-oauth` | OAuth tokens | login (PKCE flow), logout, status |
|
||||
|
||||
## DOCTOR CATEGORIES (14 Checks)
|
||||
## DOCTOR CHECK CATEGORIES
|
||||
|
||||
| Category | Checks |
|
||||
|----------|--------|
|
||||
| installation | opencode, plugin |
|
||||
| configuration | config validity, Zod, model-resolution |
|
||||
| configuration | config validity, Zod, model-resolution (6 sub-checks) |
|
||||
| authentication | anthropic, openai, google |
|
||||
| dependencies | ast-grep, comment-checker, gh-cli |
|
||||
| tools | LSP, MCP |
|
||||
| tools | LSP, MCP, MCP-OAuth |
|
||||
| updates | version comparison |
|
||||
|
||||
## HOW TO ADD CHECK
|
||||
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`
|
||||
2. Export `getXXXCheckDefinition()` factory returning `CheckDefinition`
|
||||
2. Export `getXXXCheckDefinition()` returning `CheckDefinition`
|
||||
3. Add to `getAllCheckDefinitions()` in `checks/index.ts`
|
||||
|
||||
## TUI FRAMEWORK
|
||||
|
||||
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`
|
||||
- **picocolors**: Terminal colors for status and headers
|
||||
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), ℹ (info)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- **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`
|
||||
- **Blocking in non-TTY**: Check `process.stdout.isTTY`
|
||||
- **Direct JSON.parse**: Use `parseJsonc()` from shared
|
||||
- **Silent failures**: Return `warn` or `fail` in doctor, don't throw
|
||||
- **Hardcoded paths**: Use `getOpenCodeConfigPaths()` from config-manager
|
||||
|
||||
@@ -247,7 +247,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"writing": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -314,7 +314,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"writing": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -372,6 +372,7 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -432,6 +433,7 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -505,6 +507,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -579,6 +582,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -652,6 +656,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash",
|
||||
@@ -726,6 +731,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash",
|
||||
@@ -799,6 +805,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
@@ -873,6 +880,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
@@ -927,10 +935,10 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
"model": "zai-coding-plan/glm-5",
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -982,10 +990,10 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
"model": "zai-coding-plan/glm-5",
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1056,6 +1064,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash",
|
||||
@@ -1129,6 +1138,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
@@ -1189,8 +1199,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
"model": "zai-coding-plan/glm-5",
|
||||
},
|
||||
"writing": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
@@ -1256,6 +1265,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -1329,6 +1339,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
@@ -1402,6 +1413,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -1476,6 +1488,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "high",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash",
|
||||
|
||||
164
src/cli/cli-installer.ts
Normal file
164
src/cli/cli-installer.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import color from "picocolors"
|
||||
import type { InstallArgs } from "./types"
|
||||
import {
|
||||
addAuthPlugins,
|
||||
addPluginToOpenCodeConfig,
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
getOpenCodeVersion,
|
||||
isOpenCodeInstalled,
|
||||
writeOmoConfig,
|
||||
} from "./config-manager"
|
||||
import {
|
||||
SYMBOLS,
|
||||
argsToConfig,
|
||||
detectedToInitialValues,
|
||||
formatConfigSummary,
|
||||
printBox,
|
||||
printError,
|
||||
printHeader,
|
||||
printInfo,
|
||||
printStep,
|
||||
printSuccess,
|
||||
printWarning,
|
||||
validateNonTuiArgs,
|
||||
} from "./install-validators"
|
||||
|
||||
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||
const validation = validateNonTuiArgs(args)
|
||||
if (!validation.valid) {
|
||||
printHeader(false)
|
||||
printError("Validation failed:")
|
||||
for (const err of validation.errors) {
|
||||
console.log(` ${SYMBOLS.bullet} ${err}`)
|
||||
}
|
||||
console.log()
|
||||
printInfo(
|
||||
"Usage: bunx oh-my-opencode install --no-tui --claude=<no|yes|max20> --gemini=<no|yes> --copilot=<no|yes>",
|
||||
)
|
||||
console.log()
|
||||
return 1
|
||||
}
|
||||
|
||||
const detected = detectCurrentConfig()
|
||||
const isUpdate = detected.isInstalled
|
||||
|
||||
printHeader(isUpdate)
|
||||
|
||||
const totalSteps = 6
|
||||
let step = 1
|
||||
|
||||
printStep(step++, totalSteps, "Checking OpenCode installation...")
|
||||
const installed = await isOpenCodeInstalled()
|
||||
const openCodeVersion = await getOpenCodeVersion()
|
||||
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 ${openCodeVersion ?? ""} detected`)
|
||||
}
|
||||
|
||||
if (isUpdate) {
|
||||
const initial = detectedToInitialValues(detected)
|
||||
printInfo(`Current config: Claude=${initial.claude}, Gemini=${initial.gemini}`)
|
||||
}
|
||||
|
||||
const config = argsToConfig(args)
|
||||
|
||||
printStep(step++, totalSteps, "Adding oh-my-opencode plugin...")
|
||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||
if (!pluginResult.success) {
|
||||
printError(`Failed: ${pluginResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(
|
||||
`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`,
|
||||
)
|
||||
|
||||
if (config.hasGemini) {
|
||||
printStep(step++, totalSteps, "Adding auth plugins...")
|
||||
const authResult = await addAuthPlugins(config)
|
||||
if (!authResult.success) {
|
||||
printError(`Failed: ${authResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`)
|
||||
|
||||
printStep(step++, totalSteps, "Adding provider configurations...")
|
||||
const providerResult = addProviderConfig(config)
|
||||
if (!providerResult.success) {
|
||||
printError(`Failed: ${providerResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`)
|
||||
} else {
|
||||
step += 2
|
||||
}
|
||||
|
||||
printStep(step++, totalSteps, "Writing oh-my-opencode configuration...")
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
printError(`Failed: ${omoResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
||||
|
||||
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`)
|
||||
console.log(` Run ${color.cyan("opencode")} to start!`)
|
||||
console.log()
|
||||
|
||||
printBox(
|
||||
`${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",
|
||||
)
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
|
||||
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.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.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
|
||||
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
|
||||
"Authenticate Your Providers",
|
||||
)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
183
src/cli/cli-program.ts
Normal file
183
src/cli/cli-program.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Command } from "commander"
|
||||
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"
|
||||
import type { DoctorOptions } from "./doctor"
|
||||
import packageJson from "../../package.json" with { type: "json" }
|
||||
|
||||
const VERSION = packageJson.version
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program
|
||||
.name("oh-my-opencode")
|
||||
.description("The ultimate OpenCode plugin - multi-model orchestration, LSP tools, and more")
|
||||
.version(VERSION, "-v, --version", "Show version number")
|
||||
.enablePositionalOptions()
|
||||
|
||||
program
|
||||
.command("install")
|
||||
.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("--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 --openai=yes --gemini=yes --copilot=no
|
||||
$ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes --opencode-zen=yes
|
||||
|
||||
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > 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-6, 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,
|
||||
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)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
program
|
||||
.command("run <message>")
|
||||
.allowUnknownOption()
|
||||
.passThroughOptions()
|
||||
.description("Run opencode with todo/background task completion enforcement")
|
||||
.option("-a, --agent <name>", "Agent to use (default: from CLI/env/config, fallback: Sisyphus)")
|
||||
.option("-d, --directory <path>", "Working directory")
|
||||
.option("-t, --timeout <ms>", "Timeout in milliseconds (default: 30 minutes)", parseInt)
|
||||
.option("-p, --port <port>", "Server port (attaches if port already in use)", parseInt)
|
||||
.option("--attach <url>", "Attach to existing opencode server URL")
|
||||
.option("--on-complete <command>", "Shell command to run after completion")
|
||||
.option("--json", "Output structured JSON result to stdout")
|
||||
.option("--session-id <id>", "Resume existing session instead of creating new one")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode run "Fix the bug in index.ts"
|
||||
$ bunx oh-my-opencode run --agent Sisyphus "Implement feature X"
|
||||
$ bunx oh-my-opencode run --timeout 3600000 "Large refactoring task"
|
||||
$ bunx oh-my-opencode run --port 4321 "Fix the bug"
|
||||
$ bunx oh-my-opencode run --attach http://127.0.0.1:4321 "Fix the bug"
|
||||
$ bunx oh-my-opencode run --json "Fix the bug" | jq .sessionId
|
||||
$ bunx oh-my-opencode run --on-complete "notify-send Done" "Fix the bug"
|
||||
$ bunx oh-my-opencode run --session-id ses_abc123 "Continue the work"
|
||||
|
||||
Agent resolution order:
|
||||
1) --agent flag
|
||||
2) OPENCODE_DEFAULT_AGENT
|
||||
3) oh-my-opencode.json "default_run_agent"
|
||||
4) Sisyphus (fallback)
|
||||
|
||||
Available core agents:
|
||||
Sisyphus, Hephaestus, Prometheus, Atlas
|
||||
|
||||
Unlike 'opencode run', this command waits until:
|
||||
- All todos are completed or cancelled
|
||||
- All child sessions (background tasks) are idle
|
||||
`)
|
||||
.action(async (message: string, options) => {
|
||||
if (options.port && options.attach) {
|
||||
console.error("Error: --port and --attach are mutually exclusive")
|
||||
process.exit(1)
|
||||
}
|
||||
const runOptions: RunOptions = {
|
||||
message,
|
||||
agent: options.agent,
|
||||
directory: options.directory,
|
||||
timeout: options.timeout,
|
||||
port: options.port,
|
||||
attach: options.attach,
|
||||
onComplete: options.onComplete,
|
||||
json: options.json ?? false,
|
||||
sessionId: options.sessionId,
|
||||
}
|
||||
const exitCode = await run(runOptions)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
program
|
||||
.command("get-local-version")
|
||||
.description("Show current installed version and check for updates")
|
||||
.option("-d, --directory <path>", "Working directory to check config from")
|
||||
.option("--json", "Output in JSON format for scripting")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode get-local-version
|
||||
$ bunx oh-my-opencode get-local-version --json
|
||||
$ bunx oh-my-opencode get-local-version --directory /path/to/project
|
||||
|
||||
This command shows:
|
||||
- Current installed version
|
||||
- Latest available version on npm
|
||||
- Whether you're up to date
|
||||
- Special modes (local dev, pinned version)
|
||||
`)
|
||||
.action(async (options) => {
|
||||
const versionOptions: GetLocalVersionOptions = {
|
||||
directory: options.directory,
|
||||
json: options.json ?? false,
|
||||
}
|
||||
const exitCode = await getLocalVersion(versionOptions)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
program
|
||||
.command("doctor")
|
||||
.description("Check oh-my-opencode installation health and diagnose issues")
|
||||
.option("--status", "Show compact system dashboard")
|
||||
.option("--verbose", "Show detailed diagnostic information")
|
||||
.option("--json", "Output results in JSON format")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode doctor # Show problems only
|
||||
$ bunx oh-my-opencode doctor --status # Compact dashboard
|
||||
$ bunx oh-my-opencode doctor --verbose # Deep diagnostics
|
||||
$ bunx oh-my-opencode doctor --json # JSON output
|
||||
`)
|
||||
.action(async (options) => {
|
||||
const mode = options.status ? "status" : options.verbose ? "verbose" : "default"
|
||||
const doctorOptions: DoctorOptions = {
|
||||
mode,
|
||||
json: options.json ?? false,
|
||||
}
|
||||
const exitCode = await doctor(doctorOptions)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
program
|
||||
.command("version")
|
||||
.description("Show version information")
|
||||
.action(() => {
|
||||
console.log(`oh-my-opencode v${VERSION}`)
|
||||
})
|
||||
|
||||
program.addCommand(createMcpOAuthCommand())
|
||||
|
||||
export function runCli(): void {
|
||||
program.parse()
|
||||
}
|
||||
@@ -1,667 +1,23 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "node:fs"
|
||||
import {
|
||||
parseJsonc,
|
||||
getOpenCodeConfigPaths,
|
||||
type OpenCodeBinaryType,
|
||||
type OpenCodeConfigPaths,
|
||||
} from "../shared"
|
||||
import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
|
||||
import { generateModelConfig } from "./model-fallback"
|
||||
export type { ConfigContext } from "./config-manager/config-context"
|
||||
export {
|
||||
initConfigContext,
|
||||
getConfigContext,
|
||||
resetConfigContext,
|
||||
} from "./config-manager/config-context"
|
||||
|
||||
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
||||
export { fetchNpmDistTags } from "./config-manager/npm-dist-tags"
|
||||
export { getPluginNameWithVersion } from "./config-manager/plugin-name-with-version"
|
||||
export { addPluginToOpenCodeConfig } from "./config-manager/add-plugin-to-opencode-config"
|
||||
|
||||
interface ConfigContext {
|
||||
binary: OpenCodeBinaryType
|
||||
version: string | null
|
||||
paths: OpenCodeConfigPaths
|
||||
}
|
||||
export { generateOmoConfig } from "./config-manager/generate-omo-config"
|
||||
export { writeOmoConfig } from "./config-manager/write-omo-config"
|
||||
|
||||
let configContext: ConfigContext | null = null
|
||||
export { isOpenCodeInstalled, getOpenCodeVersion } from "./config-manager/opencode-binary"
|
||||
|
||||
export function initConfigContext(binary: OpenCodeBinaryType, version: string | null): void {
|
||||
const paths = getOpenCodeConfigPaths({ binary, version })
|
||||
configContext = { binary, version, paths }
|
||||
}
|
||||
export { fetchLatestVersion, addAuthPlugins } from "./config-manager/auth-plugins"
|
||||
export { ANTIGRAVITY_PROVIDER_CONFIG } from "./config-manager/antigravity-provider-configuration"
|
||||
export { addProviderConfig } from "./config-manager/add-provider-config"
|
||||
export { detectCurrentConfig } from "./config-manager/detect-current-config"
|
||||
|
||||
export function getConfigContext(): ConfigContext {
|
||||
if (!configContext) {
|
||||
const paths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
configContext = { binary: "opencode", version: null, paths }
|
||||
}
|
||||
return configContext
|
||||
}
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
configContext = null
|
||||
}
|
||||
|
||||
function getConfigDir(): string {
|
||||
return getConfigContext().paths.configDir
|
||||
}
|
||||
|
||||
function getConfigJson(): string {
|
||||
return getConfigContext().paths.configJson
|
||||
}
|
||||
|
||||
function getConfigJsonc(): string {
|
||||
return getConfigContext().paths.configJsonc
|
||||
}
|
||||
|
||||
function getPackageJson(): string {
|
||||
return getConfigContext().paths.packageJson
|
||||
}
|
||||
|
||||
function getOmoConfig(): string {
|
||||
return getConfigContext().paths.omoConfig
|
||||
}
|
||||
|
||||
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
||||
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
||||
|
||||
interface NodeError extends Error {
|
||||
code?: string
|
||||
}
|
||||
|
||||
function isPermissionError(err: unknown): boolean {
|
||||
const nodeErr = err as NodeError
|
||||
return nodeErr?.code === "EACCES" || nodeErr?.code === "EPERM"
|
||||
}
|
||||
|
||||
function isFileNotFoundError(err: unknown): boolean {
|
||||
const nodeErr = err as NodeError
|
||||
return nodeErr?.code === "ENOENT"
|
||||
}
|
||||
|
||||
function formatErrorWithSuggestion(err: unknown, context: string): string {
|
||||
if (isPermissionError(err)) {
|
||||
return `Permission denied: Cannot ${context}. Try running with elevated permissions or check file ownership.`
|
||||
}
|
||||
|
||||
if (isFileNotFoundError(err)) {
|
||||
return `File not found while trying to ${context}. The file may have been deleted or moved.`
|
||||
}
|
||||
|
||||
if (err instanceof SyntaxError) {
|
||||
return `JSON syntax error while trying to ${context}: ${err.message}. Check for missing commas, brackets, or invalid characters.`
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
|
||||
if (message.includes("ENOSPC")) {
|
||||
return `Disk full: Cannot ${context}. Free up disk space and try again.`
|
||||
}
|
||||
|
||||
if (message.includes("EROFS")) {
|
||||
return `Read-only filesystem: Cannot ${context}. Check if the filesystem is mounted read-only.`
|
||||
}
|
||||
|
||||
return `Failed to ${context}: ${message}`
|
||||
}
|
||||
|
||||
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json() as { version: string }
|
||||
return data.version
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface NpmDistTags {
|
||||
latest?: string
|
||||
beta?: string
|
||||
next?: string
|
||||
[tag: string]: string | undefined
|
||||
}
|
||||
|
||||
const NPM_FETCH_TIMEOUT_MS = 5000
|
||||
|
||||
export async function fetchNpmDistTags(packageName: string): Promise<NpmDistTags | null> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, {
|
||||
signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
|
||||
})
|
||||
if (!res.ok) return null
|
||||
const data = await res.json() as NpmDistTags
|
||||
return data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
|
||||
const PRIORITIZED_TAGS = ["latest", "beta", "next"] as const
|
||||
|
||||
export async function getPluginNameWithVersion(currentVersion: string): Promise<string> {
|
||||
const distTags = await fetchNpmDistTags(PACKAGE_NAME)
|
||||
|
||||
if (distTags) {
|
||||
const allTags = new Set([...PRIORITIZED_TAGS, ...Object.keys(distTags)])
|
||||
for (const tag of allTags) {
|
||||
if (distTags[tag] === currentVersion) {
|
||||
return `${PACKAGE_NAME}@${tag}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${PACKAGE_NAME}@${currentVersion}`
|
||||
}
|
||||
|
||||
type ConfigFormat = "json" | "jsonc" | "none"
|
||||
|
||||
interface OpenCodeConfig {
|
||||
plugin?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export function detectConfigFormat(): { format: ConfigFormat; path: string } {
|
||||
const configJsonc = getConfigJsonc()
|
||||
const configJson = getConfigJson()
|
||||
|
||||
if (existsSync(configJsonc)) {
|
||||
return { format: "jsonc", path: configJsonc }
|
||||
}
|
||||
if (existsSync(configJson)) {
|
||||
return { format: "json", path: configJson }
|
||||
}
|
||||
return { format: "none", path: configJson }
|
||||
}
|
||||
|
||||
interface ParseConfigResult {
|
||||
config: OpenCodeConfig | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
function isEmptyOrWhitespace(content: string): boolean {
|
||||
return content.trim().length === 0
|
||||
}
|
||||
|
||||
function parseConfig(path: string, _isJsonc: boolean): OpenCodeConfig | null {
|
||||
const result = parseConfigWithError(path)
|
||||
return result.config
|
||||
}
|
||||
|
||||
function parseConfigWithError(path: string): ParseConfigResult {
|
||||
try {
|
||||
const stat = statSync(path)
|
||||
if (stat.size === 0) {
|
||||
return { config: null, error: `Config file is empty: ${path}. Delete it or add valid JSON content.` }
|
||||
}
|
||||
|
||||
const content = readFileSync(path, "utf-8")
|
||||
|
||||
if (isEmptyOrWhitespace(content)) {
|
||||
return { config: null, error: `Config file contains only whitespace: ${path}. Delete it or add valid JSON content.` }
|
||||
}
|
||||
|
||||
const config = parseJsonc<OpenCodeConfig>(content)
|
||||
|
||||
if (config === null || config === undefined) {
|
||||
return { config: null, error: `Config file parsed to null/undefined: ${path}. Ensure it contains valid JSON.` }
|
||||
}
|
||||
|
||||
if (typeof config !== "object" || Array.isArray(config)) {
|
||||
return { config: null, error: `Config file must contain a JSON object, not ${Array.isArray(config) ? "an array" : typeof config}: ${path}` }
|
||||
}
|
||||
|
||||
return { config }
|
||||
} catch (err) {
|
||||
return { config: null, error: formatErrorWithSuggestion(err, `parse config file ${path}`) }
|
||||
}
|
||||
}
|
||||
|
||||
function ensureConfigDir(): void {
|
||||
const configDir = getConfigDir()
|
||||
if (!existsSync(configDir)) {
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
const pluginEntry = await getPluginNameWithVersion(currentVersion)
|
||||
|
||||
try {
|
||||
if (format === "none") {
|
||||
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
|
||||
const parseResult = parseConfigWithError(path)
|
||||
if (!parseResult.config) {
|
||||
return { success: false, configPath: path, error: parseResult.error ?? "Failed to parse config file" }
|
||||
}
|
||||
|
||||
const config = parseResult.config
|
||||
const plugins = config.plugin ?? []
|
||||
const existingIndex = plugins.findIndex((p) => p === PACKAGE_NAME || p.startsWith(`${PACKAGE_NAME}@`))
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
if (plugins[existingIndex] === pluginEntry) {
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
plugins[existingIndex] = pluginEntry
|
||||
} else {
|
||||
plugins.push(pluginEntry)
|
||||
}
|
||||
|
||||
config.plugin = plugins
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
const pluginArrayRegex = /"plugin"\s*:\s*\[([\s\S]*?)\]/
|
||||
const match = content.match(pluginArrayRegex)
|
||||
|
||||
if (match) {
|
||||
const formattedPlugins = plugins.map((p) => `"${p}"`).join(",\n ")
|
||||
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${formattedPlugins}\n ]`)
|
||||
writeFileSync(path, newContent)
|
||||
} else {
|
||||
const newContent = content.replace(/^(\s*\{)/, `$1\n "plugin": ["${pluginEntry}"],`)
|
||||
writeFileSync(path, newContent)
|
||||
}
|
||||
} else {
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
}
|
||||
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return { success: false, configPath: path, error: formatErrorWithSuggestion(err, "update opencode config") }
|
||||
}
|
||||
}
|
||||
|
||||
function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
|
||||
const result = { ...target }
|
||||
|
||||
for (const key of Object.keys(source) as Array<keyof T>) {
|
||||
const sourceValue = source[key]
|
||||
const targetValue = result[key]
|
||||
|
||||
if (
|
||||
sourceValue !== null &&
|
||||
typeof sourceValue === "object" &&
|
||||
!Array.isArray(sourceValue) &&
|
||||
targetValue !== null &&
|
||||
typeof targetValue === "object" &&
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
result[key] = deepMerge(
|
||||
targetValue as Record<string, unknown>,
|
||||
sourceValue as Record<string, unknown>
|
||||
) as T[keyof T]
|
||||
} else if (sourceValue !== undefined) {
|
||||
result[key] = sourceValue as T[keyof T]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function generateOmoConfig(installConfig: InstallConfig): Record<string, unknown> {
|
||||
return generateModelConfig(installConfig)
|
||||
}
|
||||
|
||||
export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const omoConfigPath = getOmoConfig()
|
||||
|
||||
try {
|
||||
const newConfig = generateOmoConfig(installConfig)
|
||||
|
||||
if (existsSync(omoConfigPath)) {
|
||||
try {
|
||||
const stat = statSync(omoConfigPath)
|
||||
const content = readFileSync(omoConfigPath, "utf-8")
|
||||
|
||||
if (stat.size === 0 || isEmptyOrWhitespace(content)) {
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
}
|
||||
|
||||
const existing = parseJsonc<Record<string, unknown>>(content)
|
||||
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
}
|
||||
|
||||
const merged = deepMerge(existing, newConfig)
|
||||
writeFileSync(omoConfigPath, JSON.stringify(merged, null, 2) + "\n")
|
||||
} catch (parseErr) {
|
||||
if (parseErr instanceof SyntaxError) {
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
}
|
||||
throw parseErr
|
||||
}
|
||||
} else {
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
}
|
||||
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
} catch (err) {
|
||||
return { success: false, configPath: omoConfigPath, error: formatErrorWithSuggestion(err, "write oh-my-opencode config") }
|
||||
}
|
||||
}
|
||||
|
||||
interface OpenCodeBinaryResult {
|
||||
binary: OpenCodeBinaryType
|
||||
version: string
|
||||
}
|
||||
|
||||
async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | null> {
|
||||
for (const binary of OPENCODE_BINARIES) {
|
||||
try {
|
||||
const proc = Bun.spawn([binary, "--version"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
if (proc.exitCode === 0) {
|
||||
const version = output.trim()
|
||||
initConfigContext(binary, version)
|
||||
return { binary, version }
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function isOpenCodeInstalled(): Promise<boolean> {
|
||||
const result = await findOpenCodeBinaryWithVersion()
|
||||
return result !== null
|
||||
}
|
||||
|
||||
export async function getOpenCodeVersion(): Promise<string | null> {
|
||||
const result = await findOpenCodeBinaryWithVersion()
|
||||
return result?.version ?? null
|
||||
}
|
||||
|
||||
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
|
||||
try {
|
||||
let existingConfig: OpenCodeConfig | null = null
|
||||
if (format !== "none") {
|
||||
const parseResult = parseConfigWithError(path)
|
||||
if (parseResult.error && !parseResult.config) {
|
||||
existingConfig = {}
|
||||
} else {
|
||||
existingConfig = parseResult.config
|
||||
}
|
||||
}
|
||||
|
||||
const plugins: string[] = existingConfig?.plugin ?? []
|
||||
|
||||
if (config.hasGemini) {
|
||||
const version = await fetchLatestVersion("opencode-antigravity-auth")
|
||||
const pluginEntry = version ? `opencode-antigravity-auth@${version}` : "opencode-antigravity-auth"
|
||||
if (!plugins.some((p) => p.startsWith("opencode-antigravity-auth"))) {
|
||||
plugins.push(pluginEntry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
|
||||
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return { success: false, configPath: path, error: formatErrorWithSuggestion(err, "add auth plugins to config") }
|
||||
}
|
||||
}
|
||||
|
||||
export interface BunInstallResult {
|
||||
success: boolean
|
||||
timedOut?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function runBunInstall(): Promise<boolean> {
|
||||
const result = await runBunInstallWithDetails()
|
||||
return result.success
|
||||
}
|
||||
|
||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
try {
|
||||
const proc = Bun.spawn(["bun", "install"], {
|
||||
cwd: getConfigDir(),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const timeoutPromise = new Promise<"timeout">((resolve) =>
|
||||
setTimeout(() => resolve("timeout"), BUN_INSTALL_TIMEOUT_MS)
|
||||
)
|
||||
|
||||
const exitPromise = proc.exited.then(() => "completed" as const)
|
||||
|
||||
const result = await Promise.race([exitPromise, timeoutPromise])
|
||||
|
||||
if (result === "timeout") {
|
||||
try {
|
||||
proc.kill()
|
||||
} catch {
|
||||
/* intentionally empty - process may have already exited */
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
timedOut: true,
|
||||
error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually: cd ~/.config/opencode && bun i`,
|
||||
}
|
||||
}
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
return {
|
||||
success: false,
|
||||
error: stderr.trim() || `bun install failed with exit code ${proc.exitCode}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return {
|
||||
success: false,
|
||||
error: `bun install failed: ${message}. Is bun installed? Try: curl -fsSL https://bun.sh/install | bash`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Antigravity Provider Configuration
|
||||
*
|
||||
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
|
||||
*
|
||||
* 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 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#models
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
models: {
|
||||
"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)",
|
||||
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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
|
||||
try {
|
||||
let existingConfig: OpenCodeConfig | null = null
|
||||
if (format !== "none") {
|
||||
const parseResult = parseConfigWithError(path)
|
||||
if (parseResult.error && !parseResult.config) {
|
||||
existingConfig = {}
|
||||
} else {
|
||||
existingConfig = parseResult.config
|
||||
}
|
||||
}
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}) }
|
||||
|
||||
const providers = (newConfig.provider ?? {}) as Record<string, unknown>
|
||||
|
||||
if (config.hasGemini) {
|
||||
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
|
||||
}
|
||||
|
||||
if (Object.keys(providers).length > 0) {
|
||||
newConfig.provider = providers
|
||||
}
|
||||
|
||||
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return { success: false, configPath: path, error: formatErrorWithSuggestion(err, "add provider config") }
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const result: DetectedConfig = {
|
||||
isInstalled: false,
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: true,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
if (format === "none") {
|
||||
return result
|
||||
}
|
||||
|
||||
const parseResult = parseConfigWithError(path)
|
||||
if (!parseResult.config) {
|
||||
return result
|
||||
}
|
||||
|
||||
const openCodeConfig = parseResult.config
|
||||
const plugins = openCodeConfig.plugin ?? []
|
||||
result.isInstalled = plugins.some((p) => p.startsWith("oh-my-opencode"))
|
||||
|
||||
if (!result.isInstalled) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Gemini auth plugin detection still works via plugin presence
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
|
||||
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
|
||||
result.hasOpenAI = hasOpenAI
|
||||
result.hasOpencodeZen = hasOpencodeZen
|
||||
result.hasZaiCodingPlan = hasZaiCodingPlan
|
||||
result.hasKimiForCoding = hasKimiForCoding
|
||||
|
||||
return result
|
||||
}
|
||||
export type { BunInstallResult } from "./config-manager/bun-install"
|
||||
export { runBunInstall, runBunInstallWithDetails } from "./config-manager/bun-install"
|
||||
|
||||
82
src/cli/config-manager/add-plugin-to-opencode-config.ts
Normal file
82
src/cli/config-manager/add-plugin-to-opencode-config.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs"
|
||||
import type { ConfigMergeResult } from "../types"
|
||||
import { getConfigDir } from "./config-context"
|
||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||
import { detectConfigFormat } from "./opencode-config-format"
|
||||
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
||||
import { getPluginNameWithVersion } from "./plugin-name-with-version"
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
|
||||
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDirectoryExists()
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: getConfigDir(),
|
||||
error: formatErrorWithSuggestion(err, "create config directory"),
|
||||
}
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
const pluginEntry = await getPluginNameWithVersion(currentVersion)
|
||||
|
||||
try {
|
||||
if (format === "none") {
|
||||
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
|
||||
const parseResult = parseOpenCodeConfigFileWithError(path)
|
||||
if (!parseResult.config) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: parseResult.error ?? "Failed to parse config file",
|
||||
}
|
||||
}
|
||||
|
||||
const config = parseResult.config
|
||||
const plugins = config.plugin ?? []
|
||||
const existingIndex = plugins.findIndex((p) => p === PACKAGE_NAME || p.startsWith(`${PACKAGE_NAME}@`))
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
if (plugins[existingIndex] === pluginEntry) {
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
plugins[existingIndex] = pluginEntry
|
||||
} else {
|
||||
plugins.push(pluginEntry)
|
||||
}
|
||||
|
||||
config.plugin = plugins
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
const pluginArrayRegex = /"plugin"\s*:\s*\[([\s\S]*?)\]/
|
||||
const match = content.match(pluginArrayRegex)
|
||||
|
||||
if (match) {
|
||||
const formattedPlugins = plugins.map((p) => `"${p}"`).join(",\n ")
|
||||
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${formattedPlugins}\n ]`)
|
||||
writeFileSync(path, newContent)
|
||||
} else {
|
||||
const newContent = content.replace(/(\{)/, `$1\n "plugin": ["${pluginEntry}"],`)
|
||||
writeFileSync(path, newContent)
|
||||
}
|
||||
} else {
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
}
|
||||
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: formatErrorWithSuggestion(err, "update opencode config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/cli/config-manager/add-provider-config.test.ts
Normal file
205
src/cli/config-manager/add-provider-config.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
|
||||
describe("modifyProviderInJsonc", () => {
|
||||
describe("Test 1: Basic JSONC with existing provider", () => {
|
||||
it("replaces provider value, preserves comments and other keys", () => {
|
||||
// given
|
||||
const content = `{
|
||||
// my config
|
||||
"provider": { "openai": {} },
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"google"')
|
||||
expect(result).toContain('"plugin": ["foo"]')
|
||||
expect(result).toContain('// my config')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed).toHaveProperty('provider')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 2: Comment containing '}' inside provider block", () => {
|
||||
it("must NOT corrupt file", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"provider": {
|
||||
// } this brace should be ignored
|
||||
"openai": {}
|
||||
},
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 3: Comment containing '\"provider\"' before real key", () => {
|
||||
it("must NOT match wrong location", () => {
|
||||
// given
|
||||
const content = `{
|
||||
// "provider": { "example": true }
|
||||
"provider": { "openai": {} },
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
expect(parsed.provider).toHaveProperty('google')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 4: Comment containing '{' inside provider", () => {
|
||||
it("must NOT mess up depth", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"provider": {
|
||||
// { unmatched brace in comment
|
||||
"openai": {}
|
||||
},
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 5: No existing provider key", () => {
|
||||
it("inserts provider without corrupting", () => {
|
||||
// given
|
||||
const content = `{
|
||||
// config comment
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"provider"')
|
||||
expect(result).toContain('"plugin"')
|
||||
expect(result).toContain('foo')
|
||||
expect(result).toContain('// config comment')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('provider')
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed.plugin).toEqual(['foo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 6: String value exactly 'provider' before real key", () => {
|
||||
it("must NOT match wrong location", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"note": "provider",
|
||||
"provider": { "openai": {} },
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
expect(result).toContain('"note": "provider"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
expect(parsed.note).toBe('provider')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 7: Post-write validation", () => {
|
||||
it("result file must be valid JSONC for all cases", () => {
|
||||
// Test Case 1
|
||||
const content1 = `{
|
||||
"provider": { "openai": {} },
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const result1 = modifyProviderInJsonc(content1, { google: {} })
|
||||
expect(() => parseJsonc(result1)).not.toThrow()
|
||||
|
||||
// Test Case 2
|
||||
const content2 = `{
|
||||
"provider": {
|
||||
// } comment
|
||||
"openai": {}
|
||||
}
|
||||
}`
|
||||
const result2 = modifyProviderInJsonc(content2, { google: {} })
|
||||
expect(() => parseJsonc(result2)).not.toThrow()
|
||||
|
||||
// Test Case 3
|
||||
const content3 = `{
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const result3 = modifyProviderInJsonc(content3, { google: {} })
|
||||
expect(() => parseJsonc(result3)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 8: Trailing commas preserved", () => {
|
||||
it("file is valid JSONC with trailing commas", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"provider": { "openai": {}, },
|
||||
"plugin": ["foo",],
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(() => parseJsonc(result)).not.toThrow()
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed.plugin).toEqual(['foo'])
|
||||
})
|
||||
})
|
||||
})
|
||||
82
src/cli/config-manager/add-provider-config.ts
Normal file
82
src/cli/config-manager/add-provider-config.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { readFileSync, writeFileSync, copyFileSync } from "node:fs"
|
||||
import type { ConfigMergeResult, InstallConfig } from "../types"
|
||||
import { getConfigDir } from "./config-context"
|
||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||
import { detectConfigFormat } from "./opencode-config-format"
|
||||
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
||||
import { ANTIGRAVITY_PROVIDER_CONFIG } from "./antigravity-provider-configuration"
|
||||
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
|
||||
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
try {
|
||||
ensureConfigDirectoryExists()
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: getConfigDir(),
|
||||
error: formatErrorWithSuggestion(err, "create config directory"),
|
||||
}
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
|
||||
try {
|
||||
let existingConfig: OpenCodeConfig | null = null
|
||||
if (format !== "none") {
|
||||
const parseResult = parseOpenCodeConfigFileWithError(path)
|
||||
if (parseResult.error && !parseResult.config) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to parse config file: ${parseResult.error}`,
|
||||
}
|
||||
}
|
||||
existingConfig = parseResult.config
|
||||
}
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}) }
|
||||
const providers = (newConfig.provider ?? {}) as Record<string, unknown>
|
||||
|
||||
if (config.hasGemini) {
|
||||
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
|
||||
}
|
||||
|
||||
if (Object.keys(providers).length > 0) {
|
||||
newConfig.provider = providers
|
||||
}
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
|
||||
// Backup original file
|
||||
copyFileSync(path, `${path}.bak`)
|
||||
|
||||
const providerValue = (newConfig.provider ?? {}) as Record<string, unknown>
|
||||
const newContent = modifyProviderInJsonc(content, providerValue)
|
||||
|
||||
// Post-write validation
|
||||
try {
|
||||
parseJsonc(newContent)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(path, newContent)
|
||||
} else {
|
||||
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
}
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: formatErrorWithSuggestion(err, "add provider config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/cli/config-manager/antigravity-provider-configuration.ts
Normal file
64
src/cli/config-manager/antigravity-provider-configuration.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Antigravity Provider Configuration
|
||||
*
|
||||
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
|
||||
*
|
||||
* 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 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#models
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
models: {
|
||||
"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)",
|
||||
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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
224
src/cli/config-manager/auth-plugins.test.ts
Normal file
224
src/cli/config-manager/auth-plugins.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "node:fs"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
import type { InstallConfig } from "../types"
|
||||
import { resetConfigContext } from "./config-context"
|
||||
|
||||
let testConfigPath: string
|
||||
let testConfigDir: string
|
||||
let testCounter = 0
|
||||
let fetchVersionSpy: unknown
|
||||
|
||||
beforeEach(async () => {
|
||||
testCounter++
|
||||
testConfigDir = join(tmpdir(), `test-opencode-${Date.now()}-${testCounter}`)
|
||||
testConfigPath = join(testConfigDir, "opencode.jsonc")
|
||||
mkdirSync(testConfigDir, { recursive: true })
|
||||
|
||||
process.env.OPENCODE_CONFIG_DIR = testConfigDir
|
||||
resetConfigContext()
|
||||
|
||||
const module = await import("./auth-plugins")
|
||||
fetchVersionSpy = spyOn(module, "fetchLatestVersion").mockResolvedValue("1.2.3")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
rmSync(testConfigDir, { recursive: true, force: true })
|
||||
} catch {}
|
||||
})
|
||||
|
||||
const testConfig: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: true,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
describe("addAuthPlugins", () => {
|
||||
describe("Test 1: JSONC with commented plugin line", () => {
|
||||
it("preserves comment, updates actual plugin array", async () => {
|
||||
const content = `{
|
||||
// "plugin": ["old-plugin"]
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(result.configPath, "utf-8")
|
||||
expect(newContent).toContain('// "plugin": ["old-plugin"]')
|
||||
expect(newContent).toContain('existing-plugin')
|
||||
expect(newContent).toContain('opencode-antigravity-auth')
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
expect(plugins).toContain('existing-plugin')
|
||||
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 2: Plugin array already contains antigravity", () => {
|
||||
it("does not add duplicate", async () => {
|
||||
const content = `{
|
||||
"plugin": ["existing-plugin", "opencode-antigravity-auth"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
|
||||
const antigravityCount = plugins.filter((p) => p.startsWith('opencode-antigravity-auth')).length
|
||||
expect(antigravityCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 3: Backup created before write", () => {
|
||||
it("creates .bak file", async () => {
|
||||
const originalContent = `{
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, originalContent, "utf-8")
|
||||
readFileSync(testConfigPath, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(existsSync(`${result.configPath}.bak`)).toBe(true)
|
||||
|
||||
const backupContent = readFileSync(`${result.configPath}.bak`, "utf-8")
|
||||
expect(backupContent).toBe(originalContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 4: Comment with } character", () => {
|
||||
it("preserves comments with special characters", async () => {
|
||||
const content = `{
|
||||
// This comment has } special characters
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
expect(newContent).toContain('// This comment has } special characters')
|
||||
|
||||
expect(() => parseJsonc(newContent)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 5: Comment containing 'plugin' string", () => {
|
||||
it("must NOT match comment location", async () => {
|
||||
const content = `{
|
||||
// "plugin": ["fake"]
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
expect(newContent).toContain('// "plugin": ["fake"]')
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
expect(plugins).toContain('existing-plugin')
|
||||
expect(plugins).not.toContain('fake')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 6: No existing plugin array", () => {
|
||||
it("creates plugin array when none exists", async () => {
|
||||
const content = `{
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(result.configPath, "utf-8")
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
const plugins = parsed.plugin as string[]
|
||||
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 7: Post-write validation ensures valid JSONC", () => {
|
||||
it("result file must be valid JSONC", async () => {
|
||||
const content = `{
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
expect(() => parseJsonc(newContent)).not.toThrow()
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed).toHaveProperty('provider')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 8: Multiple plugins in array", () => {
|
||||
it("appends to existing plugins", async () => {
|
||||
const content = `{
|
||||
"plugin": ["plugin-1", "plugin-2", "plugin-3"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(result.configPath, "utf-8")
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
|
||||
expect(plugins).toContain('plugin-1')
|
||||
expect(plugins).toContain('plugin-2')
|
||||
expect(plugins).toContain('plugin-3')
|
||||
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
145
src/cli/config-manager/auth-plugins.ts
Normal file
145
src/cli/config-manager/auth-plugins.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"
|
||||
import { modify, applyEdits } from "jsonc-parser"
|
||||
import type { ConfigMergeResult, InstallConfig } from "../types"
|
||||
import { getConfigDir } from "./config-context"
|
||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||
import { detectConfigFormat } from "./opencode-config-format"
|
||||
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
|
||||
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`)
|
||||
if (!res.ok) return null
|
||||
const data = (await res.json()) as { version: string }
|
||||
return data.version
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDirectoryExists()
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: getConfigDir(),
|
||||
error: formatErrorWithSuggestion(err, "create config directory"),
|
||||
}
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
const backupPath = `${path}.bak`
|
||||
|
||||
try {
|
||||
let existingConfig: OpenCodeConfig | null = null
|
||||
if (format !== "none") {
|
||||
const parseResult = parseOpenCodeConfigFileWithError(path)
|
||||
if (parseResult.error && !parseResult.config) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to parse config file: ${parseResult.error}`,
|
||||
}
|
||||
}
|
||||
existingConfig = parseResult.config
|
||||
}
|
||||
|
||||
const rawPlugins = existingConfig?.plugin
|
||||
const plugins: string[] = Array.isArray(rawPlugins) ? rawPlugins : []
|
||||
|
||||
if (config.hasGemini) {
|
||||
const version = await fetchLatestVersion("opencode-antigravity-auth")
|
||||
const pluginEntry = version ? `opencode-antigravity-auth@${version}` : "opencode-antigravity-auth"
|
||||
if (!plugins.some((p) => p.startsWith("opencode-antigravity-auth"))) {
|
||||
plugins.push(pluginEntry)
|
||||
}
|
||||
}
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
|
||||
|
||||
if (format !== "none" && existsSync(path)) {
|
||||
copyFileSync(path, backupPath)
|
||||
}
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
|
||||
const newContent = applyEdits(
|
||||
content,
|
||||
modify(content, ["plugin"], plugins, {
|
||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||
})
|
||||
)
|
||||
|
||||
try {
|
||||
parseJsonc(newContent)
|
||||
} catch (error) {
|
||||
if (existsSync(backupPath)) {
|
||||
copyFileSync(backupPath, path)
|
||||
}
|
||||
throw new Error(`Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(path, newContent)
|
||||
} catch (error) {
|
||||
const hasBackup = existsSync(backupPath)
|
||||
try {
|
||||
if (hasBackup) {
|
||||
copyFileSync(backupPath, path)
|
||||
}
|
||||
} catch (restoreError) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: hasBackup
|
||||
? `Failed to write config file. Restored from backup: ${String(error)}`
|
||||
: `Failed to write config file. No backup was available: ${String(error)}`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const nextContent = JSON.stringify(newConfig, null, 2) + "\n"
|
||||
try {
|
||||
writeFileSync(path, nextContent)
|
||||
} catch (error) {
|
||||
const hasBackup = existsSync(backupPath)
|
||||
try {
|
||||
if (hasBackup) {
|
||||
copyFileSync(backupPath, path)
|
||||
}
|
||||
} catch (restoreError) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: hasBackup
|
||||
? `Failed to write config file. Restored from backup: ${String(error)}`
|
||||
: `Failed to write config file. No backup was available: ${String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: formatErrorWithSuggestion(err, "add auth plugins to config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/cli/config-manager/bun-install.ts
Normal file
61
src/cli/config-manager/bun-install.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { getConfigDir } from "./config-context"
|
||||
|
||||
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
||||
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
||||
|
||||
export interface BunInstallResult {
|
||||
success: boolean
|
||||
timedOut?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function runBunInstall(): Promise<boolean> {
|
||||
const result = await runBunInstallWithDetails()
|
||||
return result.success
|
||||
}
|
||||
|
||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
try {
|
||||
const proc = Bun.spawn(["bun", "install"], {
|
||||
cwd: getConfigDir(),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
let timeoutId: ReturnType<typeof setTimeout>
|
||||
const timeoutPromise = new Promise<"timeout">((resolve) => {
|
||||
timeoutId = setTimeout(() => resolve("timeout"), BUN_INSTALL_TIMEOUT_MS)
|
||||
})
|
||||
const exitPromise = proc.exited.then(() => "completed" as const)
|
||||
const result = await Promise.race([exitPromise, timeoutPromise])
|
||||
clearTimeout(timeoutId!)
|
||||
|
||||
if (result === "timeout") {
|
||||
try {
|
||||
proc.kill()
|
||||
} catch {
|
||||
/* intentionally empty - process may have already exited */
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
timedOut: true,
|
||||
error: `bun install timed out after ${BUN_INSTALL_TIMEOUT_SECONDS} seconds. Try running manually: cd ${getConfigDir()} && bun i`,
|
||||
}
|
||||
}
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `bun install failed with exit code ${proc.exitCode}`,
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return {
|
||||
success: false,
|
||||
error: `bun install failed: ${message}. Is bun installed? Try: curl -fsSL https://bun.sh/install | bash`,
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/cli/config-manager/config-context.ts
Normal file
49
src/cli/config-manager/config-context.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { getOpenCodeConfigPaths } from "../../shared"
|
||||
import type {
|
||||
OpenCodeBinaryType,
|
||||
OpenCodeConfigPaths,
|
||||
} from "../../shared/opencode-config-dir-types"
|
||||
|
||||
export interface ConfigContext {
|
||||
binary: OpenCodeBinaryType
|
||||
version: string | null
|
||||
paths: OpenCodeConfigPaths
|
||||
}
|
||||
|
||||
let configContext: ConfigContext | null = null
|
||||
|
||||
export function initConfigContext(binary: OpenCodeBinaryType, version: string | null): void {
|
||||
const paths = getOpenCodeConfigPaths({ binary, version })
|
||||
configContext = { binary, version, paths }
|
||||
}
|
||||
|
||||
export function getConfigContext(): ConfigContext {
|
||||
if (!configContext) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.warn("[config-context] getConfigContext() called before initConfigContext(); defaulting to CLI paths.")
|
||||
}
|
||||
const paths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
configContext = { binary: "opencode", version: null, paths }
|
||||
}
|
||||
return configContext
|
||||
}
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
configContext = null
|
||||
}
|
||||
|
||||
export function getConfigDir(): string {
|
||||
return getConfigContext().paths.configDir
|
||||
}
|
||||
|
||||
export function getConfigJson(): string {
|
||||
return getConfigContext().paths.configJson
|
||||
}
|
||||
|
||||
export function getConfigJsonc(): string {
|
||||
return getConfigContext().paths.configJsonc
|
||||
}
|
||||
|
||||
export function getOmoConfigPath(): string {
|
||||
return getConfigContext().paths.omoConfig
|
||||
}
|
||||
30
src/cli/config-manager/deep-merge-record.ts
Normal file
30
src/cli/config-manager/deep-merge-record.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export function deepMergeRecord<TTarget extends Record<string, unknown>>(
|
||||
target: TTarget,
|
||||
source: Partial<TTarget>
|
||||
): TTarget {
|
||||
const result: TTarget = { ...target }
|
||||
|
||||
for (const key of Object.keys(source) as Array<keyof TTarget>) {
|
||||
if (key === "__proto__" || key === "constructor" || key === "prototype") continue
|
||||
const sourceValue = source[key]
|
||||
const targetValue = result[key]
|
||||
|
||||
if (
|
||||
sourceValue !== null &&
|
||||
typeof sourceValue === "object" &&
|
||||
!Array.isArray(sourceValue) &&
|
||||
targetValue !== null &&
|
||||
typeof targetValue === "object" &&
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
result[key] = deepMergeRecord(
|
||||
targetValue as Record<string, unknown>,
|
||||
sourceValue as Record<string, unknown>
|
||||
) as TTarget[keyof TTarget]
|
||||
} else if (sourceValue !== undefined) {
|
||||
result[key] = sourceValue as TTarget[keyof TTarget]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
78
src/cli/config-manager/detect-current-config.ts
Normal file
78
src/cli/config-manager/detect-current-config.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { parseJsonc } from "../../shared"
|
||||
import type { DetectedConfig } from "../types"
|
||||
import { getOmoConfigPath } from "./config-context"
|
||||
import { detectConfigFormat } from "./opencode-config-format"
|
||||
import { parseOpenCodeConfigFileWithError } from "./parse-opencode-config-file"
|
||||
|
||||
function detectProvidersFromOmoConfig(): {
|
||||
hasOpenAI: boolean
|
||||
hasOpencodeZen: boolean
|
||||
hasZaiCodingPlan: boolean
|
||||
hasKimiForCoding: boolean
|
||||
} {
|
||||
const omoConfigPath = getOmoConfigPath()
|
||||
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 {
|
||||
const result: DetectedConfig = {
|
||||
isInstalled: false,
|
||||
hasClaude: true,
|
||||
isMax20: true,
|
||||
hasOpenAI: true,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: true,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
if (format === "none") {
|
||||
return result
|
||||
}
|
||||
|
||||
const parseResult = parseOpenCodeConfigFileWithError(path)
|
||||
if (!parseResult.config) {
|
||||
return result
|
||||
}
|
||||
|
||||
const openCodeConfig = parseResult.config
|
||||
const plugins = openCodeConfig.plugin ?? []
|
||||
result.isInstalled = plugins.some((p) => p.startsWith("oh-my-opencode"))
|
||||
|
||||
if (!result.isInstalled) {
|
||||
return result
|
||||
}
|
||||
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
|
||||
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
|
||||
result.hasOpenAI = hasOpenAI
|
||||
result.hasOpencodeZen = hasOpencodeZen
|
||||
result.hasZaiCodingPlan = hasZaiCodingPlan
|
||||
result.hasKimiForCoding = hasKimiForCoding
|
||||
|
||||
return result
|
||||
}
|
||||
9
src/cli/config-manager/ensure-config-directory-exists.ts
Normal file
9
src/cli/config-manager/ensure-config-directory-exists.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { existsSync, mkdirSync } from "node:fs"
|
||||
import { getConfigDir } from "./config-context"
|
||||
|
||||
export function ensureConfigDirectoryExists(): void {
|
||||
const configDir = getConfigDir()
|
||||
if (!existsSync(configDir)) {
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
39
src/cli/config-manager/format-error-with-suggestion.ts
Normal file
39
src/cli/config-manager/format-error-with-suggestion.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
interface NodeError extends Error {
|
||||
code?: string
|
||||
}
|
||||
|
||||
function isPermissionError(err: unknown): boolean {
|
||||
const nodeErr = err as NodeError
|
||||
return nodeErr?.code === "EACCES" || nodeErr?.code === "EPERM"
|
||||
}
|
||||
|
||||
function isFileNotFoundError(err: unknown): boolean {
|
||||
const nodeErr = err as NodeError
|
||||
return nodeErr?.code === "ENOENT"
|
||||
}
|
||||
|
||||
export function formatErrorWithSuggestion(err: unknown, context: string): string {
|
||||
if (isPermissionError(err)) {
|
||||
return `Permission denied: Cannot ${context}. Try running with elevated permissions or check file ownership.`
|
||||
}
|
||||
|
||||
if (isFileNotFoundError(err)) {
|
||||
return `File not found while trying to ${context}. The file may have been deleted or moved.`
|
||||
}
|
||||
|
||||
if (err instanceof SyntaxError) {
|
||||
return `JSON syntax error while trying to ${context}: ${err.message}. Check for missing commas, brackets, or invalid characters.`
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
|
||||
if (message.includes("ENOSPC")) {
|
||||
return `Disk full: Cannot ${context}. Free up disk space and try again.`
|
||||
}
|
||||
|
||||
if (message.includes("EROFS")) {
|
||||
return `Read-only filesystem: Cannot ${context}. Check if the filesystem is mounted read-only.`
|
||||
}
|
||||
|
||||
return `Failed to ${context}: ${message}`
|
||||
}
|
||||
6
src/cli/config-manager/generate-omo-config.ts
Normal file
6
src/cli/config-manager/generate-omo-config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { InstallConfig } from "../types"
|
||||
import { generateModelConfig } from "../model-fallback"
|
||||
|
||||
export function generateOmoConfig(installConfig: InstallConfig): Record<string, unknown> {
|
||||
return generateModelConfig(installConfig)
|
||||
}
|
||||
11
src/cli/config-manager/jsonc-provider-editor.ts
Normal file
11
src/cli/config-manager/jsonc-provider-editor.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { modify, applyEdits } from "jsonc-parser"
|
||||
|
||||
export function modifyProviderInJsonc(
|
||||
content: string,
|
||||
newProviderValue: Record<string, unknown>
|
||||
): string {
|
||||
const edits = modify(content, ["provider"], newProviderValue, {
|
||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||
})
|
||||
return applyEdits(content, edits)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user