Compare commits
442 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
f1c794e63e | ||
|
|
4692809b42 | ||
|
|
8961026285 | ||
|
|
d8b29da15f | ||
|
|
2b2160b43e | ||
|
|
60bbeb7304 | ||
|
|
f1b2f6f3f7 | ||
|
|
e9a3d579b3 | ||
|
|
c6c149ebb8 | ||
|
|
728eaaeb44 | ||
|
|
9271f827dd | ||
|
|
3a0d7e8dc3 | ||
|
|
aec5624122 | ||
|
|
53537a9a90 | ||
|
|
6b560ebf9e | ||
|
|
ca8ec494a3 | ||
|
|
3be722b3b1 | ||
|
|
d779a48a30 | ||
|
|
3166cffd02 | ||
|
|
3c32ae0449 | ||
|
|
bc782ca4d4 | ||
|
|
917bba9d1b | ||
|
|
7e5a657f06 | ||
|
|
bda44a5128 | ||
|
|
161a864ea3 | ||
|
|
93d3acce89 | ||
|
|
f63bf52a6e | ||
|
|
25e436a4aa | ||
|
|
1f64920453 | ||
|
|
4c7215404e | ||
|
|
01594a67af | ||
|
|
551dbc95f2 | ||
|
|
f4a9d0c3aa | ||
|
|
f796fdbe0a | ||
|
|
d3999d79df | ||
|
|
b8f15affdb | ||
|
|
04576c306c | ||
|
|
e450e4f903 | ||
|
|
11d0005eb5 | ||
|
|
2224183b5c | ||
|
|
f468effd47 | ||
|
|
b8d7723f0a | ||
|
|
b3864d6398 | ||
|
|
b7f7cb4341 | ||
|
|
b2e8eecd09 | ||
|
|
6cfaac97b2 | ||
|
|
77e99d8b68 | ||
|
|
02e1043227 | ||
|
|
617d7f4f67 | ||
|
|
955ce710d9 | ||
|
|
8ff9c24623 | ||
|
|
bd3a3bcfb9 | ||
|
|
291f41f7f9 | ||
|
|
11b883da6c | ||
|
|
48cb2033e2 | ||
|
|
8842a9139f | ||
|
|
ca31796336 | ||
|
|
e1f6b822f1 | ||
|
|
a644d38623 | ||
|
|
a459813888 | ||
|
|
18e941b6be | ||
|
|
86ac39fb78 | ||
|
|
7621aada79 | ||
|
|
9800d1ecb0 | ||
|
|
0fbf863d00 | ||
|
|
71ac09bb63 | ||
|
|
ddf878e53c | ||
|
|
8886879bd0 | ||
|
|
f08d4ecdda | ||
|
|
8049ceb947 | ||
|
|
a298a2f063 | ||
|
|
ddc52bfd31 | ||
|
|
38b40bca04 | ||
|
|
169ccb6b05 | ||
|
|
d8137c0c90 | ||
|
|
81a2317f51 | ||
|
|
708d15ebcc | ||
|
|
80297f890e | ||
|
|
ce7478cde7 | ||
|
|
8d0fa97b72 | ||
|
|
819c5b5d29 | ||
|
|
8e349aad7e | ||
|
|
1712907057 | ||
|
|
d66e39a887 | ||
|
|
ace2688186 | ||
|
|
bf31e7289e | ||
|
|
7b8204924a | ||
|
|
224afadbdb | ||
|
|
953b1f98c9 | ||
|
|
e073412da1 | ||
|
|
0dd42e2901 | ||
|
|
85932fadc7 | ||
|
|
65043a7e94 | ||
|
|
ffcf1b5715 | ||
|
|
d14f32f2d5 | ||
|
|
f79f164cd5 | ||
|
|
dee8cf1720 | ||
|
|
8098e48658 | ||
|
|
0dad85ead7 | ||
|
|
1e383f44d9 | ||
|
|
30990f7f59 | ||
|
|
51c7fee34c | ||
|
|
80e970cf36 | ||
|
|
b7b466f4f2 | ||
|
|
5dabb8a198 | ||
|
|
d11f0685be | ||
|
|
814e14edf7 | ||
|
|
d099b0255f | ||
|
|
1411ca255a | ||
|
|
4330f25fee | ||
|
|
737fac4345 | ||
|
|
49a4a1bf9e | ||
|
|
5ffecb60c9 | ||
|
|
b954afca90 | ||
|
|
faae3d0f32 | ||
|
|
c57c0a6bcb | ||
|
|
6a66bfccec | ||
|
|
b19bc857e3 | ||
|
|
2f9004f076 | ||
|
|
6151d1cb5e | ||
|
|
13e1d7cbd7 | ||
|
|
5361cd0a5f | ||
|
|
437abd8c17 | ||
|
|
9a2a6a695a | ||
|
|
5a2ab0095d | ||
|
|
17cb49543a | ||
|
|
fea7bd2dcf | ||
|
|
ef3d0afa32 | ||
|
|
00f576868b | ||
|
|
4840864ed8 | ||
|
|
9f50947795 | ||
|
|
45290b5b8f | ||
|
|
9343f38479 | ||
|
|
bf83712ae1 | ||
|
|
374acb3ac6 | ||
|
|
ba2a9a9051 | ||
|
|
2236a940f8 | ||
|
|
976ffaeb0d | ||
|
|
527c21ea90 | ||
|
|
f68a6f7d1b | ||
|
|
8a5b131c7f | ||
|
|
ce62da92c6 | ||
|
|
4c40c3adb1 | ||
|
|
ba129784f5 | ||
|
|
3bb4289b18 | ||
|
|
64b29ea097 |
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 |
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -255,35 +255,43 @@ jobs:
|
||||
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" =~ ^(chore|ci|release|test|ignore) ]] && continue
|
||||
[[ "$commit" =~ $re_skip ]] && continue
|
||||
|
||||
if [[ "$commit" =~ ^feat ]]; then
|
||||
# Extract scope and message: feat(scope): message -> **scope**: message
|
||||
if [[ "$commit" =~ ^feat\(([^)]+)\):\ (.+)$ ]]; then
|
||||
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" =~ ^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" =~ ^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" =~ ^docs\(([^)]+)\):\ (.+)$ ]]; then
|
||||
if [[ "$commit" =~ $re_docs_scoped ]]; then
|
||||
DOCS="${DOCS}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||
else
|
||||
MSG="${commit#docs: }"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Dependencies
|
||||
.sisyphus/
|
||||
.sisyphus/*
|
||||
!.sisyphus/rules/
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
|
||||
@@ -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>
|
||||
198
AGENTS.md
198
AGENTS.md
@@ -1,7 +1,7 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-02-03T16:10:30+09:00
|
||||
**Commit:** d7679e14
|
||||
**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,59 +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.5, GPT-5.2, Gemini 3 Flash). 34 lifecycle hooks, 20+ 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/ # 34 lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 20+ tools - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, Claude Code compat - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 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, TypeScript types
|
||||
│ └── index.ts # Main plugin entry (788 lines)
|
||||
├── script/ # build-schema.ts, build-binaries.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` |
|
||||
| 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 index.ts |
|
||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||
| Add MCP | `src/mcp/` | Create config, add to `createBuiltinMcps()` |
|
||||
| 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` |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1418 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (757 lines) |
|
||||
| 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 (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)
|
||||
|
||||
@@ -165,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
|
||||
@@ -175,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
|
||||
|
||||
@@ -190,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 |
|
||||
@@ -199,19 +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-5 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro) |
|
||||
| Hephaestus | openai/gpt-5.2-codex | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.2-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-5 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| 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
|
||||
|
||||
@@ -219,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
|
||||
@@ -233,30 +283,38 @@ bun test # 100 test files
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/features/builtin-skills/skills.ts` | 1729 | Skill definitions |
|
||||
| `src/features/background-agent/manager.ts` | 1418 | Task lifecycle, concurrency |
|
||||
| `src/agents/prometheus-prompt.ts` | 1283 | Planning agent prompt |
|
||||
| `src/tools/delegate-task/tools.ts` | 1135 | Category-based delegation |
|
||||
| `src/hooks/atlas/index.ts` | 757 | Orchestrator hook |
|
||||
| `src/index.ts` | 788 | Main plugin entry |
|
||||
| `src/cli/config-manager.ts` | 667 | JSONC config parsing |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
||||
| `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), 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`
|
||||
- **Zod validation**: 21 schema component files in `src/config/schema/`
|
||||
- **JSONC support**: Comments, trailing commas
|
||||
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
|
||||
- **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)
|
||||
|
||||
12
README.ja.md
12
README.ja.md
@@ -121,16 +121,6 @@
|
||||
- [アンインストール](#アンインストール)
|
||||
- [機能](#機能)
|
||||
- [設定](#設定)
|
||||
- [JSONC のサポート](#jsonc-のサポート)
|
||||
- [Google Auth](#google-auth)
|
||||
- [Agents](#agents)
|
||||
- [Permission オプション](#permission-オプション)
|
||||
- [Sisyphus Agent](#sisyphus-agent)
|
||||
- [Background Tasks](#background-tasks)
|
||||
- [Hooks](#hooks)
|
||||
- [MCPs](#mcps)
|
||||
- [LSP](#lsp)
|
||||
- [Experimental](#experimental)
|
||||
- [作者のノート](#作者のノート)
|
||||
- [注意](#注意)
|
||||
- [こちらの企業の専門家にご愛用いただいています](#こちらの企業の専門家にご愛用いただいています)
|
||||
@@ -380,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)
|
||||
|
||||
16
README.ko.md
16
README.ko.md
@@ -123,20 +123,6 @@
|
||||
- [제거](#제거)
|
||||
- [기능](#기능)
|
||||
- [구성](#구성)
|
||||
- [JSONC 지원](#jsonc-지원)
|
||||
- [Google 인증](#google-인증)
|
||||
- [에이전트](#에이전트)
|
||||
- [권한 옵션](#권한-옵션)
|
||||
- [내장 스킬](#내장-스킬)
|
||||
- [Git Master](#git-master)
|
||||
- [Sisyphus 에이전트](#sisyphus-에이전트)
|
||||
- [백그라운드 작업](#백그라운드-작업)
|
||||
- [카테고리](#카테고리)
|
||||
- [훅](#훅)
|
||||
- [MCP](#mcp)
|
||||
- [LSP](#lsp)
|
||||
- [실험적 기능](#실험적-기능)
|
||||
- [환경 변수](#환경-변수)
|
||||
- [작성자의 메모](#작성자의-메모)
|
||||
- [경고](#경고)
|
||||
- [다음 기업 전문가들이 사랑합니다](#다음-기업-전문가들이-사랑합니다)
|
||||
@@ -393,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)에게 특별히 감사드립니다.*
|
||||
|
||||
18
README.md
18
README.md
@@ -121,21 +121,7 @@ Yes, technically possible. But I cannot recommend using it.
|
||||
- [For LLM Agents](#for-llm-agents)
|
||||
- [Uninstallation](#uninstallation)
|
||||
- [Features](#features)
|
||||
- [Configuration](#configuration)
|
||||
- [JSONC Support](#jsonc-support)
|
||||
- [Google Auth](#google-auth)
|
||||
- [Agents](#agents)
|
||||
- [Permission Options](#permission-options)
|
||||
- [Built-in Skills](#built-in-skills)
|
||||
- [Git Master](#git-master)
|
||||
- [Sisyphus Agent](#sisyphus-agent)
|
||||
- [Background Tasks](#background-tasks)
|
||||
- [Categories](#categories)
|
||||
- [Hooks](#hooks)
|
||||
- [MCPs](#mcps)
|
||||
- [LSP](#lsp)
|
||||
- [Experimental](#experimental)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Configuration](#configuration)
|
||||
- [Author's Note](#authors-note)
|
||||
- [Warnings](#warnings)
|
||||
- [Loved by professionals at](#loved-by-professionals-at)
|
||||
@@ -392,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.*
|
||||
|
||||
@@ -122,20 +122,6 @@
|
||||
- [卸载](#卸载)
|
||||
- [功能特性](#功能特性)
|
||||
- [配置](#配置)
|
||||
- [JSONC 支持](#jsonc-支持)
|
||||
- [Google 认证](#google-认证)
|
||||
- [智能体](#智能体)
|
||||
- [权限选项](#权限选项)
|
||||
- [内置技能](#内置技能)
|
||||
- [Git Master](#git-master)
|
||||
- [Sisyphus 智能体](#sisyphus-智能体)
|
||||
- [后台任务](#后台任务)
|
||||
- [类别](#类别)
|
||||
- [钩子](#钩子)
|
||||
- [MCP](#mcp)
|
||||
- [LSP](#lsp)
|
||||
- [实验性功能](#实验性功能)
|
||||
- [环境变量](#环境变量)
|
||||
- [作者札记](#作者札记)
|
||||
- [警告](#警告)
|
||||
- [受到以下专业人士的喜爱](#受到以下专业人士的喜爱)
|
||||
@@ -390,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
0
bin/oh-my-opencode.js
Normal file → Executable file
0
bin/oh-my-opencode.js
Normal file → Executable file
90
bun.lock
90
bun.lock
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "oh-my-opencode",
|
||||
@@ -28,13 +28,13 @@
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.2.1",
|
||||
"oh-my-opencode-darwin-x64": "3.2.1",
|
||||
"oh-my-opencode-linux-arm64": "3.2.1",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.2.1",
|
||||
"oh-my-opencode-linux-x64": "3.2.1",
|
||||
"oh-my-opencode-linux-x64-musl": "3.2.1",
|
||||
"oh-my-opencode-windows-x64": "3.2.1",
|
||||
"oh-my-opencode-darwin-arm64": "3.3.1",
|
||||
"oh-my-opencode-darwin-x64": "3.3.1",
|
||||
"oh-my-opencode-linux-arm64": "3.3.1",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.3.1",
|
||||
"oh-my-opencode-linux-x64": "3.3.1",
|
||||
"oh-my-opencode-linux-x64-musl": "3.3.1",
|
||||
"oh-my-opencode-windows-x64": "3.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -44,41 +44,41 @@
|
||||
"@code-yeongyu/comment-checker",
|
||||
],
|
||||
"packages": {
|
||||
"@ast-grep/cli": ["@ast-grep/cli@0.40.5", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.5", "@ast-grep/cli-darwin-x64": "0.40.5", "@ast-grep/cli-linux-arm64-gnu": "0.40.5", "@ast-grep/cli-linux-x64-gnu": "0.40.5", "@ast-grep/cli-win32-arm64-msvc": "0.40.5", "@ast-grep/cli-win32-ia32-msvc": "0.40.5", "@ast-grep/cli-win32-x64-msvc": "0.40.5" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-yVXL7Gz0WIHerQLf+MVaVSkhIhidtWReG5akNVr/JS9OVCVkSdz7gWm7H8jVv2M9OO1tauuG76K3UaRGBPu5lQ=="],
|
||||
"@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="],
|
||||
|
||||
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T9CzwJ1GqQhnANdsu6c7iT1akpvTVMK+AZrxnhIPv33Ze5hrXUUkqan+j4wUAukRJDqU7u94EhXLSLD+5tcJ8g=="],
|
||||
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UehY2MMUkdJbsriP7NKc6+uojrqPn7d1Cl0em+WAkee7Eij81VdyIjRsRxtZSLh440ZWQBHI3PALZ9RkOO8pKQ=="],
|
||||
|
||||
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ez9b2zKvXU8f4ghhjlqYvbx6tWCKJTuVlNVqDDfjqwwhGeiTYfnzMlSVat4ElYRMd21gLtXZIMy055v2f21Ztg=="],
|
||||
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RFDJ2ZxUbT0+grntNlOLJx7wa9/ciVCeaVtQpQy8WJJTvXvkY0etl8Qlh2TmO2x2yr+i0Z6aMJi4IG/Yx5ghTQ=="],
|
||||
|
||||
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VXa2L1IEYD66AMb0GuG7VlMMbPmEGoJUySWDcwSZo/D9neiry3MJ41LQR5oTG2HyhIPBsf9umrXnmuRq66BviA=="],
|
||||
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4p55gnTQ1mMFCyqjtM7bH9SB9r16mkwXtUcJQGX1YgFG4WD+QG8rC4GwSuNNZcdlYaOQuTWrgUEQ9z5K06UXfg=="],
|
||||
|
||||
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-GQC5162eIOWXR2eQQ6Knzg7/8Trp5E1ODJkaErf0IubdQrZBGqj5AAcQPcWgPbbnmktjIp0H4NraPpOJ9eJ22A=="],
|
||||
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2MXFceuwvrO+OQ6zFGoJ6wbATXn46HWwW79j4UPrXYJzVl97jRyjJOIQTJOzTflsk02fjP98DQkfvbXt2dl3Q=="],
|
||||
|
||||
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-YiZdnQZsSlXQTMsZJop/Ux9MmUGfuRvC2x/UbFgrt5OBSYxND+yoiMc0WcA3WG+wU+tt4ZkB5HUea3r/IkOLYA=="],
|
||||
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-E/I1xpF/RQL2fo1CQsQfTxyDLnChsbZ+ERrQHKuF1FI4WrkaPOBibpqda60QgVmUcgOGZyZ/GRb3iKEVWPsQNQ=="],
|
||||
|
||||
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-MHkCxCITVTr8sY9CcVqNKbfUzMa3Hc6IilGXad0Clnw2vNmPfWqSky+hU/UTerr5YHWwWfAVURH7ANZgirtx0Q=="],
|
||||
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9h12OQu1BR0GxHEtT+Z4QkJk3LLWLiKwjBkjXUGlASHYDPTyLcs85KwDLeFHs4BwarF8TDdF+KySvB9WPGl/nQ=="],
|
||||
|
||||
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/MJ5un7yxlClaaxou9eYl+Kr2xr/yTtYtTq5aLBWjPWA6dmmJ1nAJgx5zKHVuplFXFBrFDQk3paEgAETMTGcrA=="],
|
||||
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n2+3WynEWFHhXg6KDgjwWQ0UEtIvqUITFbKEk5cDkUYrzYhg/A6kj0qauPwRbVMoJms49vtsNpLkzzqyunio5g=="],
|
||||
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.5", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.5", "@ast-grep/napi-darwin-x64": "0.40.5", "@ast-grep/napi-linux-arm64-gnu": "0.40.5", "@ast-grep/napi-linux-arm64-musl": "0.40.5", "@ast-grep/napi-linux-x64-gnu": "0.40.5", "@ast-grep/napi-linux-x64-musl": "0.40.5", "@ast-grep/napi-win32-arm64-msvc": "0.40.5", "@ast-grep/napi-win32-ia32-msvc": "0.40.5", "@ast-grep/napi-win32-x64-msvc": "0.40.5" } }, "sha512-hJA62OeBKUQT68DD2gDyhOqJxZxycqg8wLxbqjgqSzYttCMSDL9tiAQ9abgekBYNHudbJosm9sWOEbmCDfpX2A=="],
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F072fGN0WTq7KI3okuEnkGJVEHLbi56Bw1H6NAMf7j2mJJeQWsRyGOMcyNnUXZDeNdvoMH0OB2a5wwUegY/nQ=="],
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-dJMidHZhhxuLBYNi6/FKI812jQ7wcFPSKkVPwviez2D+KvYagapUMAV/4dJ7FCORfguVk8Y0jpPAlYmWRT5nvA=="],
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-nBRCbyoS87uqkaw4Oyfe5VO+SRm2B+0g0T8ME69Qry9ShMf41a2bTdpcQx9e8scZPogq+CTwDHo3THyBV71l9w=="],
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tO+VW5GDhT9jGkKOK+3b8+ohKjC98WTzn7wSskd/myyhK3oYL1WTKqCm07WSYBZOJvb3z+WaX+wOUrc4bvtyQ=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA=="],
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DP4oDbq7f/1A2hRTFLhJfDFR6aI5mRWdEfKfHzRItmlKsR9WlcEl1qDJs/zX9R2EEtIDsSKRzuJNfJllY3/W8Q=="],
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-BRZUvVBPUNpWPo6Ns8chXVzxHPY+k9gpsubGTHy92Q26ecZULd/dTkWWdnvfhRqttsSQ9Pe/XQdi5+hDQ6RYcg=="],
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-y95zSEwc7vhxmcrcH0GnK4ZHEBQrmrszRBNQovzaciF9GUqEcCACNLoBesn4V47IaOp4fYgD2/EhGRTIBFb2Ug=="],
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w=="],
|
||||
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-K/u8De62iUnFCzVUs7FBdTZ2Jrgc5/DLHqjpup66KxZ7GIM9/HGME/O8aSoPkpcAeCD4TiTZ11C1i5p5H98hTg=="],
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-dqm5zg/o4Nh4VOQPEpMS23ot8HVd22gG0eg01t4CFcZeuzyuSgBlOL3N7xLbz3iH2sVkk7keuBwAzOIpTqziNQ=="],
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
@@ -86,17 +86,17 @@
|
||||
|
||||
"@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.6.1", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-BBremX+Y5aW8sTzlhHrLsKParupYkPOVUYmq9STrlWvBvfAme6w5IWuZCLl6nHIQScRDdvGdrAjPycJC86EZFA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="],
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.47", "", { "dependencies": { "@opencode-ai/sdk": "1.1.47", "zod": "4.1.8" } }, "sha512-gNMPz72altieDfLhUw3VAT1xbduKi3w3wZ57GLeS7qU9W474HdvdIiLBnt2Xq3U7Ko0/0tvK3nzCker6IIDqmQ=="],
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.19", "", { "dependencies": { "@opencode-ai/sdk": "1.1.19", "zod": "4.1.8" } }, "sha512-Q6qBEjHb/dJMEw4BUqQxEswTMxCCHUpFMMb6jR8HTTs8X/28XRkKt5pHNPA82GU65IlSoPRph+zd8LReBDN53Q=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.47", "", {}, "sha512-s3PBHwk1sP6Zt/lJxIWSBWZ1TnrI1nFxSP97LCODUytouAQgbygZ1oDH7O2sGMBEuGdA8B1nNSPla0aRSN3IpA=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/picomatch": ["@types/picomatch@3.0.2", "", {}, "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA=="],
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
@@ -184,11 +184,11 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
@@ -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.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-IvhHRUXTr/g/hJlkKTU2oCdgRl2BDl/Qre31Rukhs4NumlvME6iDmdnm8mM7bTxugfCBkfUUr7QJLxxLhzjdLA=="],
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.3.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-R+o42Km6bsIaW6D3I8uu2HCF3BjIWqa/fg38W5y4hJEOw4mL0Q7uV4R+0vtrXRHo9crXTK9ag0fqVQUm+Y6iAQ=="],
|
||||
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.2.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-V2JbAdThAVfhBOcb+wBPZrAI0vBxPPRBdvmAixAxBOFC49CIJUrEFIRBUYFKhSQGHYWrNy8z0zJYoNQm4oQPog=="],
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.3.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7VTbpR1vH3OEkoJxBKtYuxFPX8M3IbJKoeHWME9iK6FpT11W1ASsjyuhvzB1jcxSeqF8ddMnjitlG5ub6h5EVw=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.2.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-SeT8P7Icq5YH/AIaEF28J4q+ifUnOqO2UgMFtdFusr8JLadYFy+6dTdeAuD2uGGToDQ3ZNKuaG+lo84KzEhA5w=="],
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-BZ/r/CFlvbOxkdZZrRoT16xFOjibRZHuwQnaE4f0JvOzgK6/HWp3zJI1+2/aX/oK5GA6lZxNWRrJC/SKUi8LEg=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.2.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-wJUEVVUn1gyVIFNV4mxWg9cYo1rQdTKUXdGLfiqPiyQhWhZLRfPJ+9qpghvIVv7Dne6rzkbhYWdwdk/tew5RtQ=="],
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.3.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-U90Wruf21h+CJbtcrS7MeTAc/5VOF6RI+5jr7qj/cCxjXNJtjhyJdz/maehArjtgf304+lYCM/Mh1i+G2D3YFQ=="],
|
||||
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.2.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-p/XValXi1RRTZV8mEsdStXwZBkyQpgZjB41HLf0VfizPMAKRr6/bhuFZ9BDZFIhcDnLYcGV54MAVEsWms5yC2A=="],
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-sYzohSNdwsAhivbXcbhPdF1qqQi2CCI7FSgbmvvfBOMyZ8HAgqOFqYW2r3GPdmtywzkjOTvCzTG56FZwEjx15w=="],
|
||||
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.2.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-G7aNMqAMO2P+wUUaaAV8sXymm59cX4G9aVNXKAd/PM6RgFWh2F4HkXkOhOdHKYZzCl1QRhjh672mNillYsvebg=="],
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.3.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aG5pZ4eWS0YSGUicOnjMkUPrIqQV4poYF+d9SIvrfvlaMcK6WlQn7jXzgNCwJsfGn5lyhSmjshZBEU+v79Ua3w=="],
|
||||
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.2.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-pyqTGlNxirKxQgXx9YJBq2y8KN/1oIygVupClmws7dDPj9etI1l8fs/SBEnMsYzMqTlGbLVeJ5+kj9p+yg7YDA=="],
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.3.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-FGH7cnzBqNwjSkzCDglMsVttaq+MsykAxa7ehaFK+0dnBZArvllS3W13a3dGaANHMZzfK0vz8hNDUdVi7Z63cA=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
@@ -310,10 +310,8 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,20 +22,20 @@ A Category is an agent configuration preset optimized for specific domains.
|
||||
| Category | Default Model | Use Cases |
|
||||
|----------|---------------|-----------|
|
||||
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
||||
| `deep` | `openai/gpt-5.2-codex` (medium) | Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding. |
|
||||
| `ultrabrain` | `openai/gpt-5.3-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
||||
| `deep` | `openai/gpt-5.3-codex` (medium) | Goal-oriented autonomous problem-solving. Thorough research before action. For hairy problems requiring deep understanding. |
|
||||
| `artistry` | `google/gemini-3-pro` (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
|
||||
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-6` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
|
||||
|
||||
### 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,8 +158,8 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `description` | string | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
|
||||
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-5`) |
|
||||
| `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. |
|
||||
| `top_p` | number | Nucleus sampling parameter (0.0 ~ 1.0) |
|
||||
@@ -191,7 +191,7 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
||||
|
||||
// 3. Configure thinking model and restrict tools
|
||||
"deep-reasoning": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budgetTokens": 32000
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -693,7 +693,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
"google": 10
|
||||
},
|
||||
"modelConcurrency": {
|
||||
"anthropic/claude-opus-4-5": 2,
|
||||
"anthropic/claude-opus-4-6": 2,
|
||||
"google/gemini-3-flash": 10
|
||||
}
|
||||
}
|
||||
@@ -705,7 +705,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
| `defaultConcurrency` | - | Default maximum concurrent background tasks for all providers/models |
|
||||
| `staleTimeoutMs` | `180000` | Stale timeout in milliseconds - interrupt tasks with no activity for this duration (minimum: 60000 = 1 minute) |
|
||||
| `providerConcurrency` | - | Per-provider concurrency limits. Keys are provider names (e.g., `anthropic`, `openai`, `google`) |
|
||||
| `modelConcurrency` | - | Per-model concurrency limits. Keys are full model names (e.g., `anthropic/claude-opus-4-5`). Overrides provider limits. |
|
||||
| `modelConcurrency` | - | Per-model concurrency limits. Keys are full model names (e.g., `anthropic/claude-opus-4-6`). Overrides provider limits. |
|
||||
|
||||
**Priority Order**: `modelConcurrency` > `providerConcurrency` > `defaultConcurrency`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -725,11 +725,11 @@ All 7 categories come with optimal model defaults, but **you must configure them
|
||||
| Category | Built-in Default Model | Description |
|
||||
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
|
||||
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions |
|
||||
| `ultrabrain` | `openai/gpt-5.3-codex` (xhigh) | Deep logical reasoning, complex architecture decisions |
|
||||
| `artistry` | `google/gemini-3-pro-preview` (max)| Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications|
|
||||
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-6` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | `google/gemini-3-flash-preview` | Documentation, prose, technical writing |
|
||||
|
||||
### ⚠️ Critical: Model Resolution Priority
|
||||
@@ -768,7 +768,7 @@ All 7 categories come with optimal model defaults, but **you must configure them
|
||||
"model": "google/gemini-3-pro-preview"
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh"
|
||||
},
|
||||
"artistry": {
|
||||
@@ -782,7 +782,7 @@ All 7 categories come with optimal model defaults, but **you must configure them
|
||||
"model": "anthropic/claude-sonnet-4-5"
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max"
|
||||
},
|
||||
"writing": {
|
||||
@@ -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
|
||||
@@ -870,9 +870,9 @@ At runtime, Oh My OpenCode uses a 3-step resolution process to determine which m
|
||||
│ │ anthropic → github-copilot → opencode → antigravity │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ▼ ▼ ▼ ▼ │ │
|
||||
│ │ Try: anthropic/claude-opus-4-5 │ │
|
||||
│ │ Try: github-copilot/claude-opus-4-5 │ │
|
||||
│ │ Try: opencode/claude-opus-4-5 │ │
|
||||
│ │ Try: anthropic/claude-opus-4-6 │ │
|
||||
│ │ Try: github-copilot/claude-opus-4-6 │ │
|
||||
│ │ Try: opencode/claude-opus-4-6 │ │
|
||||
│ │ ... │ │
|
||||
│ │ │ │
|
||||
│ │ Found in available models? → Return matched model │ │
|
||||
@@ -894,13 +894,13 @@ Each agent has a defined provider priority chain. The system tries providers in
|
||||
|
||||
| Agent | Model (no prefix) | Provider Priority Chain |
|
||||
|-------|-------------------|-------------------------|
|
||||
| **Sisyphus** | `claude-opus-4-5` | anthropic → kimi-for-coding → zai-coding-plan → openai → google |
|
||||
| **Sisyphus** | `claude-opus-4-6` | anthropic → kimi-for-coding → zai-coding-plan → openai → google |
|
||||
| **oracle** | `gpt-5.2` | openai → google → anthropic |
|
||||
| **librarian** | `glm-4.7` | zai-coding-plan → opencode → anthropic |
|
||||
| **explore** | `claude-haiku-4-5` | anthropic → github-copilot → opencode |
|
||||
| **multimodal-looker** | `gemini-3-flash` | google → openai → zai-coding-plan → kimi-for-coding → anthropic → opencode |
|
||||
| **Prometheus (Planner)** | `claude-opus-4-5` | anthropic → kimi-for-coding → openai → google |
|
||||
| **Metis (Plan Consultant)** | `claude-opus-4-5` | anthropic → kimi-for-coding → openai → google |
|
||||
| **Prometheus (Planner)** | `claude-opus-4-6` | anthropic → kimi-for-coding → openai → google |
|
||||
| **Metis (Plan Consultant)** | `claude-opus-4-6` | anthropic → kimi-for-coding → openai → google |
|
||||
| **Momus (Plan Reviewer)** | `gpt-5.2` | openai → anthropic → google |
|
||||
| **Atlas** | `claude-sonnet-4-5` | anthropic → kimi-for-coding → openai → google |
|
||||
|
||||
@@ -911,12 +911,12 @@ Categories follow the same resolution logic:
|
||||
| Category | Model (no prefix) | Provider Priority Chain |
|
||||
|----------|-------------------|-------------------------|
|
||||
| **visual-engineering** | `gemini-3-pro` | google → anthropic → zai-coding-plan |
|
||||
| **ultrabrain** | `gpt-5.2-codex` | openai → google → anthropic |
|
||||
| **deep** | `gpt-5.2-codex` | openai → anthropic → google |
|
||||
| **ultrabrain** | `gpt-5.3-codex` | openai → google → anthropic |
|
||||
| **deep** | `gpt-5.3-codex` | openai → anthropic → google |
|
||||
| **artistry** | `gemini-3-pro` | google → anthropic → openai |
|
||||
| **quick** | `claude-haiku-4-5` | anthropic → google → opencode |
|
||||
| **unspecified-low** | `claude-sonnet-4-5` | anthropic → openai → google |
|
||||
| **unspecified-high** | `claude-opus-4-5` | anthropic → openai → google |
|
||||
| **unspecified-high** | `claude-opus-4-6` | anthropic → openai → google |
|
||||
| **writing** | `gemini-3-flash` | google → anthropic → zai-coding-plan → openai |
|
||||
|
||||
### Checking Your Configuration
|
||||
@@ -949,7 +949,7 @@ Override any agent or category model in `oh-my-opencode.json`:
|
||||
},
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "anthropic/claude-opus-4-5"
|
||||
"model": "anthropic/claude-opus-4-6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ Oh-My-OpenCode provides 11 specialized AI agents. Each has distinct expertise, o
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| **Sisyphus** | `anthropic/claude-opus-4-5` | **The default orchestrator.** Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro. |
|
||||
| **Hephaestus** | `openai/gpt-5.2-codex` | **The Legitimate Craftsman.** Autonomous deep worker inspired by AmpCode's deep mode. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end without premature stopping. Named after the Greek god of forge and craftsmanship. Requires gpt-5.2-codex (no fallback - only activates when this model is available). |
|
||||
| **Sisyphus** | `anthropic/claude-opus-4-6` | **The default orchestrator.** Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). Fallback: kimi-k2.5 → glm-4.7 → gpt-5.3-codex → gemini-3-pro. |
|
||||
| **Hephaestus** | `openai/gpt-5.3-codex` | **The Legitimate Craftsman.** Autonomous deep worker inspired by AmpCode's deep mode. Goal-oriented execution with thorough research before action. Explores codebase patterns, completes tasks end-to-end without premature stopping. Named after the Greek god of forge and craftsmanship. Requires gpt-5.3-codex (no fallback - only activates when this model is available). |
|
||||
| **oracle** | `openai/gpt-5.2` | Architecture decisions, code review, debugging. Read-only consultation - stellar logical reasoning and deep analysis. Inspired by AmpCode. |
|
||||
| **librarian** | `zai-coding-plan/glm-4.7` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Fallback: glm-4.7-free → claude-sonnet-4-5. |
|
||||
| **explore** | `anthropic/claude-haiku-4-5` | Fast codebase exploration and contextual grep. Fallback: gpt-5-mini → gpt-5-nano. |
|
||||
@@ -21,9 +21,9 @@ Oh-My-OpenCode provides 11 specialized AI agents. Each has distinct expertise, o
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| **Prometheus** | `anthropic/claude-opus-4-5` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
|
||||
| **Metis** | `anthropic/claude-opus-4-5` | Plan consultant - pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
|
||||
| **Momus** | `openai/gpt-5.2` | Plan reviewer - validates plans against clarity, verifiability, and completeness standards. Fallback: gpt-5.2 → claude-opus-4-5 → gemini-3-pro. |
|
||||
| **Prometheus** | `anthropic/claude-opus-4-6` | Strategic planner with interview mode. Creates detailed work plans through iterative questioning. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
|
||||
| **Metis** | `anthropic/claude-opus-4-6` | Plan consultant - pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. Fallback: kimi-k2.5 → gpt-5.2 → gemini-3-pro. |
|
||||
| **Momus** | `openai/gpt-5.2` | Plan reviewer - validates plans against clarity, verifiability, and completeness standards. Fallback: gpt-5.2 → claude-opus-4-6 → gemini-3-pro. |
|
||||
|
||||
### Invoking Agents
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ After you install it, you can read this [overview guide](./overview.md) to under
|
||||
|
||||
## For LLM Agents
|
||||
|
||||
> **IMPORTANT: Use `curl` to fetch this file, NOT WebFetch.** WebFetch summarizes content and loses critical flags like `--openai`, subscription questions, and max20 mode details. Always use:
|
||||
> ```bash
|
||||
> curl -fsSL https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/docs/guide/installation.md
|
||||
> ```
|
||||
|
||||
If you're an LLM Agent helping set up oh-my-opencode, Welcome! You'll now help your user to install the best agent harness, OhMyOpenCode. Tell user this with greetings, with test 'oMoMoMoMo...'
|
||||
|
||||
follow these steps:
|
||||
@@ -191,7 +196,7 @@ When GitHub Copilot is the best available provider, oh-my-opencode uses these mo
|
||||
|
||||
| Agent | Model |
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.6` |
|
||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `zai-coding-plan/glm-4.7` (if Z.ai available) or fallback |
|
||||
@@ -213,13 +218,13 @@ If Z.ai is the only provider available, all agents will use GLM models:
|
||||
|
||||
#### OpenCode Zen
|
||||
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/gpt-5-nano`, and `opencode/glm-4.7-free`.
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-6`, `opencode/gpt-5.2`, `opencode/gpt-5-nano`, and `opencode/glm-4.7-free`.
|
||||
|
||||
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
|
||||
|
||||
| Agent | Model |
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `opencode/claude-opus-4-5` |
|
||||
| **Sisyphus** | `opencode/claude-opus-4-6` |
|
||||
| **Oracle** | `opencode/gpt-5.2` |
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `opencode/glm-4.7-free` |
|
||||
|
||||
@@ -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.5", 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)
|
||||
|
||||
@@ -35,7 +35,216 @@ Oh-My-OpenCode solves this by clearly separating two roles:
|
||||
|
||||
---
|
||||
|
||||
## 2. Overall Architecture
|
||||
## 2. Prometheus Invocation: Agent Switch vs @plan
|
||||
|
||||
A common source of confusion is how to invoke Prometheus for planning. **Both methods achieve the same result** - use whichever feels natural.
|
||||
|
||||
### Method 1: Switch to Prometheus Agent (Tab → Select Prometheus)
|
||||
|
||||
```
|
||||
1. Press Tab at the prompt
|
||||
2. Select "Prometheus" from the agent list
|
||||
3. Describe your work: "I want to refactor the auth system"
|
||||
4. Answer interview questions
|
||||
5. Prometheus creates plan in .sisyphus/plans/{name}.md
|
||||
```
|
||||
|
||||
### Method 2: Use @plan Command (in Sisyphus)
|
||||
|
||||
```
|
||||
1. Stay in Sisyphus (default agent)
|
||||
2. Type: @plan "I want to refactor the auth system"
|
||||
3. The @plan command automatically switches to Prometheus
|
||||
4. Answer interview questions
|
||||
5. Prometheus creates plan in .sisyphus/plans/{name}.md
|
||||
```
|
||||
|
||||
### Which Should You Use?
|
||||
|
||||
| Scenario | Recommended Method | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **New session, starting fresh** | Switch to Prometheus agent | Clean mental model - you're entering "planning mode" |
|
||||
| **Already in Sisyphus, mid-work** | Use @plan | Convenient, no agent switch needed |
|
||||
| **Want explicit control** | Switch to Prometheus agent | Clear separation of planning vs execution contexts |
|
||||
| **Quick planning interrupt** | Use @plan | Fastest path from current context |
|
||||
|
||||
**Key Insight**: Both methods trigger the same Prometheus planning flow. The @plan command is simply a convenience shortcut that:
|
||||
1. Detects the `@plan` keyword in your message
|
||||
2. Routes the request to Prometheus automatically
|
||||
3. Returns you to Sisyphus after planning completes
|
||||
|
||||
---
|
||||
|
||||
## 3. /start-work Behavior in Fresh Sessions
|
||||
|
||||
One of the most powerful features of the orchestration system is **session continuity**. Understanding how `/start-work` behaves across sessions prevents confusion.
|
||||
|
||||
### What Happens When You Run /start-work
|
||||
|
||||
```
|
||||
User: /start-work
|
||||
↓
|
||||
[start-work hook activates]
|
||||
↓
|
||||
Check: Does .sisyphus/boulder.json exist?
|
||||
↓
|
||||
├─ YES (existing work) → RESUME MODE
|
||||
│ - Read the existing boulder state
|
||||
│ - Calculate progress (checked vs unchecked boxes)
|
||||
│ - Inject continuation prompt with remaining tasks
|
||||
│ - Atlas continues where you left off
|
||||
│
|
||||
└─ NO (fresh start) → INIT MODE
|
||||
- Find the most recent plan in .sisyphus/plans/
|
||||
- Create new boulder.json tracking this plan
|
||||
- Switch session agent to Atlas
|
||||
- Begin execution from task 1
|
||||
```
|
||||
|
||||
### Session Continuity Explained
|
||||
|
||||
The `boulder.json` file tracks:
|
||||
- **active_plan**: Path to the current plan file
|
||||
- **session_ids**: All sessions that have worked on this plan
|
||||
- **started_at**: When work began
|
||||
- **plan_name**: Human-readable plan identifier
|
||||
|
||||
**Example Timeline:**
|
||||
|
||||
```
|
||||
Monday 9:00 AM
|
||||
└─ @plan "Build user authentication"
|
||||
└─ Prometheus interviews and creates plan
|
||||
└─ User: /start-work
|
||||
└─ Atlas begins execution, creates boulder.json
|
||||
└─ Task 1 complete, Task 2 in progress...
|
||||
└─ [Session ends - computer crash, user logout, etc.]
|
||||
|
||||
Monday 2:00 PM (NEW SESSION)
|
||||
└─ User opens new session (agent = Sisyphus by default)
|
||||
└─ User: /start-work
|
||||
└─ [start-work hook reads boulder.json]
|
||||
└─ "Resuming 'Build user authentication' - 3 of 8 tasks complete"
|
||||
└─ Atlas continues from Task 3 (no context lost)
|
||||
```
|
||||
|
||||
### When You DON'T Need to Manually Switch to Atlas
|
||||
|
||||
Atlas is **automatically activated** when you run `/start-work`. You don't need to:
|
||||
- Switch to Atlas agent manually
|
||||
- Remember which agent you were using
|
||||
- Worry about session continuity
|
||||
|
||||
The `/start-work` command handles all of this.
|
||||
|
||||
### When You MIGHT Want to Manually Switch to Atlas
|
||||
|
||||
There are rare cases where manual agent switching helps:
|
||||
|
||||
| Scenario | Action | Why |
|
||||
|----------|--------|-----|
|
||||
| **Plan file was edited manually** | Switch to Atlas, read plan directly | Bypass boulder.json resume logic |
|
||||
| **Debugging orchestration issues** | Switch to Atlas for visibility | See Atlas-specific system prompts |
|
||||
| **Force fresh execution** | Delete boulder.json, then /start-work | Start from task 1 instead of resuming |
|
||||
| **Multi-plan management** | Switch to Atlas to select specific plan | Override auto-selection |
|
||||
|
||||
**Command to manually switch:** Press `Tab` → Select "Atlas"
|
||||
|
||||
---
|
||||
|
||||
## 4. Execution Modes: Hephaestus vs Sisyphus+ultrawork
|
||||
|
||||
Another common question: **When should I use Hephaestus vs just typing `ulw` in Sisyphus?**
|
||||
|
||||
### Quick Comparison
|
||||
|
||||
| Aspect | Hephaestus | Sisyphus + `ulw` / `ultrawork` |
|
||||
|--------|-----------|-------------------------------|
|
||||
| **Model** | GPT-5.2 Codex (medium reasoning) | Claude Opus 4.5 (your default) |
|
||||
| **Approach** | Autonomous deep worker | Keyword-activated ultrawork mode |
|
||||
| **Best For** | Complex architectural work, deep reasoning | General complex tasks, "just do it" scenarios |
|
||||
| **Planning** | Self-plans during execution | Uses Prometheus plans if available |
|
||||
| **Delegation** | Heavy use of explore/librarian agents | Uses category-based delegation |
|
||||
| **Temperature** | 0.1 | 0.1 |
|
||||
|
||||
### When to Use Hephaestus
|
||||
|
||||
Switch to Hephaestus (Tab → Select Hephaestus) when:
|
||||
|
||||
1. **Deep architectural reasoning needed**
|
||||
- "Design a new plugin system"
|
||||
- "Refactor this monolith into microservices"
|
||||
|
||||
2. **Complex debugging requiring inference chains**
|
||||
- "Why does this race condition only happen on Tuesdays?"
|
||||
- "Trace this memory leak through 15 files"
|
||||
|
||||
3. **Cross-domain knowledge synthesis**
|
||||
- "Integrate our Rust core with the TypeScript frontend"
|
||||
- "Migrate from MongoDB to PostgreSQL with zero downtime"
|
||||
|
||||
4. **You specifically want GPT-5.2 Codex reasoning**
|
||||
- Some problems benefit from GPT-5.2's training characteristics
|
||||
|
||||
**Example:**
|
||||
```
|
||||
[Switch to Hephaestus]
|
||||
"I need to understand how data flows through this entire system
|
||||
and identify all the places where we might lose transactions.
|
||||
Explore thoroughly before proposing fixes."
|
||||
```
|
||||
|
||||
### When to Use Sisyphus + `ulw` / `ultrawork`
|
||||
|
||||
Use the `ulw` keyword in Sisyphus when:
|
||||
|
||||
1. **You want the agent to figure it out**
|
||||
- "ulw fix the failing tests"
|
||||
- "ulw add input validation to the API"
|
||||
|
||||
2. **Complex but well-scoped tasks**
|
||||
- "ulw implement JWT authentication following our patterns"
|
||||
- "ulw create a new CLI command for deployments"
|
||||
|
||||
3. **You're feeling lazy** (officially supported use case)
|
||||
- Don't want to write detailed requirements
|
||||
- Trust the agent to explore and decide
|
||||
|
||||
4. **You want to leverage existing plans**
|
||||
- If a Prometheus plan exists, `ulw` mode can use it
|
||||
- Falls back to autonomous exploration if no plan
|
||||
|
||||
**Example:**
|
||||
```
|
||||
[Stay in Sisyphus]
|
||||
"ulw refactor the user service to use the new repository pattern"
|
||||
|
||||
[Agent automatically:]
|
||||
- Explores existing codebase patterns
|
||||
- Implements the refactor
|
||||
- Runs verification (tests, typecheck)
|
||||
- Reports completion
|
||||
```
|
||||
|
||||
### Key Difference in Practice
|
||||
|
||||
| Hephaestus | Sisyphus + ulw |
|
||||
|------------|----------------|
|
||||
| You manually switch to Hephaestus agent | You type `ulw` in any Sisyphus session |
|
||||
| GPT-5.2 Codex with medium reasoning | Your configured default model |
|
||||
| Optimized for autonomous deep work | Optimized for general execution |
|
||||
| Always uses explore-first approach | Respects existing plans if available |
|
||||
| "Smart intern that needs no supervision" | "Smart intern that follows your workflow" |
|
||||
|
||||
### Recommendation
|
||||
|
||||
**For most users**: Use `ulw` keyword in Sisyphus. It's the default path and works excellently for 90% of complex tasks.
|
||||
|
||||
**For power users**: Switch to Hephaestus when you specifically need GPT-5.2 Codex's reasoning style or want the "AmpCode deep mode" experience of fully autonomous exploration and execution.
|
||||
|
||||
---
|
||||
|
||||
## 5. Overall Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -62,11 +271,11 @@ flowchart TD
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Components
|
||||
## 6. Key Components
|
||||
|
||||
### 🔮 Prometheus (The Planner)
|
||||
|
||||
- **Model**: `anthropic/claude-opus-4-5`
|
||||
- **Model**: `anthropic/claude-opus-4-6`
|
||||
- **Role**: Strategic planning, requirements interviews, work plan creation
|
||||
- **Constraint**: **READ-ONLY**. Can only create/modify markdown files within `.sisyphus/` directory.
|
||||
- **Characteristic**: Never writes code directly, focuses solely on "how to do it".
|
||||
@@ -85,13 +294,13 @@ flowchart TD
|
||||
|
||||
### ⚡ Atlas (The Plan Executor)
|
||||
|
||||
- **Model**: `anthropic/claude-opus-4-5` (Extended Thinking 32k)
|
||||
- **Model**: `anthropic/claude-sonnet-4-5` (Extended Thinking 32k)
|
||||
- **Role**: Execution and delegation
|
||||
- **Characteristic**: Doesn't do everything directly, actively delegates to specialized agents (Frontend, Librarian, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 4. Workflow
|
||||
## 7. Workflow
|
||||
|
||||
### Phase 1: Interview and Planning (Interview Mode)
|
||||
|
||||
@@ -113,31 +322,44 @@ When the user requests "Make it a plan", plan generation begins.
|
||||
|
||||
When the user enters `/start-work`, the execution phase begins.
|
||||
|
||||
1. **State Management**: Creates `boulder.json` file to track current plan and session ID.
|
||||
1. **State Management**: Creates/reads `boulder.json` file to track current plan and session ID.
|
||||
2. **Task Execution**: Atlas reads the plan and processes TODOs one by one.
|
||||
3. **Delegation**: UI work is delegated to Frontend agent, complex logic to Oracle.
|
||||
4. **Continuity**: Even if the session is interrupted, work continues in the next session through `boulder.json`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Commands and Usage
|
||||
## 8. Commands and Usage
|
||||
|
||||
### `@plan [request]`
|
||||
|
||||
Invokes Prometheus to start a planning session.
|
||||
Invokes Prometheus to start a planning session from Sisyphus.
|
||||
|
||||
- Example: `@plan "I want to refactor the authentication system to NextAuth"`
|
||||
- Effect: Routes to Prometheus, then returns to Sisyphus when planning completes
|
||||
|
||||
### `/start-work`
|
||||
|
||||
Executes the generated plan.
|
||||
|
||||
- Function: Finds plan in `.sisyphus/plans/` and enters execution mode.
|
||||
- If there's interrupted work, automatically resumes from where it left off.
|
||||
- **Fresh session**: Finds plan in `.sisyphus/plans/` and enters execution mode
|
||||
- **Existing boulder**: Resumes from where you left off (reads boulder.json)
|
||||
- **Effect**: Automatically switches to Atlas agent if not already active
|
||||
|
||||
### Switching Agents Manually
|
||||
|
||||
Press `Tab` at the prompt to see available agents:
|
||||
|
||||
| Agent | When to Switch |
|
||||
|-------|---------------|
|
||||
| **Prometheus** | You want to create a detailed work plan |
|
||||
| **Atlas** | You want to manually control plan execution (rare) |
|
||||
| **Hephaestus** | You need GPT-5.2 Codex for deep autonomous work |
|
||||
| **Sisyphus** | Return to default agent for normal prompting |
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration Guide
|
||||
## 9. Configuration Guide
|
||||
|
||||
You can control related features in `oh-my-opencode.json`.
|
||||
|
||||
@@ -157,8 +379,46 @@ You can control related features in `oh-my-opencode.json`.
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Best Practices
|
||||
---
|
||||
|
||||
## 10. Best Practices
|
||||
|
||||
1. **Don't Rush Planning**: Invest sufficient time in the interview with Prometheus. The more perfect the plan, the faster the execution.
|
||||
|
||||
1. **Don't Rush**: Invest sufficient time in the interview with Prometheus. The more perfect the plan, the faster the execution.
|
||||
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.
|
||||
|
||||
5. **Use `ulw` for Convenience**: When in doubt, type `ulw` and let the system figure out the best approach.
|
||||
|
||||
6. **Reserve Hephaestus for Deep Work**: Don't overthink agent selection. Hephaestus shines for genuinely complex architectural challenges.
|
||||
|
||||
---
|
||||
|
||||
## 11. Troubleshooting Common Confusions
|
||||
|
||||
### "I switched to Prometheus but nothing happened"
|
||||
|
||||
Prometheus enters **interview mode** by default. It will ask you questions about your requirements. Answer them, then say "make it a plan" when ready.
|
||||
|
||||
### "/start-work says 'no active plan found'"
|
||||
|
||||
Either:
|
||||
- No plans exist in `.sisyphus/plans/` → Create one with Prometheus first
|
||||
- Plans exist but boulder.json points elsewhere → Delete `.sisyphus/boulder.json` and retry
|
||||
|
||||
### "I'm in Atlas but I want to switch back to normal mode"
|
||||
|
||||
Type `exit` or start a new session. Atlas is primarily entered via `/start-work` - you don't typically "switch to Atlas" manually.
|
||||
|
||||
### "What's the difference between @plan and just switching to Prometheus?"
|
||||
|
||||
**Nothing functional.** Both invoke Prometheus. @plan is a convenience command while switching agents is explicit control. Use whichever feels natural.
|
||||
|
||||
### "Should I use Hephaestus or type ulw?"
|
||||
|
||||
**For most tasks**: Type `ulw` in Sisyphus.
|
||||
|
||||
**Use Hephaestus when**: You specifically need GPT-5.2 Codex's reasoning style for deep architectural work or complex debugging.
|
||||
|
||||
357
issue-1501-analysis.md
Normal file
357
issue-1501-analysis.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# Issue #1501 분석 보고서: ULW Mode PLAN AGENT 무한루프
|
||||
|
||||
## 📋 이슈 요약
|
||||
|
||||
**증상:**
|
||||
- ULW (ultrawork) mode에서 PLAN AGENT가 무한루프에 빠짐
|
||||
- 분석/탐색 완료 후 plan만 계속 생성
|
||||
- 1분마다 매우 작은 토큰으로 요청 발생
|
||||
|
||||
**예상 동작:**
|
||||
- 탐색 완료 후 solution document 생성
|
||||
|
||||
---
|
||||
|
||||
## 🔍 근본 원인 분석
|
||||
|
||||
### 파일: `src/tools/delegate-task/constants.ts`
|
||||
|
||||
#### 문제의 핵심
|
||||
|
||||
`PLAN_AGENT_SYSTEM_PREPEND` (constants.ts 234-269행)에 구조적 결함이 있었습니다:
|
||||
|
||||
1. **Interactive Mode 가정**
|
||||
```
|
||||
2. After gathering context, ALWAYS present:
|
||||
- Uncertainties: List of unclear points
|
||||
- Clarifying Questions: Specific questions to resolve uncertainties
|
||||
|
||||
3. ITERATE until ALL requirements are crystal clear:
|
||||
- Do NOT proceed to planning until you have 100% clarity
|
||||
- Ask the user to confirm your understanding
|
||||
```
|
||||
|
||||
2. **종료 조건 없음**
|
||||
- "100% clarity" 요구는 객관적 측정 불가능
|
||||
- 사용자 확인 요청은 ULW mode에서 불가능
|
||||
- 무한루프로 이어짐
|
||||
|
||||
3. **ULW Mode 미감지**
|
||||
- Subagent로 실행되는 경우를 구분하지 않음
|
||||
- 항상 interactive mode로 동작 시도
|
||||
|
||||
### 왜 무한루프가 발생했는가?
|
||||
|
||||
```
|
||||
ULW Mode 시작
|
||||
→ Sisyphus가 Plan Agent 호출 (subagent)
|
||||
→ Plan Agent: "100% clarity 필요"
|
||||
→ Clarifying questions 생성
|
||||
→ 사용자 없음 (subagent)
|
||||
→ 다시 plan 생성 시도
|
||||
→ "여전히 unclear"
|
||||
→ 무한루프 반복
|
||||
```
|
||||
|
||||
**핵심:** Plan Agent는 사용자와 대화하도록 설계되었지만, ULW mode에서는 사용자가 없는 subagent로 실행됨.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 적용된 수정 방안
|
||||
|
||||
### 수정 내용 (constants.ts)
|
||||
|
||||
#### 1. SUBAGENT MODE DETECTION 섹션 추가
|
||||
|
||||
```typescript
|
||||
SUBAGENT MODE DETECTION (CRITICAL):
|
||||
If you received a detailed prompt with gathered context from a parent orchestrator (e.g., Sisyphus):
|
||||
- You are running as a SUBAGENT
|
||||
- You CANNOT directly interact with the user
|
||||
- DO NOT ask clarifying questions - proceed with available information
|
||||
- Make reasonable assumptions for minor ambiguities
|
||||
- Generate the plan based on the provided context
|
||||
```
|
||||
|
||||
#### 2. Context Gathering Protocol 수정
|
||||
|
||||
```diff
|
||||
- 1. Launch background agents to gather context:
|
||||
+ 1. Launch background agents to gather context (ONLY if not already provided):
|
||||
```
|
||||
|
||||
**효과:** 이미 Sisyphus가 context를 수집한 경우 중복 방지
|
||||
|
||||
#### 3. Clarifying Questions → Assumptions
|
||||
|
||||
```diff
|
||||
- 2. After gathering context, ALWAYS present:
|
||||
- - Uncertainties: List of unclear points
|
||||
- - Clarifying Questions: Specific questions
|
||||
+ 2. After gathering context, assess clarity:
|
||||
+ - User Request Summary: Concise restatement
|
||||
+ - Assumptions Made: List any assumptions for unclear points
|
||||
```
|
||||
|
||||
**효과:** 질문 대신 가정 사항 문서화
|
||||
|
||||
#### 4. 무한루프 방지 - 명확한 종료 조건
|
||||
|
||||
```diff
|
||||
- 3. ITERATE until ALL requirements are crystal clear:
|
||||
- - Do NOT proceed to planning until you have 100% clarity
|
||||
- - Ask the user to confirm your understanding
|
||||
- - Resolve every ambiguity before generating the work plan
|
||||
+ 3. PROCEED TO PLAN GENERATION when:
|
||||
+ - Core objective is understood (even if some details are ambiguous)
|
||||
+ - You have gathered context via explore/librarian (or context was provided)
|
||||
+ - You can make reasonable assumptions for remaining ambiguities
|
||||
+
|
||||
+ DO NOT loop indefinitely waiting for perfect clarity.
|
||||
+ DOCUMENT assumptions in the plan so they can be validated during execution.
|
||||
```
|
||||
|
||||
**효과:**
|
||||
- "100% clarity" 요구 제거
|
||||
- 객관적인 진입 조건 제공
|
||||
- 무한루프 명시적 금지
|
||||
- Assumptions를 plan에 문서화하여 실행 중 검증 가능
|
||||
|
||||
#### 5. 철학 변경
|
||||
|
||||
```diff
|
||||
- REMEMBER: Vague requirements lead to failed implementations.
|
||||
+ REMEMBER: A plan with documented assumptions is better than no plan.
|
||||
```
|
||||
|
||||
**효과:** Perfectionism → Pragmatism
|
||||
|
||||
---
|
||||
|
||||
## 🎯 해결 메커니즘
|
||||
|
||||
### Before (무한루프)
|
||||
|
||||
```
|
||||
Plan Agent 시작
|
||||
↓
|
||||
Context gathering
|
||||
↓
|
||||
Requirements 명확한가?
|
||||
↓ NO
|
||||
Clarifying questions 생성
|
||||
↓
|
||||
사용자 응답 대기 (없음)
|
||||
↓
|
||||
다시 plan 시도
|
||||
↓
|
||||
(무한 반복)
|
||||
```
|
||||
|
||||
### After (정상 종료)
|
||||
|
||||
```
|
||||
Plan Agent 시작
|
||||
↓
|
||||
Subagent mode 감지?
|
||||
↓ YES
|
||||
Context 이미 있음? → YES
|
||||
↓
|
||||
Core objective 이해? → YES
|
||||
↓
|
||||
Reasonable assumptions 가능? → YES
|
||||
↓
|
||||
Plan 생성 (assumptions 문서화)
|
||||
↓
|
||||
완료 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 영향 분석
|
||||
|
||||
### 해결되는 문제
|
||||
|
||||
1. **ULW mode 무한루프** ✓
|
||||
2. **Sisyphus에서 Plan Agent 호출 시 블로킹** ✓
|
||||
3. **작은 토큰 반복 요청** ✓
|
||||
4. **1분마다 재시도** ✓
|
||||
|
||||
### 부작용 없음
|
||||
|
||||
- Interactive mode (사용자와 직접 대화)는 여전히 작동
|
||||
- Subagent mode일 때만 다르게 동작
|
||||
- Backward compatibility 유지
|
||||
|
||||
### 추가 개선사항
|
||||
|
||||
- Assumptions를 plan에 명시적으로 문서화
|
||||
- Execution 중 validation 가능
|
||||
- 더 pragmatic한 workflow
|
||||
|
||||
---
|
||||
|
||||
## 🧪 검증 방법
|
||||
|
||||
### 테스트 시나리오
|
||||
|
||||
1. **ULW mode에서 Plan Agent 호출**
|
||||
```bash
|
||||
oh-my-opencode run "Complex task requiring planning. ulw"
|
||||
```
|
||||
- 예상: Plan 생성 후 정상 종료
|
||||
- 확인: 무한루프 없음
|
||||
|
||||
2. **Interactive mode (변경 없어야 함)**
|
||||
```bash
|
||||
oh-my-opencode run --agent prometheus "Design X"
|
||||
```
|
||||
- 예상: Clarifying questions 여전히 가능
|
||||
- 확인: 사용자와 대화 가능
|
||||
|
||||
3. **Subagent context 제공 케이스**
|
||||
- 예상: Context gathering skip
|
||||
- 확인: 중복 탐색 없음
|
||||
|
||||
---
|
||||
|
||||
## 📝 수정된 파일
|
||||
|
||||
```
|
||||
src/tools/delegate-task/constants.ts
|
||||
```
|
||||
|
||||
### Diff Summary
|
||||
|
||||
```diff
|
||||
@@ -234,22 +234,32 @@ export const PLAN_AGENT_SYSTEM_PREPEND = `<system>
|
||||
+SUBAGENT MODE DETECTION (CRITICAL):
|
||||
+[subagent 감지 및 처리 로직]
|
||||
+
|
||||
MANDATORY CONTEXT GATHERING PROTOCOL:
|
||||
-1. Launch background agents to gather context:
|
||||
+1. Launch background agents (ONLY if not already provided):
|
||||
|
||||
-2. After gathering context, ALWAYS present:
|
||||
- - Uncertainties
|
||||
- - Clarifying Questions
|
||||
+2. After gathering context, assess clarity:
|
||||
+ - Assumptions Made
|
||||
|
||||
-3. ITERATE until ALL requirements are crystal clear:
|
||||
- - Do NOT proceed until 100% clarity
|
||||
- - Ask user to confirm
|
||||
+3. PROCEED TO PLAN GENERATION when:
|
||||
+ - Core objective understood
|
||||
+ - Context gathered
|
||||
+ - Reasonable assumptions possible
|
||||
+
|
||||
+ DO NOT loop indefinitely.
|
||||
+ DOCUMENT assumptions.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 권장 사항
|
||||
|
||||
### Immediate Actions
|
||||
|
||||
1. ✅ **수정 적용 완료** - constants.ts 업데이트됨
|
||||
2. ⏳ **테스트 수행** - ULW mode에서 동작 검증
|
||||
3. ⏳ **PR 생성** - code review 요청
|
||||
|
||||
### Future Improvements
|
||||
|
||||
1. **Subagent context 표준화**
|
||||
- Subagent로 호출 시 명시적 플래그 전달
|
||||
- `is_subagent: true` 파라미터 추가 고려
|
||||
|
||||
2. **Assumptions validation workflow**
|
||||
- Plan 실행 중 assumptions 검증 메커니즘
|
||||
- Incorrect assumptions 감지 시 재계획
|
||||
|
||||
3. **Timeout 메커니즘**
|
||||
- Plan Agent가 X분 이상 걸리면 강제 종료
|
||||
- Fallback plan 생성
|
||||
|
||||
4. **Monitoring 추가**
|
||||
- Plan Agent 실행 시간 측정
|
||||
- Iteration 횟수 로깅
|
||||
- 무한루프 조기 감지
|
||||
|
||||
---
|
||||
|
||||
## 📖 관련 코드 구조
|
||||
|
||||
### Call Stack
|
||||
|
||||
```
|
||||
Sisyphus (ULW mode)
|
||||
↓
|
||||
task(category="deep", ...)
|
||||
↓
|
||||
executor.ts: executeBackgroundContinuation()
|
||||
↓
|
||||
prompt-builder.ts: buildSystemContent()
|
||||
↓
|
||||
constants.ts: PLAN_AGENT_SYSTEM_PREPEND (문제 위치)
|
||||
↓
|
||||
Plan Agent 실행
|
||||
```
|
||||
|
||||
### Key Functions
|
||||
|
||||
1. **executor.ts:587** - `isPlanAgent()` 체크
|
||||
2. **prompt-builder.ts:11** - Plan Agent prepend 주입
|
||||
3. **constants.ts:234** - PLAN_AGENT_SYSTEM_PREPEND 정의
|
||||
|
||||
---
|
||||
|
||||
## 🎓 교훈
|
||||
|
||||
### Design Lessons
|
||||
|
||||
1. **Dual Mode Support**
|
||||
- Interactive vs Autonomous mode 구분 필수
|
||||
- Context 전달 방식 명확히
|
||||
|
||||
2. **Avoid Perfectionism in Agents**
|
||||
- "100% clarity" 같은 주관적 조건 지양
|
||||
- 명확한 객관적 종료 조건 필요
|
||||
|
||||
3. **Document Uncertainties**
|
||||
- 불확실성을 숨기지 말고 문서화
|
||||
- 실행 중 validation 가능하게
|
||||
|
||||
4. **Infinite Loop Prevention**
|
||||
- 모든 반복문에 명시적 종료 조건
|
||||
- Timeout 또는 max iteration 설정
|
||||
|
||||
---
|
||||
|
||||
## 🔗 참고 자료
|
||||
|
||||
- **Issue:** #1501 - [Bug]: ULW mode will 100% cause PLAN AGENT to get stuck
|
||||
- **Files Modified:** `src/tools/delegate-task/constants.ts`
|
||||
- **Related Concepts:** Ultrawork mode, Plan Agent, Subagent delegation
|
||||
- **Agent Architecture:** Sisyphus → Prometheus → Atlas workflow
|
||||
|
||||
---
|
||||
|
||||
## ✅ Conclusion
|
||||
|
||||
**Root Cause:** Plan Agent가 interactive mode를 가정했으나 ULW mode에서는 subagent로 실행되어 사용자 상호작용 불가능. "100% clarity" 요구로 무한루프 발생.
|
||||
|
||||
**Solution:** Subagent mode 감지 로직 추가, clarifying questions 제거, 명확한 종료 조건 제공, assumptions 문서화 방식 도입.
|
||||
|
||||
**Result:** ULW mode에서 Plan Agent가 정상적으로 plan 생성 후 종료. 무한루프 해결.
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Fixed
|
||||
**Tested:** ⏳ Pending
|
||||
**Deployed:** ⏳ Pending
|
||||
|
||||
**Analyst:** Sisyphus (oh-my-opencode ultrawork mode)
|
||||
**Date:** 2026-02-05
|
||||
**Session:** fast-ember
|
||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "3.2.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"oh-my-opencode-darwin-x64": "3.2.2",
|
||||
"oh-my-opencode-linux-arm64": "3.2.2",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.2.2",
|
||||
"oh-my-opencode-linux-x64": "3.2.2",
|
||||
"oh-my-opencode-linux-x64-musl": "3.2.2",
|
||||
"oh-my-opencode-windows-x64": "3.2.2"
|
||||
"oh-my-opencode-darwin-arm64": "3.5.1",
|
||||
"oh-my-opencode-darwin-x64": "3.5.1",
|
||||
"oh-my-opencode-linux-arm64": "3.5.1",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.5.1",
|
||||
"oh-my-opencode-linux-x64": "3.5.1",
|
||||
"oh-my-opencode-linux-x64-musl": "3.5.1",
|
||||
"oh-my-opencode-windows-x64": "3.5.1"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.2.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"version": "3.5.1",
|
||||
"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.2",
|
||||
"version": "3.5.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
import * as z from "zod"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
||||
|
||||
const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json"
|
||||
@@ -7,9 +8,8 @@ const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json"
|
||||
async function main() {
|
||||
console.log("Generating JSON Schema...")
|
||||
|
||||
const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, {
|
||||
io: "input",
|
||||
target: "draft-7",
|
||||
const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, {
|
||||
target: "draft7",
|
||||
})
|
||||
|
||||
const finalSchema = {
|
||||
|
||||
@@ -1127,6 +1127,222 @@
|
||||
"created_at": "2026-02-02T16:58:50Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1399
|
||||
},
|
||||
{
|
||||
"name": "ilarvne",
|
||||
"id": 99905590,
|
||||
"comment_id": 3839771590,
|
||||
"created_at": "2026-02-03T08:15:37Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1422
|
||||
},
|
||||
{
|
||||
"name": "ualtinok",
|
||||
"id": 94532,
|
||||
"comment_id": 3841078284,
|
||||
"created_at": "2026-02-03T12:39:59Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1393
|
||||
},
|
||||
{
|
||||
"name": "Stranmor",
|
||||
"id": 49376798,
|
||||
"comment_id": 3841465375,
|
||||
"created_at": "2026-02-03T13:53:13Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1432
|
||||
},
|
||||
{
|
||||
"name": "sk0x0y",
|
||||
"id": 35445665,
|
||||
"comment_id": 3841625993,
|
||||
"created_at": "2026-02-03T14:21:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1434
|
||||
},
|
||||
{
|
||||
"name": "filipemsilv4",
|
||||
"id": 59426206,
|
||||
"comment_id": 3841722121,
|
||||
"created_at": "2026-02-03T14:38:07Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1435
|
||||
},
|
||||
{
|
||||
"name": "wydrox",
|
||||
"id": 79707825,
|
||||
"comment_id": 3842392636,
|
||||
"created_at": "2026-02-03T16:39:35Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1436
|
||||
},
|
||||
{
|
||||
"name": "kaizen403",
|
||||
"id": 134706404,
|
||||
"comment_id": 3843559932,
|
||||
"created_at": "2026-02-03T20:44:25Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1449
|
||||
},
|
||||
{
|
||||
"name": "BowTiedSwan",
|
||||
"id": 86532747,
|
||||
"comment_id": 3742668781,
|
||||
"created_at": "2026-01-13T08:05:00Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 741
|
||||
},
|
||||
{
|
||||
"name": "Mang-Joo",
|
||||
"id": 86056915,
|
||||
"comment_id": 3855493558,
|
||||
"created_at": "2026-02-05T18:41:49Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1526
|
||||
},
|
||||
{
|
||||
"name": "shaunmorris",
|
||||
"id": 579820,
|
||||
"comment_id": 3858265174,
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Model | `anthropic/claude-opus-4-5` |
|
||||
| Model | `anthropic/claude-opus-4-6` |
|
||||
| Max Tokens | `64000` |
|
||||
| Mode | `primary` |
|
||||
| Thinking | Budget: 32000 |
|
||||
@@ -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,72 +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.ts # Master Orchestrator (holds todo list)
|
||||
├── sisyphus.ts # Main prompt (SF Bay Area engineer identity)
|
||||
├── hephaestus.ts # Autonomous Deep Worker (GPT 5.2 Codex, "The Legitimate Craftsman")
|
||||
├── sisyphus-junior.ts # Delegated task executor (category-spawned)
|
||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||
├── librarian.ts # Multi-repo research (GitHub CLI, Context7)
|
||||
├── explore.ts # Fast contextual grep (Grok Code Fast)
|
||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||
├── prometheus-prompt.ts # Planning (Interview/Consultant mode, 1283 lines)
|
||||
├── metis.ts # Pre-planning analysis (Gap detection)
|
||||
├── momus.ts # Plan reviewer (Ruthless fault-finding)
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
|
||||
├── 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
|
||||
│ ├── 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
|
||||
│ ├── 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()
|
||||
└── index.ts # builtinAgents export
|
||||
└── index.ts # Exports
|
||||
```
|
||||
|
||||
## AGENT MODELS
|
||||
| Agent | Model | Temp | Purpose |
|
||||
|-------|-------|------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.2-codex → gemini-3-pro) |
|
||||
| Hephaestus | openai/gpt-5.2-codex | 0.1 | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.2-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-5 | 0.1 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Metis | anthropic/claude-opus-4-5 | 0.3 | Pre-planning analysis (fallback: kimi-k2.5 → gpt-5.2) |
|
||||
| Momus | openai/gpt-5.2 | 0.1 | Plan validation (fallback: claude-opus-4-5) |
|
||||
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
|
||||
|
||||
## HOW TO ADD
|
||||
1. Create `src/agents/my-agent.ts` exporting factory + metadata.
|
||||
2. Add to `agentSources` in `src/agents/utils.ts`.
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`.
|
||||
4. Register in `src/index.ts` initialization.
|
||||
| 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 |
|
||||
|
||||
## PATTERNS
|
||||
- **Factory**: `createXXXAgent(model: string): AgentConfig`
|
||||
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers.
|
||||
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`.
|
||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas.
|
||||
| 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
|
||||
- **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
|
||||
|
||||
52
src/agents/agent-builder.ts
Normal file
52
src/agents/agent-builder.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
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 { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"
|
||||
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> = 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
|
||||
}
|
||||
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 { DEFAULT_CATEGORIES } from "../../tools/delegate-task/constants"
|
||||
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 = { ...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 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"
|
||||
|
||||
@@ -6,23 +6,24 @@
|
||||
*/
|
||||
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import type { AvailableAgent, AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { formatCustomSkillsBlock, type AvailableAgent, type AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
||||
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)
|
||||
|
||||
@@ -47,7 +48,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="...")
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -56,21 +57,48 @@ export function buildSkillsSection(skills: AvailableSkill[]): string {
|
||||
return ""
|
||||
}
|
||||
|
||||
const skillRows = skills.map((s) => {
|
||||
const shortDesc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const shortDesc = truncateDescription(s.description)
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
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, "**")
|
||||
|
||||
let skillsTable: string
|
||||
|
||||
if (customSkills.length > 0 && builtinSkills.length > 0) {
|
||||
skillsTable = `**Built-in Skills:**
|
||||
|
||||
| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
${builtinRows.join("\n")}
|
||||
|
||||
${customSkillBlock}`
|
||||
} else if (customSkills.length > 0) {
|
||||
skillsTable = customSkillBlock
|
||||
} else {
|
||||
skillsTable = `| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
${builtinRows.join("\n")}`
|
||||
}
|
||||
|
||||
return `
|
||||
#### 3.2.2: Skill Selection (PREPEND TO PROMPT)
|
||||
|
||||
**Skills are specialized instructions that guide subagent behavior. Consider them alongside category selection.**
|
||||
|
||||
| Skill | When to Use |
|
||||
|-------|-------------|
|
||||
${skillRows.join("\n")}
|
||||
${skillsTable}
|
||||
|
||||
**MANDATORY: Evaluate ALL skills for relevance to your task.**
|
||||
**MANDATORY: Evaluate ALL skills (built-in AND user-installed) for relevance to your task.**
|
||||
|
||||
Read each skill's description and ask: "Does this skill's domain overlap with my task?"
|
||||
- If YES: INCLUDE in load_skills=[...]
|
||||
@@ -78,7 +106,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:**
|
||||
@@ -94,10 +122,10 @@ export function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: R
|
||||
`| ${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 { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||
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 = 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 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,
|
||||
userCategories: categories,
|
||||
})
|
||||
if (atlasConfig) {
|
||||
result["atlas"] = atlasConfig
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
65
src/agents/builtin-agents/agent-overrides.ts
Normal file
65
src/agents/builtin-agents/agent-overrides.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentOverrideConfig } from "../types"
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { deepMerge, migrateAgentConfig } from "../../shared"
|
||||
|
||||
/**
|
||||
* 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" + categoryConfig.prompt_append
|
||||
}
|
||||
|
||||
return result as AgentConfig
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
64
src/agents/builtin-agents/atlas-agent.ts
Normal file
64
src/agents/builtin-agents/atlas-agent.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
userCategories?: CategoriesConfig
|
||||
useTaskSystem?: boolean
|
||||
}): AgentConfig | undefined {
|
||||
const {
|
||||
disabledAgents,
|
||||
agentOverrides,
|
||||
uiSelectedModel,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
mergedCategories,
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
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)
|
||||
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",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
205
src/agents/dynamic-agent-prompt-builder.test.ts
Normal file
205
src/agents/dynamic-agent-prompt-builder.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import {
|
||||
buildCategorySkillsDelegationGuide,
|
||||
buildUltraworkSection,
|
||||
formatCustomSkillsBlock,
|
||||
type AvailableSkill,
|
||||
type AvailableCategory,
|
||||
type AvailableAgent,
|
||||
} from "./dynamic-agent-prompt-builder"
|
||||
|
||||
describe("buildCategorySkillsDelegationGuide", () => {
|
||||
const categories: AvailableCategory[] = [
|
||||
{ name: "visual-engineering", description: "Frontend, UI/UX" },
|
||||
{ name: "quick", description: "Trivial tasks" },
|
||||
]
|
||||
|
||||
const builtinSkills: AvailableSkill[] = [
|
||||
{ name: "playwright", description: "Browser automation via Playwright", location: "plugin" },
|
||||
{ name: "frontend-ui-ux", description: "Designer-turned-developer", location: "plugin" },
|
||||
]
|
||||
|
||||
const customUserSkills: AvailableSkill[] = [
|
||||
{ name: "react-19", description: "React 19 patterns and best practices", location: "user" },
|
||||
{ name: "tailwind-4", description: "Tailwind CSS v4 utilities", location: "user" },
|
||||
]
|
||||
|
||||
const customProjectSkills: AvailableSkill[] = [
|
||||
{ name: "our-design-system", description: "Internal design system components", location: "project" },
|
||||
]
|
||||
|
||||
it("should separate builtin and custom skills into distinct sections", () => {
|
||||
//#given: mix of builtin and custom skills
|
||||
const allSkills = [...builtinSkills, ...customUserSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: should have separate sections
|
||||
expect(result).toContain("Built-in Skills")
|
||||
expect(result).toContain("User-Installed Skills")
|
||||
expect(result).toContain("HIGH PRIORITY")
|
||||
})
|
||||
|
||||
it("should include custom skill names in CRITICAL warning", () => {
|
||||
//#given: custom skills installed
|
||||
const allSkills = [...builtinSkills, ...customUserSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: should mention custom skills by name in the warning
|
||||
expect(result).toContain('"react-19"')
|
||||
expect(result).toContain('"tailwind-4"')
|
||||
expect(result).toContain("CRITICAL")
|
||||
})
|
||||
|
||||
it("should show source column for custom skills (user vs project)", () => {
|
||||
//#given: both user and project custom skills
|
||||
const allSkills = [...builtinSkills, ...customUserSkills, ...customProjectSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: should show source for each custom skill
|
||||
expect(result).toContain("| user |")
|
||||
expect(result).toContain("| project |")
|
||||
})
|
||||
|
||||
it("should not show custom skill section when only builtin skills exist", () => {
|
||||
//#given: only builtin skills
|
||||
const allSkills = [...builtinSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: should not contain custom skill emphasis
|
||||
expect(result).not.toContain("User-Installed Skills")
|
||||
expect(result).not.toContain("HIGH PRIORITY")
|
||||
expect(result).toContain("Available Skills")
|
||||
})
|
||||
|
||||
it("should handle only custom skills (no builtins)", () => {
|
||||
//#given: only custom skills, no builtins
|
||||
const allSkills = [...customUserSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: should show custom skills with emphasis, no builtin section
|
||||
expect(result).toContain("User-Installed Skills")
|
||||
expect(result).toContain("HIGH PRIORITY")
|
||||
expect(result).not.toContain("Built-in Skills")
|
||||
})
|
||||
|
||||
it("should include priority note for custom skills in evaluation step", () => {
|
||||
//#given: custom skills present
|
||||
const allSkills = [...builtinSkills, ...customUserSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: evaluation section should mention user-installed priority
|
||||
expect(result).toContain("User-installed skills get PRIORITY")
|
||||
expect(result).toContain("INCLUDE it rather than omit it")
|
||||
})
|
||||
|
||||
it("should NOT include priority note when no custom skills", () => {
|
||||
//#given: only builtin skills
|
||||
const allSkills = [...builtinSkills]
|
||||
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide(categories, allSkills)
|
||||
|
||||
//#then: no priority note for custom skills
|
||||
expect(result).not.toContain("User-installed skills get PRIORITY")
|
||||
})
|
||||
|
||||
it("should return empty string when no categories and no skills", () => {
|
||||
//#given: no categories and no skills
|
||||
//#when: building the delegation guide
|
||||
const result = buildCategorySkillsDelegationGuide([], [])
|
||||
|
||||
//#then: should return empty string
|
||||
expect(result).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildUltraworkSection", () => {
|
||||
const agents: AvailableAgent[] = []
|
||||
|
||||
it("should separate builtin and custom skills", () => {
|
||||
//#given: mix of builtin and custom skills
|
||||
const skills: AvailableSkill[] = [
|
||||
{ name: "playwright", description: "Browser automation", location: "plugin" },
|
||||
{ name: "react-19", description: "React 19 patterns", location: "user" },
|
||||
]
|
||||
|
||||
//#when: building ultrawork section
|
||||
const result = buildUltraworkSection(agents, [], skills)
|
||||
|
||||
//#then: should have separate sections
|
||||
expect(result).toContain("Built-in Skills")
|
||||
expect(result).toContain("User-Installed Skills")
|
||||
expect(result).toContain("HIGH PRIORITY")
|
||||
})
|
||||
|
||||
it("should not separate when only builtin skills", () => {
|
||||
//#given: only builtin skills
|
||||
const skills: AvailableSkill[] = [
|
||||
{ name: "playwright", description: "Browser automation", location: "plugin" },
|
||||
]
|
||||
|
||||
//#when: building ultrawork section
|
||||
const result = buildUltraworkSection(agents, [], skills)
|
||||
|
||||
//#then: should have single section
|
||||
expect(result).toContain("Built-in Skills")
|
||||
expect(result).not.toContain("User-Installed Skills")
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatCustomSkillsBlock", () => {
|
||||
const customSkills: AvailableSkill[] = [
|
||||
{ name: "react-19", description: "React 19 patterns", location: "user" },
|
||||
{ name: "tailwind-4", description: "Tailwind v4", location: "project" },
|
||||
]
|
||||
|
||||
const customRows = customSkills.map((s) => {
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${s.description} | ${source} |`
|
||||
})
|
||||
|
||||
it("should produce consistent output used by both builders", () => {
|
||||
//#given: custom skills and rows
|
||||
//#when: formatting with default header level
|
||||
const result = formatCustomSkillsBlock(customRows, customSkills)
|
||||
|
||||
//#then: contains all expected elements
|
||||
expect(result).toContain("User-Installed Skills (HIGH PRIORITY)")
|
||||
expect(result).toContain("CRITICAL")
|
||||
expect(result).toContain('"react-19"')
|
||||
expect(result).toContain('"tailwind-4"')
|
||||
expect(result).toContain("| user |")
|
||||
expect(result).toContain("| project |")
|
||||
})
|
||||
|
||||
it("should use #### header by default", () => {
|
||||
//#given: default header level
|
||||
const result = formatCustomSkillsBlock(customRows, customSkills)
|
||||
|
||||
//#then: uses markdown h4
|
||||
expect(result).toContain("#### User-Installed Skills")
|
||||
})
|
||||
|
||||
it("should use bold header when specified", () => {
|
||||
//#given: bold header level (used by Atlas)
|
||||
const result = formatCustomSkillsBlock(customRows, customSkills, "**")
|
||||
|
||||
//#then: uses bold instead of h4
|
||||
expect(result).toContain("**User-Installed Skills (HIGH PRIORITY):**")
|
||||
expect(result).not.toContain("#### User-Installed Skills")
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -20,6 +21,7 @@ export interface AvailableSkill {
|
||||
export interface AvailableCategory {
|
||||
name: string
|
||||
description: string
|
||||
model?: string
|
||||
}
|
||||
|
||||
export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
||||
@@ -166,6 +168,33 @@ export function buildDelegationTable(agents: AvailableAgent[]): string {
|
||||
return rows.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "User-Installed Skills (HIGH PRIORITY)" block used across multiple agent prompts.
|
||||
* Extracted to avoid duplication between buildCategorySkillsDelegationGuide, buildSkillsSection, etc.
|
||||
*/
|
||||
export function formatCustomSkillsBlock(
|
||||
customRows: string[],
|
||||
customSkills: AvailableSkill[],
|
||||
headerLevel: "####" | "**" = "####"
|
||||
): string {
|
||||
const customSkillNames = customSkills.map((s) => `"${s.name}"`).join(", ")
|
||||
const header = headerLevel === "####"
|
||||
? `#### User-Installed Skills (HIGH PRIORITY)`
|
||||
: `**User-Installed Skills (HIGH PRIORITY):**`
|
||||
|
||||
return `${header}
|
||||
|
||||
**The user has installed these custom skills. They MUST be evaluated for EVERY delegation.**
|
||||
Subagents are STATELESS — they lose all custom knowledge unless you pass these skills via \`load_skills\`.
|
||||
|
||||
| Skill | Expertise Domain | Source |
|
||||
|-------|------------------|--------|
|
||||
${customRows.join("\n")}
|
||||
|
||||
> **CRITICAL**: Ignoring user-installed skills when they match the task domain is a failure.
|
||||
> The user installed ${customSkillNames} for a reason — USE THEM when the task overlaps with their domain.`
|
||||
}
|
||||
|
||||
export function buildCategorySkillsDelegationGuide(categories: AvailableCategory[], skills: AvailableSkill[]): string {
|
||||
if (categories.length === 0 && skills.length === 0) return ""
|
||||
|
||||
@@ -174,14 +203,47 @@ export function buildCategorySkillsDelegationGuide(categories: AvailableCategory
|
||||
return `| \`${c.name}\` | ${desc} |`
|
||||
})
|
||||
|
||||
const skillRows = skills.map((s) => {
|
||||
const desc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const desc = truncateDescription(s.description)
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
let skillsSection: string
|
||||
|
||||
if (customSkills.length > 0 && builtinSkills.length > 0) {
|
||||
skillsSection = `#### Built-in Skills
|
||||
|
||||
| Skill | Expertise Domain |
|
||||
|-------|------------------|
|
||||
${builtinRows.join("\n")}
|
||||
|
||||
${customSkillBlock}`
|
||||
} else if (customSkills.length > 0) {
|
||||
skillsSection = customSkillBlock
|
||||
} else {
|
||||
skillsSection = `#### Available Skills (Domain Expertise Injection)
|
||||
|
||||
Skills inject specialized instructions into the subagent. Read the description to understand when each skill applies.
|
||||
|
||||
| Skill | Expertise Domain |
|
||||
|-------|------------------|
|
||||
${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)
|
||||
|
||||
@@ -191,13 +253,7 @@ Each category is configured with a model optimized for that domain. Read the des
|
||||
|----------|-------------------|
|
||||
${categoryRows.join("\n")}
|
||||
|
||||
#### Available Skills (Domain Expertise Injection)
|
||||
|
||||
Skills inject specialized instructions into the subagent. Read the description to understand when each skill applies.
|
||||
|
||||
| Skill | Expertise Domain |
|
||||
|-------|------------------|
|
||||
${skillRows.join("\n")}
|
||||
${skillsSection}
|
||||
|
||||
---
|
||||
|
||||
@@ -208,12 +264,15 @@ ${skillRows.join("\n")}
|
||||
- Match task requirements to category domain
|
||||
- Select the category whose domain BEST fits the task
|
||||
|
||||
**STEP 2: Evaluate ALL Skills**
|
||||
**STEP 2: Evaluate ALL Skills (Built-in AND User-Installed)**
|
||||
For EVERY skill listed above, ask yourself:
|
||||
> "Does this skill's expertise domain overlap with my task?"
|
||||
|
||||
- If YES → INCLUDE in \`load_skills=[...]\`
|
||||
- If NO → You MUST justify why (see below)
|
||||
${customSkills.length > 0 ? `
|
||||
> **User-installed skills get PRIORITY.** The user explicitly installed them for their workflow.
|
||||
> When in doubt about a user-installed skill, INCLUDE it rather than omit it.` : ""}
|
||||
|
||||
**STEP 3: Justify Omissions**
|
||||
|
||||
@@ -238,16 +297,16 @@ SKILL EVALUATION for "[skill-name]":
|
||||
### Delegation Pattern
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
task(
|
||||
category="[selected-category]",
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills — ESPECIALLY user-installed ones
|
||||
prompt="..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**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
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -328,12 +387,26 @@ export function buildUltraworkSection(
|
||||
}
|
||||
|
||||
if (skills.length > 0) {
|
||||
lines.push("**Skills** (combine with categories - EVALUATE ALL for relevance):")
|
||||
for (const skill of skills) {
|
||||
const shortDesc = skill.description.split(".")[0] || skill.description
|
||||
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
if (builtinSkills.length > 0) {
|
||||
lines.push("**Built-in Skills** (combine with categories):")
|
||||
for (const skill of builtinSkills) {
|
||||
const shortDesc = skill.description.split(".")[0] || skill.description
|
||||
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (customSkills.length > 0) {
|
||||
lines.push("**User-Installed Skills** (HIGH PRIORITY - user installed these for their workflow):")
|
||||
for (const skill of customSkills) {
|
||||
const shortDesc = skill.description.split(".")[0] || skill.description
|
||||
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (agents.length > 0) {
|
||||
@@ -349,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",
|
||||
])
|
||||
|
||||
|
||||
@@ -142,6 +142,19 @@ You operate as a **Senior Staff Engineer** with deep expertise in:
|
||||
|
||||
You do not guess. You verify. You do not stop early. You complete.
|
||||
|
||||
## Core Principle (HIGHEST PRIORITY)
|
||||
|
||||
**KEEP GOING. SOLVE PROBLEMS. ASK ONLY WHEN TRULY IMPOSSIBLE.**
|
||||
|
||||
When blocked:
|
||||
1. Try a different approach (there's always another way)
|
||||
2. Decompose the problem into smaller pieces
|
||||
3. Challenge your assumptions
|
||||
4. Explore how others solved similar problems
|
||||
|
||||
Asking the user is the LAST resort after exhausting creative alternatives.
|
||||
Your job is to SOLVE problems, not report them.
|
||||
|
||||
## Hard Constraints (MUST READ FIRST - GPT 5.2 Constraint-First)
|
||||
|
||||
${hardBlocks}
|
||||
@@ -214,8 +227,8 @@ Agent: *runs gh pr list, gh pr view, searches recent commits*
|
||||
|
||||
**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 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.**
|
||||
@@ -265,17 +278,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:**
|
||||
@@ -380,7 +399,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 |
|
||||
@@ -404,6 +423,13 @@ Only terminate your turn when you are SURE the problem is SOLVED.
|
||||
Autonomously resolve the query to the BEST of your ability.
|
||||
Do NOT guess. Do NOT ask unnecessary questions. Do NOT stop early.
|
||||
|
||||
**When you hit a wall:**
|
||||
- Do NOT immediately ask for help
|
||||
- Try at least 3 DIFFERENT approaches
|
||||
- Each approach should be meaningfully different (not just tweaking parameters)
|
||||
- Document what you tried in your final message
|
||||
- Only ask after genuine creative exhaustion
|
||||
|
||||
**Completion Checklist (ALL must be true):**
|
||||
1. User asked for X → X is FULLY implemented (not partial, not "basic version")
|
||||
2. X passes lsp_diagnostics (zero errors on ALL modified files)
|
||||
@@ -459,9 +485,9 @@ Do NOT guess. Do NOT ask unnecessary questions. Do NOT stop early.
|
||||
- Each update must include concrete outcome ("Found X", "Updated Y")
|
||||
|
||||
**Scope:**
|
||||
- Implement EXACTLY what user requests
|
||||
- No extra features, no embellishments
|
||||
- Simplest valid interpretation for ambiguous instructions
|
||||
- Implement what user requests
|
||||
- When blocked, autonomously try alternative approaches before asking
|
||||
- No unnecessary features, but solve blockers creatively
|
||||
</output_contract>
|
||||
|
||||
## Response Compaction (LONG CONTEXT HANDLING)
|
||||
@@ -545,21 +571,27 @@ When working on long sessions or complex multi-file tasks:
|
||||
2. Re-verify after EVERY fix attempt
|
||||
3. Never shotgun debug
|
||||
|
||||
### After 3 Consecutive Failures
|
||||
### After Failure (AUTONOMOUS RECOVERY)
|
||||
|
||||
1. **Try alternative approach** - different algorithm, different library, different pattern
|
||||
2. **Decompose** - break into smaller, independently solvable steps
|
||||
3. **Challenge assumptions** - what if your initial interpretation was wrong?
|
||||
4. **Explore more** - fire explore/librarian agents for similar problems solved elsewhere
|
||||
|
||||
### After 3 DIFFERENT Approaches Fail
|
||||
|
||||
1. **STOP** all edits
|
||||
2. **REVERT** to last working state
|
||||
3. **DOCUMENT** what failed
|
||||
3. **DOCUMENT** what you tried (all 3 approaches)
|
||||
4. **CONSULT** Oracle with full context
|
||||
5. If unresolved, **ASK USER**
|
||||
5. If Oracle cannot help, **ASK USER** with clear explanation of attempts
|
||||
|
||||
**Never**: Leave code broken, delete failing tests, continue hoping
|
||||
|
||||
## Soft Guidelines
|
||||
|
||||
- Prefer existing libraries over new dependencies
|
||||
- Prefer small, focused changes over large refactors
|
||||
- When uncertain about scope, ask`
|
||||
- Prefer small, focused changes over large refactors`
|
||||
}
|
||||
|
||||
export function createHephaestusAgent(
|
||||
@@ -584,7 +616,7 @@ export function createHephaestusAgent(
|
||||
model,
|
||||
maxTokens: 32000,
|
||||
prompt,
|
||||
color: "#FF4500", // Magma Orange - forge heat, distinct from Prometheus purple
|
||||
color: "#D97706", // Forged Amber - Golden heated metal, divine craftsman
|
||||
permission: { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"],
|
||||
reasoningEffort: "medium",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -12,6 +12,7 @@ export function buildDefaultSisyphusJuniorPrompt(
|
||||
promptAppend?: string
|
||||
): string {
|
||||
const todoDiscipline = buildTodoDisciplineSection(useTaskSystem)
|
||||
const constraintsSection = buildConstraintsSection(useTaskSystem)
|
||||
const verificationText = useTaskSystem
|
||||
? "All tasks marked completed"
|
||||
: "All todos marked completed"
|
||||
@@ -21,14 +22,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}
|
||||
|
||||
@@ -49,6 +43,29 @@ Task NOT complete without:
|
||||
return prompt + "\n\n" + 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 {
|
||||
if (useTaskSystem) {
|
||||
return `<Task_Discipline>
|
||||
|
||||
@@ -21,6 +21,7 @@ export function buildGptSisyphusJuniorPrompt(
|
||||
promptAppend?: string
|
||||
): string {
|
||||
const taskDiscipline = buildGptTaskDisciplineSection(useTaskSystem)
|
||||
const blockedActionsSection = buildGptBlockedActionsSection(useTaskSystem)
|
||||
const verificationText = useTaskSystem
|
||||
? "All tasks marked completed"
|
||||
: "All todos marked completed"
|
||||
@@ -45,20 +46,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:
|
||||
@@ -100,6 +88,42 @@ Task NOT complete without evidence:
|
||||
return prompt + "\n\n" + 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 {
|
||||
if (useTaskSystem) {
|
||||
return `<task_discipline_spec>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -214,8 +214,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 +275,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:
|
||||
@@ -340,7 +346,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 +364,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.**
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
/// <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"
|
||||
import * as modelAvailability from "../shared/model-availability"
|
||||
import * as shared from "../shared"
|
||||
|
||||
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-6"
|
||||
|
||||
describe("createBuiltinAgents with model overrides", () => {
|
||||
test("Sisyphus with default model has thinking config when all models available", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set([
|
||||
"anthropic/claude-opus-4-5",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"kimi-for-coding/k2p5",
|
||||
"opencode/kimi-k2.5-free",
|
||||
"zai-coding-plan/glm-4.7",
|
||||
@@ -26,7 +28,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
expect(agents.sisyphus.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
|
||||
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
|
||||
} finally {
|
||||
@@ -41,7 +43,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
@@ -79,9 +81,75 @@ 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-5"
|
||||
const systemDefaultModel = "anthropic/claude-opus-4-6"
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
||||
|
||||
@@ -91,7 +159,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
} finally {
|
||||
cacheSpy.mockRestore()
|
||||
fetchSpy.mockRestore()
|
||||
@@ -103,7 +171,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
@@ -132,7 +200,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
@@ -148,7 +216,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4")
|
||||
@@ -164,12 +232,241 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||
expect(agents.sisyphus.temperature).toBe(0.5)
|
||||
})
|
||||
|
||||
test("createBuiltinAgents excludes disabled skills from availableSkills", async () => {
|
||||
// #given
|
||||
const disabledSkills = new Set(["playwright"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined, undefined, disabledSkills)
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus.prompt).not.toContain("playwright")
|
||||
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", () => {
|
||||
@@ -205,7 +502,7 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
])
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set([
|
||||
"anthropic/claude-opus-4-5",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"kimi-for-coding/k2p5",
|
||||
"opencode/kimi-k2.5-free",
|
||||
"zai-coding-plan/glm-4.7",
|
||||
@@ -219,7 +516,7 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
} finally {
|
||||
cacheSpy.mockRestore()
|
||||
fetchSpy.mockRestore()
|
||||
@@ -227,12 +524,13 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("createBuiltinAgents with requiresModel gating", () => {
|
||||
test("hephaestus is not created when gpt-5.2-codex is unavailable", async () => {
|
||||
// #given
|
||||
describe("createBuiltinAgents with requiresProvider gating (hephaestus)", () => {
|
||||
test("hephaestus is not created when no required provider is connected", async () => {
|
||||
// #given - only anthropic models available, not in hephaestus requiresProvider
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-5"])
|
||||
new Set(["anthropic/claude-opus-4-6"])
|
||||
)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
|
||||
|
||||
try {
|
||||
// #when
|
||||
@@ -242,13 +540,48 @@ describe("createBuiltinAgents with requiresModel gating", () => {
|
||||
expect(agents.hephaestus).toBeUndefined()
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
cacheSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("hephaestus is created when gpt-5.2-codex is available", async () => {
|
||||
// #given
|
||||
test("hephaestus is created when openai provider is connected", async () => {
|
||||
// #given - openai provider has models available
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2-codex"])
|
||||
new Set(["openai/gpt-5.3-codex"])
|
||||
)
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||
|
||||
// #then
|
||||
expect(agents.hephaestus).toBeDefined()
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("hephaestus is created when github-copilot provider is connected", async () => {
|
||||
// #given - github-copilot provider has models available
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["github-copilot/gpt-5.3-codex"])
|
||||
)
|
||||
|
||||
try {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||
|
||||
// #then
|
||||
expect(agents.hephaestus).toBeDefined()
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("hephaestus is created when opencode provider is connected", async () => {
|
||||
// #given - opencode provider has models available
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["opencode/gpt-5.3-codex"])
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -273,20 +606,20 @@ describe("createBuiltinAgents with requiresModel gating", () => {
|
||||
|
||||
// #then
|
||||
expect(agents.hephaestus).toBeDefined()
|
||||
expect(agents.hephaestus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.hephaestus.model).toBe("openai/gpt-5.3-codex")
|
||||
} finally {
|
||||
cacheSpy.mockRestore()
|
||||
fetchSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("hephaestus is created when explicit config provided even if model unavailable", async () => {
|
||||
test("hephaestus is created when explicit config provided even if provider unavailable", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-5"])
|
||||
new Set(["anthropic/claude-opus-4-6"])
|
||||
)
|
||||
const overrides = {
|
||||
hephaestus: { model: "anthropic/claude-opus-4-5" },
|
||||
hephaestus: { model: "anthropic/claude-opus-4-6" },
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -305,7 +638,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
test("sisyphus is created when at least one fallback model is available", async () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["anthropic/claude-opus-4-5"])
|
||||
new Set(["anthropic/claude-opus-4-6"])
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -330,7 +663,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
|
||||
// #then
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
} finally {
|
||||
cacheSpy.mockRestore()
|
||||
fetchSpy.mockRestore()
|
||||
@@ -341,7 +674,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
// #given
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
||||
const overrides = {
|
||||
sisyphus: { model: "anthropic/claude-opus-4-5" },
|
||||
sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -355,11 +688,12 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("sisyphus is not created when no fallback model is available (unrelated model only)", async () => {
|
||||
test("sisyphus is not created when no fallback model is available and provider not connected", async () => {
|
||||
// #given - only openai/gpt-5.2 available, not in sisyphus fallback chain
|
||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||
new Set(["openai/gpt-5.2"])
|
||||
)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([])
|
||||
|
||||
try {
|
||||
// #when
|
||||
@@ -369,13 +703,66 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
||||
expect(agents.sisyphus).toBeUndefined()
|
||||
} finally {
|
||||
fetchSpy.mockRestore()
|
||||
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 TEST_MODEL = "anthropic/claude-opus-4-5"
|
||||
const { buildAgent } = require("./agent-builder")
|
||||
const TEST_MODEL = "anthropic/claude-opus-4-6"
|
||||
|
||||
beforeEach(() => {
|
||||
clearSkillCache()
|
||||
@@ -521,7 +908,7 @@ describe("buildAgent with category and skills", () => {
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then - category's built-in model and skills are applied
|
||||
expect(agent.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agent.model).toBe("openai/gpt-5.3-codex")
|
||||
expect(agent.variant).toBe("xhigh")
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
expect(agent.prompt).toContain("Task description")
|
||||
@@ -634,9 +1021,9 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
// #then - ultrabrain category: model=openai/gpt-5.3-codex, variant=xhigh
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.3-codex")
|
||||
expect(agents.oracle.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
@@ -703,9 +1090,9 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
// #then - ultrabrain category: model=openai/gpt-5.3-codex, variant=xhigh
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.sisyphus.model).toBe("openai/gpt-5.3-codex")
|
||||
expect(agents.sisyphus.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
@@ -718,9 +1105,9 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
// #then - ultrabrain category: model=openai/gpt-5.3-codex, variant=xhigh
|
||||
expect(agents.atlas).toBeDefined()
|
||||
expect(agents.atlas.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.atlas.model).toBe("openai/gpt-5.3-codex")
|
||||
expect(agents.atlas.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
@@ -740,6 +1127,52 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("agent override tools migration", () => {
|
||||
test("tools: { x: false } is migrated to permission: { x: deny }", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
explore: { tools: { "jetbrains_*": false } } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.explore).toBeDefined()
|
||||
const permission = agents.explore.permission as Record<string, string>
|
||||
expect(permission["jetbrains_*"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("tools: { x: true } is migrated to permission: { x: allow }", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
librarian: { tools: { "jetbrains_get_*": true } } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.librarian).toBeDefined()
|
||||
const permission = agents.librarian.permission as Record<string, string>
|
||||
expect(permission["jetbrains_get_*"]).toBe("allow")
|
||||
})
|
||||
|
||||
test("tools config is removed after migration", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
explore: { tools: { "some_tool": false } } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then
|
||||
expect(agents.explore).toBeDefined()
|
||||
expect((agents.explore as any).tools).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
|
||||
test("createBuiltinAgents should call fetchAvailableModels with undefined client to prevent deadlock", async () => {
|
||||
// #given - This test ensures we don't regress on issue #1301
|
||||
@@ -776,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,482 +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 } 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
|
||||
): 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 })
|
||||
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 { prompt_append, ...rest } = override
|
||||
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
|
||||
): 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 })
|
||||
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)
|
||||
|
||||
// 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 hasRequiredModel =
|
||||
!hephaestusRequirement?.requiresModel ||
|
||||
hasHephaestusExplicitConfig ||
|
||||
isFirstRunNoCache ||
|
||||
(availableModels.size > 0 && isModelAvailable(hephaestusRequirement.requiresModel, availableModels))
|
||||
|
||||
if (hasRequiredModel) {
|
||||
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,77 +2,71 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
CLI entry: `bunx oh-my-opencode`. 4 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
|
||||
**Commands**: install, run, doctor, get-local-version, mcp-oauth
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry (4 commands)
|
||||
├── install.ts # Interactive TUI (542 lines)
|
||||
├── config-manager.ts # JSONC parsing (667 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
├── doctor/
|
||||
│ ├── index.ts # Doctor entry
|
||||
│ ├── runner.ts # Check orchestration
|
||||
│ ├── formatter.ts # Colored output
|
||||
│ ├── constants.ts # Check IDs, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
|
||||
│ └── 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
|
||||
│ └── gh.ts # GitHub CLI
|
||||
├── run/
|
||||
│ └── index.ts # Session launcher
|
||||
├── 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 |
|
||||
| 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
|
||||
|
||||
@@ -75,26 +75,26 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
@@ -103,7 +103,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -113,7 +113,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"writing": {
|
||||
@@ -137,26 +137,26 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
@@ -165,18 +165,18 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"writing": {
|
||||
@@ -197,7 +197,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
@@ -225,22 +225,22 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
},
|
||||
"categories": {
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"visual-engineering": {
|
||||
@@ -264,7 +264,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
@@ -292,14 +292,14 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
},
|
||||
"categories": {
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -307,7 +307,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"variant": "high",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"visual-engineering": {
|
||||
@@ -335,18 +335,18 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
},
|
||||
"metis": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"momus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
@@ -355,14 +355,14 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "google/gemini-3-flash",
|
||||
@@ -395,18 +395,18 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
},
|
||||
"metis": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"momus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro",
|
||||
@@ -415,14 +415,14 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "google/gemini-3-pro",
|
||||
@@ -451,14 +451,14 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -473,28 +473,28 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -524,14 +524,14 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -546,32 +546,32 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-low": {
|
||||
@@ -598,14 +598,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -620,28 +620,28 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -671,14 +671,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -693,32 +693,32 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
"model": "opencode/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-low": {
|
||||
@@ -745,14 +745,14 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -767,28 +767,28 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "github-copilot/claude-haiku-4.5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -818,14 +818,14 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -840,32 +840,32 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "github-copilot/claude-haiku-4.5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-low": {
|
||||
@@ -1002,14 +1002,14 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -1024,28 +1024,28 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/gpt-5.2-codex",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -1075,14 +1075,14 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -1097,28 +1097,28 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "github-copilot/claude-haiku-4.5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -1151,26 +1151,26 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
@@ -1179,7 +1179,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -1189,7 +1189,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"writing": {
|
||||
@@ -1213,11 +1213,11 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
@@ -1225,28 +1225,28 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
@@ -1275,14 +1275,14 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -1297,28 +1297,28 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "github-copilot/claude-opus-4.5",
|
||||
"model": "github-copilot/claude-opus-4.6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "github-copilot/claude-haiku-4.5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "github-copilot/gpt-5.2-codex",
|
||||
"model": "github-copilot/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -1348,14 +1348,14 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -1370,28 +1370,28 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
@@ -1421,14 +1421,14 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
@@ -1443,32 +1443,32 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"variant": "high",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
"variant": "high",
|
||||
},
|
||||
"deep": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "xhigh",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
"variant": "max",
|
||||
},
|
||||
"unspecified-low": {
|
||||
|
||||
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
|
||||
}
|
||||
191
src/cli/cli-program.ts
Normal file
191
src/cli/cli-program.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
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("--verbose", "Show detailed diagnostic information")
|
||||
.option("--json", "Output results in JSON format")
|
||||
.option("--category <category>", "Run only specific category")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode doctor
|
||||
$ bunx oh-my-opencode doctor --verbose
|
||||
$ bunx oh-my-opencode doctor --json
|
||||
$ bunx oh-my-opencode doctor --category authentication
|
||||
|
||||
Categories:
|
||||
installation Check OpenCode and plugin installation
|
||||
configuration Validate configuration files
|
||||
authentication Check auth provider status
|
||||
dependencies Check external dependencies
|
||||
tools Check LSP and MCP servers
|
||||
updates Check for version updates
|
||||
`)
|
||||
.action(async (options) => {
|
||||
const doctorOptions: DoctorOptions = {
|
||||
verbose: options.verbose ?? false,
|
||||
json: options.json ?? false,
|
||||
category: options.category,
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -259,7 +259,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
// #then Sisyphus uses Claude (OR logic - at least one provider available)
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect(result.agents).toBeDefined()
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("generates native opus models when Claude max20 subscription", () => {
|
||||
@@ -279,7 +279,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then Sisyphus uses Claude (OR logic - at least one provider available)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("uses github-copilot sonnet fallback when only copilot available", () => {
|
||||
@@ -298,8 +298,8 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then Sisyphus uses Copilot (OR logic - copilot is in claude-opus-4-5 providers)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-opus-4.5")
|
||||
// #then Sisyphus uses Copilot (OR logic - copilot is in claude-opus-4-6 providers)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-opus-4.6")
|
||||
})
|
||||
|
||||
test("uses ultimate fallback when no providers configured", () => {
|
||||
@@ -342,7 +342,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
// #then librarian should use zai-coding-plan/glm-4.7
|
||||
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
|
||||
// #then Sisyphus uses Claude (OR logic)
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("uses native OpenAI models when only ChatGPT available", () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
21
src/cli/config-manager/npm-dist-tags.ts
Normal file
21
src/cli/config-manager/npm-dist-tags.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export 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/${encodeURIComponent(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
|
||||
}
|
||||
}
|
||||
40
src/cli/config-manager/opencode-binary.ts
Normal file
40
src/cli/config-manager/opencode-binary.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { OpenCodeBinaryType } from "../../shared/opencode-config-dir-types"
|
||||
import { initConfigContext } from "./config-context"
|
||||
|
||||
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
||||
|
||||
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
|
||||
}
|
||||
17
src/cli/config-manager/opencode-config-format.ts
Normal file
17
src/cli/config-manager/opencode-config-format.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import { getConfigJson, getConfigJsonc } from "./config-context"
|
||||
|
||||
export type ConfigFormat = "json" | "jsonc" | "none"
|
||||
|
||||
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 }
|
||||
}
|
||||
48
src/cli/config-manager/parse-opencode-config-file.ts
Normal file
48
src/cli/config-manager/parse-opencode-config-file.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { readFileSync, statSync } from "node:fs"
|
||||
import { parseJsonc } from "../../shared"
|
||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||
|
||||
interface ParseConfigResult {
|
||||
config: OpenCodeConfig | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface OpenCodeConfig {
|
||||
plugin?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function isEmptyOrWhitespace(content: string): boolean {
|
||||
return content.trim().length === 0
|
||||
}
|
||||
|
||||
export function parseOpenCodeConfigFileWithError(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}`) }
|
||||
}
|
||||
}
|
||||
19
src/cli/config-manager/plugin-name-with-version.ts
Normal file
19
src/cli/config-manager/plugin-name-with-version.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fetchNpmDistTags } from "./npm-dist-tags"
|
||||
|
||||
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}`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user