Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 |
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -255,35 +255,43 @@ jobs:
|
|||||||
DOCS=""
|
DOCS=""
|
||||||
OTHER=""
|
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
|
while IFS= read -r commit; do
|
||||||
[ -z "$commit" ] && continue
|
[ -z "$commit" ] && continue
|
||||||
# Skip chore, ci, release, test commits
|
# Skip chore, ci, release, test commits
|
||||||
[[ "$commit" =~ ^(chore|ci|release|test|ignore) ]] && continue
|
[[ "$commit" =~ $re_skip ]] && continue
|
||||||
|
|
||||||
if [[ "$commit" =~ ^feat ]]; then
|
if [[ "$commit" =~ ^feat ]]; then
|
||||||
# Extract scope and message: feat(scope): message -> **scope**: message
|
# 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]}"
|
FEATURES="${FEATURES}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||||
else
|
else
|
||||||
MSG="${commit#feat: }"
|
MSG="${commit#feat: }"
|
||||||
FEATURES="${FEATURES}\n- ${MSG}"
|
FEATURES="${FEATURES}\n- ${MSG}"
|
||||||
fi
|
fi
|
||||||
elif [[ "$commit" =~ ^fix ]]; then
|
elif [[ "$commit" =~ ^fix ]]; then
|
||||||
if [[ "$commit" =~ ^fix\(([^)]+)\):\ (.+)$ ]]; then
|
if [[ "$commit" =~ $re_fix_scoped ]]; then
|
||||||
FIXES="${FIXES}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
FIXES="${FIXES}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||||
else
|
else
|
||||||
MSG="${commit#fix: }"
|
MSG="${commit#fix: }"
|
||||||
FIXES="${FIXES}\n- ${MSG}"
|
FIXES="${FIXES}\n- ${MSG}"
|
||||||
fi
|
fi
|
||||||
elif [[ "$commit" =~ ^refactor ]]; then
|
elif [[ "$commit" =~ ^refactor ]]; then
|
||||||
if [[ "$commit" =~ ^refactor\(([^)]+)\):\ (.+)$ ]]; then
|
if [[ "$commit" =~ $re_refactor_scoped ]]; then
|
||||||
REFACTOR="${REFACTOR}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
REFACTOR="${REFACTOR}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||||
else
|
else
|
||||||
MSG="${commit#refactor: }"
|
MSG="${commit#refactor: }"
|
||||||
REFACTOR="${REFACTOR}\n- ${MSG}"
|
REFACTOR="${REFACTOR}\n- ${MSG}"
|
||||||
fi
|
fi
|
||||||
elif [[ "$commit" =~ ^docs ]]; then
|
elif [[ "$commit" =~ ^docs ]]; then
|
||||||
if [[ "$commit" =~ ^docs\(([^)]+)\):\ (.+)$ ]]; then
|
if [[ "$commit" =~ $re_docs_scoped ]]; then
|
||||||
DOCS="${DOCS}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
DOCS="${DOCS}\n- **${BASH_REMATCH[1]}**: ${BASH_REMATCH[2]}"
|
||||||
else
|
else
|
||||||
MSG="${commit#docs: }"
|
MSG="${commit#docs: }"
|
||||||
|
|||||||
82
AGENTS.md
82
AGENTS.md
@@ -1,7 +1,7 @@
|
|||||||
# PROJECT KNOWLEDGE BASE
|
# PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
**Generated:** 2026-02-03T16:10:30+09:00
|
**Generated:** 2026-02-06T18:30:00+09:00
|
||||||
**Commit:** d7679e14
|
**Commit:** c6c149e
|
||||||
**Branch:** dev
|
**Branch:** dev
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -120,40 +120,45 @@ This is an **international open-source project**. To ensure accessibility and ma
|
|||||||
|
|
||||||
## OVERVIEW
|
## 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: multi-model agent orchestration (Claude Opus 4.6, GPT-5.3 Codex, Gemini 3 Flash). 40+ lifecycle hooks, 25+ tools (LSP, AST-Grep, delegation), 11 specialized agents, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
|
||||||
|
|
||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
|
|
||||||
```
|
```
|
||||||
oh-my-opencode/
|
oh-my-opencode/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── agents/ # 11 AI agents - see src/agents/AGENTS.md
|
│ ├── agents/ # 11 AI agents - see src/agents/AGENTS.md
|
||||||
│ ├── hooks/ # 34 lifecycle hooks - see src/hooks/AGENTS.md
|
│ ├── hooks/ # 40+ lifecycle hooks - see src/hooks/AGENTS.md
|
||||||
│ ├── tools/ # 20+ tools - see src/tools/AGENTS.md
|
│ ├── tools/ # 25+ tools - see src/tools/AGENTS.md
|
||||||
│ ├── features/ # Background agents, Claude Code compat - see src/features/AGENTS.md
|
│ ├── features/ # Background agents, skills, Claude Code compat - see src/features/AGENTS.md
|
||||||
│ ├── shared/ # 66 cross-cutting utilities - see src/shared/AGENTS.md
|
│ ├── shared/ # 66 cross-cutting utilities - see src/shared/AGENTS.md
|
||||||
│ ├── cli/ # CLI installer, doctor - see src/cli/AGENTS.md
|
│ ├── cli/ # CLI installer, doctor - see src/cli/AGENTS.md
|
||||||
│ ├── mcp/ # Built-in MCPs - see src/mcp/AGENTS.md
|
│ ├── mcp/ # Built-in MCPs - see src/mcp/AGENTS.md
|
||||||
│ ├── config/ # Zod schema, TypeScript types
|
│ ├── config/ # Zod schema (schema.ts 455 lines), TypeScript types
|
||||||
│ └── index.ts # Main plugin entry (788 lines)
|
│ ├── plugin-handlers/ # Plugin config loading (config-handler.ts 501 lines)
|
||||||
├── script/ # build-schema.ts, build-binaries.ts
|
│ ├── index.ts # Main plugin entry (924 lines)
|
||||||
├── packages/ # 11 platform-specific binaries
|
│ ├── plugin-config.ts # Config loading orchestration
|
||||||
└── dist/ # Build output (ESM + .d.ts)
|
│ └── plugin-state.ts # Model cache state
|
||||||
|
├── script/ # build-schema.ts, build-binaries.ts, publish.ts
|
||||||
|
├── packages/ # 11 platform-specific binaries
|
||||||
|
└── dist/ # Build output (ESM + .d.ts)
|
||||||
```
|
```
|
||||||
|
|
||||||
## WHERE TO LOOK
|
## WHERE TO LOOK
|
||||||
|
|
||||||
| Task | Location | Notes |
|
| Task | Location | Notes |
|
||||||
|------|----------|-------|
|
|------|----------|-------|
|
||||||
| Add agent | `src/agents/` | Create .ts with factory, add to `agentSources` |
|
| Add agent | `src/agents/` | Create .ts with factory, add to `agentSources` in utils.ts |
|
||||||
| Add hook | `src/hooks/` | Create dir with `createXXXHook()`, register in index.ts |
|
| Add hook | `src/hooks/` | Create dir with `createXXXHook()`, register in index.ts |
|
||||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
|
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
|
||||||
| Add MCP | `src/mcp/` | Create config, add to index.ts |
|
| Add MCP | `src/mcp/` | Create config, add to `createBuiltinMcps()` |
|
||||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||||
| Add command | `src/features/builtin-commands/` | Add template + register in commands.ts |
|
| Add command | `src/features/builtin-commands/` | Add template + register in commands.ts |
|
||||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` |
|
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` |
|
||||||
| Background agents | `src/features/background-agent/` | manager.ts (1418 lines) |
|
| Plugin config | `src/plugin-handlers/config-handler.ts` | JSONC loading, merging, migration |
|
||||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (757 lines) |
|
| Background agents | `src/features/background-agent/` | manager.ts (1556 lines) |
|
||||||
|
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (770 lines) |
|
||||||
|
| Delegation | `src/tools/delegate-task/` | Category routing (executor.ts 983 lines) |
|
||||||
|
|
||||||
## TDD (Test-Driven Development)
|
## TDD (Test-Driven Development)
|
||||||
|
|
||||||
@@ -165,7 +170,7 @@ oh-my-opencode/
|
|||||||
**Rules:**
|
**Rules:**
|
||||||
- NEVER write implementation before test
|
- NEVER write implementation before test
|
||||||
- NEVER delete failing tests - fix the code
|
- NEVER delete failing tests - fix the code
|
||||||
- Test file: `*.test.ts` alongside source (100 test files)
|
- Test file: `*.test.ts` alongside source (100+ test files)
|
||||||
- BDD comments: `//#given`, `//#when`, `//#then`
|
- BDD comments: `//#given`, `//#when`, `//#then`
|
||||||
|
|
||||||
## CONVENTIONS
|
## CONVENTIONS
|
||||||
@@ -175,7 +180,7 @@ oh-my-opencode/
|
|||||||
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||||
- **Exports**: Barrel pattern via index.ts
|
- **Exports**: Barrel pattern via index.ts
|
||||||
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
|
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
|
||||||
- **Testing**: BDD comments, 100 test files
|
- **Testing**: BDD comments, 100+ test files
|
||||||
- **Temperature**: 0.1 for code agents, max 0.3
|
- **Temperature**: 0.1 for code agents, max 0.3
|
||||||
|
|
||||||
## ANTI-PATTERNS
|
## ANTI-PATTERNS
|
||||||
@@ -204,14 +209,17 @@ oh-my-opencode/
|
|||||||
|
|
||||||
| Agent | Model | Purpose |
|
| 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) |
|
| Sisyphus | anthropic/claude-opus-4-6 | Primary orchestrator (fallback: kimi-k2.5 → glm-4.7 → gpt-5.3-codex → gemini-3-pro) |
|
||||||
| Hephaestus | openai/gpt-5.2-codex | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.2-codex, no fallback) |
|
| Hephaestus | openai/gpt-5.3-codex | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.3-codex, no fallback) |
|
||||||
| Atlas | anthropic/claude-sonnet-4-5 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
|
| Atlas | anthropic/claude-sonnet-4-5 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
|
||||||
| oracle | openai/gpt-5.2 | Consultation, debugging |
|
| oracle | openai/gpt-5.2 | Consultation, debugging |
|
||||||
| librarian | zai-coding-plan/glm-4.7 | Docs, GitHub search (fallback: glm-4.7-free) |
|
| 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) |
|
| 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 |
|
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
|
||||||
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
| Prometheus | anthropic/claude-opus-4-6 | Strategic planning (fallback: kimi-k2.5 → gpt-5.2) |
|
||||||
|
| Metis | anthropic/claude-opus-4-6 | Pre-planning analysis (temp 0.3, fallback: kimi-k2.5 → gpt-5.2) |
|
||||||
|
| Momus | openai/gpt-5.2 | Plan validation (temp 0.1, fallback: claude-opus-4-6) |
|
||||||
|
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | Category-spawned executor (temp 0.1) |
|
||||||
|
|
||||||
## COMMANDS
|
## COMMANDS
|
||||||
|
|
||||||
@@ -219,7 +227,7 @@ oh-my-opencode/
|
|||||||
bun run typecheck # Type check
|
bun run typecheck # Type check
|
||||||
bun run build # ESM + declarations + schema
|
bun run build # ESM + declarations + schema
|
||||||
bun run rebuild # Clean + Build
|
bun run rebuild # Clean + Build
|
||||||
bun test # 100 test files
|
bun test # 100+ test files
|
||||||
```
|
```
|
||||||
|
|
||||||
## DEPLOYMENT
|
## DEPLOYMENT
|
||||||
@@ -233,30 +241,38 @@ bun test # 100 test files
|
|||||||
|
|
||||||
| File | Lines | Description |
|
| File | Lines | Description |
|
||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| `src/features/builtin-skills/skills.ts` | 1729 | Skill definitions |
|
| `src/features/background-agent/manager.ts` | 1556 | Task lifecycle, concurrency |
|
||||||
| `src/features/background-agent/manager.ts` | 1418 | Task lifecycle, concurrency |
|
| `src/features/builtin-skills/skills/git-master.ts` | 1107 | Git master skill definition |
|
||||||
| `src/agents/prometheus-prompt.ts` | 1283 | Planning agent prompt |
|
| `src/tools/delegate-task/executor.ts` | 983 | Category-based delegation executor |
|
||||||
| `src/tools/delegate-task/tools.ts` | 1135 | Category-based delegation |
|
| `src/index.ts` | 924 | Main plugin entry |
|
||||||
| `src/hooks/atlas/index.ts` | 757 | Orchestrator hook |
|
| `src/tools/lsp/client.ts` | 803 | LSP client operations |
|
||||||
| `src/index.ts` | 788 | Main plugin entry |
|
| `src/hooks/atlas/index.ts` | 770 | Orchestrator hook |
|
||||||
|
| `src/tools/background-task/tools.ts` | 734 | Background task tools |
|
||||||
| `src/cli/config-manager.ts` | 667 | JSONC config parsing |
|
| `src/cli/config-manager.ts` | 667 | JSONC config parsing |
|
||||||
|
| `src/features/skill-mcp-manager/manager.ts` | 640 | MCP client lifecycle |
|
||||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
||||||
|
| `src/agents/hephaestus.ts` | 618 | Autonomous deep worker agent |
|
||||||
|
| `src/tools/delegate-task/constants.ts` | 552 | Delegation constants |
|
||||||
|
| `src/cli/install.ts` | 542 | Interactive CLI installer |
|
||||||
|
| `src/agents/sisyphus.ts` | 530 | Main orchestrator agent |
|
||||||
|
|
||||||
## MCP ARCHITECTURE
|
## MCP ARCHITECTURE
|
||||||
|
|
||||||
Three-tier system:
|
Three-tier system:
|
||||||
1. **Built-in**: websearch (Exa), context7 (docs), grep_app (GitHub)
|
1. **Built-in**: websearch (Exa/Tavily), context7 (docs), grep_app (GitHub)
|
||||||
2. **Claude Code compat**: .mcp.json with `${VAR}` expansion
|
2. **Claude Code compat**: .mcp.json with `${VAR}` expansion
|
||||||
3. **Skill-embedded**: YAML frontmatter in skills
|
3. **Skill-embedded**: YAML frontmatter in skills
|
||||||
|
|
||||||
## CONFIG SYSTEM
|
## CONFIG SYSTEM
|
||||||
|
|
||||||
- **Zod validation**: `src/config/schema.ts`
|
- **Zod validation**: `src/config/schema.ts` (455 lines)
|
||||||
- **JSONC support**: Comments, trailing commas
|
- **JSONC support**: Comments, trailing commas
|
||||||
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
|
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
|
||||||
|
- **Loading**: `src/plugin-handlers/config-handler.ts` → merge → validate
|
||||||
|
|
||||||
## NOTES
|
## NOTES
|
||||||
|
|
||||||
- **OpenCode**: Requires >= 1.0.150
|
- **OpenCode**: Requires >= 1.0.150
|
||||||
- **Flaky tests**: ralph-loop (CI timeout), session-state (parallel pollution)
|
- **Flaky tests**: ralph-loop (CI timeout), session-state (parallel pollution)
|
||||||
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
||||||
|
- **No linter/formatter**: No ESLint, Prettier, or Biome configured
|
||||||
|
|||||||
10
README.ja.md
10
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)
|
|
||||||
- [作者のノート](#作者のノート)
|
- [作者のノート](#作者のノート)
|
||||||
- [注意](#注意)
|
- [注意](#注意)
|
||||||
- [こちらの企業の専門家にご愛用いただいています](#こちらの企業の専門家にご愛用いただいています)
|
- [こちらの企業の専門家にご愛用いただいています](#こちらの企業の専門家にご愛用いただいています)
|
||||||
|
|||||||
14
README.ko.md
14
README.ko.md
@@ -123,20 +123,6 @@
|
|||||||
- [제거](#제거)
|
- [제거](#제거)
|
||||||
- [기능](#기능)
|
- [기능](#기능)
|
||||||
- [구성](#구성)
|
- [구성](#구성)
|
||||||
- [JSONC 지원](#jsonc-지원)
|
|
||||||
- [Google 인증](#google-인증)
|
|
||||||
- [에이전트](#에이전트)
|
|
||||||
- [권한 옵션](#권한-옵션)
|
|
||||||
- [내장 스킬](#내장-스킬)
|
|
||||||
- [Git Master](#git-master)
|
|
||||||
- [Sisyphus 에이전트](#sisyphus-에이전트)
|
|
||||||
- [백그라운드 작업](#백그라운드-작업)
|
|
||||||
- [카테고리](#카테고리)
|
|
||||||
- [훅](#훅)
|
|
||||||
- [MCP](#mcp)
|
|
||||||
- [LSP](#lsp)
|
|
||||||
- [실험적 기능](#실험적-기능)
|
|
||||||
- [환경 변수](#환경-변수)
|
|
||||||
- [작성자의 메모](#작성자의-메모)
|
- [작성자의 메모](#작성자의-메모)
|
||||||
- [경고](#경고)
|
- [경고](#경고)
|
||||||
- [다음 기업 전문가들이 사랑합니다](#다음-기업-전문가들이-사랑합니다)
|
- [다음 기업 전문가들이 사랑합니다](#다음-기업-전문가들이-사랑합니다)
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -121,21 +121,7 @@ Yes, technically possible. But I cannot recommend using it.
|
|||||||
- [For LLM Agents](#for-llm-agents)
|
- [For LLM Agents](#for-llm-agents)
|
||||||
- [Uninstallation](#uninstallation)
|
- [Uninstallation](#uninstallation)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Configuration](#configuration)
|
- [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)
|
|
||||||
- [Author's Note](#authors-note)
|
- [Author's Note](#authors-note)
|
||||||
- [Warnings](#warnings)
|
- [Warnings](#warnings)
|
||||||
- [Loved by professionals at](#loved-by-professionals-at)
|
- [Loved by professionals at](#loved-by-professionals-at)
|
||||||
|
|||||||
@@ -122,20 +122,6 @@
|
|||||||
- [卸载](#卸载)
|
- [卸载](#卸载)
|
||||||
- [功能特性](#功能特性)
|
- [功能特性](#功能特性)
|
||||||
- [配置](#配置)
|
- [配置](#配置)
|
||||||
- [JSONC 支持](#jsonc-支持)
|
|
||||||
- [Google 认证](#google-认证)
|
|
||||||
- [智能体](#智能体)
|
|
||||||
- [权限选项](#权限选项)
|
|
||||||
- [内置技能](#内置技能)
|
|
||||||
- [Git Master](#git-master)
|
|
||||||
- [Sisyphus 智能体](#sisyphus-智能体)
|
|
||||||
- [后台任务](#后台任务)
|
|
||||||
- [类别](#类别)
|
|
||||||
- [钩子](#钩子)
|
|
||||||
- [MCP](#mcp)
|
|
||||||
- [LSP](#lsp)
|
|
||||||
- [实验性功能](#实验性功能)
|
|
||||||
- [环境变量](#环境变量)
|
|
||||||
- [作者札记](#作者札记)
|
- [作者札记](#作者札记)
|
||||||
- [警告](#警告)
|
- [警告](#警告)
|
||||||
- [受到以下专业人士的喜爱](#受到以下专业人士的喜爱)
|
- [受到以下专业人士的喜爱](#受到以下专业人士的喜爱)
|
||||||
|
|||||||
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,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 1,
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "oh-my-opencode",
|
"name": "oh-my-opencode",
|
||||||
@@ -28,13 +28,13 @@
|
|||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"oh-my-opencode-darwin-arm64": "3.2.1",
|
"oh-my-opencode-darwin-arm64": "3.2.3",
|
||||||
"oh-my-opencode-darwin-x64": "3.2.1",
|
"oh-my-opencode-darwin-x64": "3.2.3",
|
||||||
"oh-my-opencode-linux-arm64": "3.2.1",
|
"oh-my-opencode-linux-arm64": "3.2.3",
|
||||||
"oh-my-opencode-linux-arm64-musl": "3.2.1",
|
"oh-my-opencode-linux-arm64-musl": "3.2.3",
|
||||||
"oh-my-opencode-linux-x64": "3.2.1",
|
"oh-my-opencode-linux-x64": "3.2.3",
|
||||||
"oh-my-opencode-linux-x64-musl": "3.2.1",
|
"oh-my-opencode-linux-x64-musl": "3.2.3",
|
||||||
"oh-my-opencode-windows-x64": "3.2.1",
|
"oh-my-opencode-windows-x64": "3.2.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -44,41 +44,41 @@
|
|||||||
"@code-yeongyu/comment-checker",
|
"@code-yeongyu/comment-checker",
|
||||||
],
|
],
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
@@ -226,19 +226,19 @@
|
|||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"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.2.3", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Doc9xQCj5Jmx3PzouBIfvDwmfWM94Y9Q9IngFqOjrVpfBef9V/WIH0PlhJU6ps4BKGey8Nf2afFq3UE06Z63Hg=="],
|
||||||
|
|
||||||
"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.2.3", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-w7lO0Hn/AlLCHe33KPbje83Js2h5weDWVMuopEs6d3pi/1zkRDBEhCi63S4J0d0EKod9kEPQA6ojtdVJ4J39zQ=="],
|
||||||
|
|
||||||
"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.2.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-m1tS1jRLO2Svm5NuetK3BAgdAR8b2GkiIfMFoIYsLJTPmzIkXaigAYkFq+BXCs5JAbRmPmvjndz9cuCddnPADQ=="],
|
||||||
|
|
||||||
"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.2.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Q/0AGtOuUFGNGIX8F6iD5W8c2spbjrqVBPt0B7laQSwnScKs/BI+TvM6HRE37vhoWg+fzhAX3QYJ2H9Un9FYrg=="],
|
||||||
|
|
||||||
"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.2.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-RIAyoj2XbT8vH++5fPUkdO+D1tfqxh+iWto7CqWr1TgbABbBJljGk91HJgS9xjnxyCQJEpFhTmO7NMHKJcZOWQ=="],
|
||||||
|
|
||||||
"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.2.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nnQK3y7R4DrBvqdqRGbujL2oAAQnVVb23JHUbJPQ6YxrRRGWpLOVGvK5c16ykSFEUPl8eZDmi1ON/R4opKLOUw=="],
|
||||||
|
|
||||||
"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.2.3", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mt8E/TkpaCp04pvzwntT8x8TaqXDt3zCD5X2eA8ZZMrb5ofNr5HyG5G4SFXrUh+Ez3b/3YXpNWv6f6rnAlk1Dg=="],
|
||||||
|
|
||||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ A Category is an agent configuration preset optimized for specific domains.
|
|||||||
| Category | Default Model | Use Cases |
|
| Category | Default Model | Use Cases |
|
||||||
|----------|---------------|-----------|
|
|----------|---------------|-----------|
|
||||||
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
|
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
|
||||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
| `ultrabrain` | `openai/gpt-5.3-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. |
|
| `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 |
|
| `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 |
|
| `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-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 |
|
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
@@ -159,7 +159,7 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
|||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `description` | string | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
|
| `description` | string | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
|
||||||
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-5`) |
|
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-6`) |
|
||||||
| `variant` | string | Model variant (e.g., `max`, `xhigh`) |
|
| `variant` | string | Model variant (e.g., `max`, `xhigh`) |
|
||||||
| `temperature` | number | Creativity level (0.0 ~ 2.0). Lower is more deterministic. |
|
| `temperature` | number | Creativity level (0.0 ~ 2.0). Lower is more deterministic. |
|
||||||
| `top_p` | number | Nucleus sampling parameter (0.0 ~ 1.0) |
|
| `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
|
// 3. Configure thinking model and restrict tools
|
||||||
"deep-reasoning": {
|
"deep-reasoning": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"thinking": {
|
"thinking": {
|
||||||
"type": "enabled",
|
"type": "enabled",
|
||||||
"budgetTokens": 32000
|
"budgetTokens": 32000
|
||||||
|
|||||||
@@ -693,7 +693,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
|||||||
"google": 10
|
"google": 10
|
||||||
},
|
},
|
||||||
"modelConcurrency": {
|
"modelConcurrency": {
|
||||||
"anthropic/claude-opus-4-5": 2,
|
"anthropic/claude-opus-4-6": 2,
|
||||||
"google/gemini-3-flash": 10
|
"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 |
|
| `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) |
|
| `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`) |
|
| `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`
|
**Priority Order**: `modelConcurrency` > `providerConcurrency` > `defaultConcurrency`
|
||||||
|
|
||||||
@@ -725,11 +725,11 @@ All 7 categories come with optimal model defaults, but **you must configure them
|
|||||||
| Category | Built-in Default Model | Description |
|
| Category | Built-in Default Model | Description |
|
||||||
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
|
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
|
||||||
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
|
| `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 |
|
| `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|
|
| `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-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 |
|
| `writing` | `google/gemini-3-flash-preview` | Documentation, prose, technical writing |
|
||||||
|
|
||||||
### ⚠️ Critical: Model Resolution Priority
|
### ⚠️ 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"
|
"model": "google/gemini-3-pro-preview"
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh"
|
"variant": "xhigh"
|
||||||
},
|
},
|
||||||
"artistry": {
|
"artistry": {
|
||||||
@@ -782,7 +782,7 @@ All 7 categories come with optimal model defaults, but **you must configure them
|
|||||||
"model": "anthropic/claude-sonnet-4-5"
|
"model": "anthropic/claude-sonnet-4-5"
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max"
|
"variant": "max"
|
||||||
},
|
},
|
||||||
"writing": {
|
"writing": {
|
||||||
@@ -870,9 +870,9 @@ At runtime, Oh My OpenCode uses a 3-step resolution process to determine which m
|
|||||||
│ │ anthropic → github-copilot → opencode → antigravity │ │
|
│ │ anthropic → github-copilot → opencode → antigravity │ │
|
||||||
│ │ │ │ │ │ │ │
|
│ │ │ │ │ │ │ │
|
||||||
│ │ ▼ ▼ ▼ ▼ │ │
|
│ │ ▼ ▼ ▼ ▼ │ │
|
||||||
│ │ Try: anthropic/claude-opus-4-5 │ │
|
│ │ Try: anthropic/claude-opus-4-6 │ │
|
||||||
│ │ Try: github-copilot/claude-opus-4-5 │ │
|
│ │ Try: github-copilot/claude-opus-4-6 │ │
|
||||||
│ │ Try: opencode/claude-opus-4-5 │ │
|
│ │ Try: opencode/claude-opus-4-6 │ │
|
||||||
│ │ ... │ │
|
│ │ ... │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ Found in available models? → Return matched model │ │
|
│ │ 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 |
|
| 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 |
|
| **oracle** | `gpt-5.2` | openai → google → anthropic |
|
||||||
| **librarian** | `glm-4.7` | zai-coding-plan → opencode → anthropic |
|
| **librarian** | `glm-4.7` | zai-coding-plan → opencode → anthropic |
|
||||||
| **explore** | `claude-haiku-4-5` | anthropic → github-copilot → opencode |
|
| **explore** | `claude-haiku-4-5` | anthropic → github-copilot → opencode |
|
||||||
| **multimodal-looker** | `gemini-3-flash` | google → openai → zai-coding-plan → kimi-for-coding → anthropic → 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 |
|
| **Prometheus (Planner)** | `claude-opus-4-6` | anthropic → kimi-for-coding → openai → google |
|
||||||
| **Metis (Plan Consultant)** | `claude-opus-4-5` | 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 |
|
| **Momus (Plan Reviewer)** | `gpt-5.2` | openai → anthropic → google |
|
||||||
| **Atlas** | `claude-sonnet-4-5` | anthropic → kimi-for-coding → openai → 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 |
|
| Category | Model (no prefix) | Provider Priority Chain |
|
||||||
|----------|-------------------|-------------------------|
|
|----------|-------------------|-------------------------|
|
||||||
| **visual-engineering** | `gemini-3-pro` | google → anthropic → zai-coding-plan |
|
| **visual-engineering** | `gemini-3-pro` | google → anthropic → zai-coding-plan |
|
||||||
| **ultrabrain** | `gpt-5.2-codex` | openai → google → anthropic |
|
| **ultrabrain** | `gpt-5.3-codex` | openai → google → anthropic |
|
||||||
| **deep** | `gpt-5.2-codex` | openai → anthropic → google |
|
| **deep** | `gpt-5.3-codex` | openai → anthropic → google |
|
||||||
| **artistry** | `gemini-3-pro` | google → anthropic → openai |
|
| **artistry** | `gemini-3-pro` | google → anthropic → openai |
|
||||||
| **quick** | `claude-haiku-4-5` | anthropic → google → opencode |
|
| **quick** | `claude-haiku-4-5` | anthropic → google → opencode |
|
||||||
| **unspecified-low** | `claude-sonnet-4-5` | anthropic → openai → google |
|
| **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 |
|
| **writing** | `gemini-3-flash` | google → anthropic → zai-coding-plan → openai |
|
||||||
|
|
||||||
### Checking Your Configuration
|
### Checking Your Configuration
|
||||||
@@ -949,7 +949,7 @@ Override any agent or category model in `oh-my-opencode.json`:
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"visual-engineering": {
|
"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 |
|
| 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. |
|
| **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.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). |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **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 |
|
| 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. |
|
| **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-5` | Plan consultant - pre-planning analysis. Identifies hidden intentions, ambiguities, and AI failure points. 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-5 → 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
|
### Invoking Agents
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ After you install it, you can read this [overview guide](./overview.md) to under
|
|||||||
|
|
||||||
## For LLM Agents
|
## 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...'
|
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:
|
follow these steps:
|
||||||
@@ -191,7 +196,7 @@ When GitHub Copilot is the best available provider, oh-my-opencode uses these mo
|
|||||||
|
|
||||||
| Agent | Model |
|
| Agent | Model |
|
||||||
| ------------- | -------------------------------- |
|
| ------------- | -------------------------------- |
|
||||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
| **Sisyphus** | `github-copilot/claude-opus-4.6` |
|
||||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||||
| **Explore** | `opencode/gpt-5-nano` |
|
| **Explore** | `opencode/gpt-5-nano` |
|
||||||
| **Librarian** | `zai-coding-plan/glm-4.7` (if Z.ai available) or fallback |
|
| **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
|
||||||
|
|
||||||
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:
|
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
|
||||||
|
|
||||||
| Agent | Model |
|
| Agent | Model |
|
||||||
| ------------- | -------------------------------- |
|
| ------------- | -------------------------------- |
|
||||||
| **Sisyphus** | `opencode/claude-opus-4-5` |
|
| **Sisyphus** | `opencode/claude-opus-4-6` |
|
||||||
| **Oracle** | `opencode/gpt-5.2` |
|
| **Oracle** | `opencode/gpt-5.2` |
|
||||||
| **Explore** | `opencode/gpt-5-nano` |
|
| **Explore** | `opencode/gpt-5-nano` |
|
||||||
| **Librarian** | `opencode/glm-4.7-free` |
|
| **Librarian** | `opencode/glm-4.7-free` |
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ This "boulder pushing" mechanism is why the system is named after Sisyphus.
|
|||||||
```typescript
|
```typescript
|
||||||
// OLD: Model name creates distributional bias
|
// OLD: Model name creates distributional bias
|
||||||
delegate_task(agent="gpt-5.2", prompt="...") // Model knows its limitations
|
delegate_task(agent="gpt-5.2", prompt="...") // Model knows its limitations
|
||||||
delegate_task(agent="claude-opus-4.5", prompt="...") // Different self-perception
|
delegate_task(agent="claude-opus-4.6", prompt="...") // Different self-perception
|
||||||
```
|
```
|
||||||
|
|
||||||
**The Solution: Semantic Categories:**
|
**The Solution: Semantic Categories:**
|
||||||
|
|||||||
@@ -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
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
@@ -62,11 +271,11 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Key Components
|
## 6. Key Components
|
||||||
|
|
||||||
### 🔮 Prometheus (The Planner)
|
### 🔮 Prometheus (The Planner)
|
||||||
|
|
||||||
- **Model**: `anthropic/claude-opus-4-5`
|
- **Model**: `anthropic/claude-opus-4-6`
|
||||||
- **Role**: Strategic planning, requirements interviews, work plan creation
|
- **Role**: Strategic planning, requirements interviews, work plan creation
|
||||||
- **Constraint**: **READ-ONLY**. Can only create/modify markdown files within `.sisyphus/` directory.
|
- **Constraint**: **READ-ONLY**. Can only create/modify markdown files within `.sisyphus/` directory.
|
||||||
- **Characteristic**: Never writes code directly, focuses solely on "how to do it".
|
- **Characteristic**: Never writes code directly, focuses solely on "how to do it".
|
||||||
@@ -85,13 +294,13 @@ flowchart TD
|
|||||||
|
|
||||||
### ⚡ Atlas (The Plan Executor)
|
### ⚡ 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
|
- **Role**: Execution and delegation
|
||||||
- **Characteristic**: Doesn't do everything directly, actively delegates to specialized agents (Frontend, Librarian, etc.).
|
- **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)
|
### 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.
|
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.
|
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.
|
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`.
|
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]`
|
### `@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"`
|
- Example: `@plan "I want to refactor the authentication system to NextAuth"`
|
||||||
|
- Effect: Routes to Prometheus, then returns to Sisyphus when planning completes
|
||||||
|
|
||||||
### `/start-work`
|
### `/start-work`
|
||||||
|
|
||||||
Executes the generated plan.
|
Executes the generated plan.
|
||||||
|
|
||||||
- Function: Finds plan in `.sisyphus/plans/` and enters execution mode.
|
- **Fresh session**: Finds plan in `.sisyphus/plans/` and enters execution mode
|
||||||
- If there's interrupted work, automatically resumes from where it left off.
|
- **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`.
|
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.
|
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 `delegate_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)
|
||||||
|
↓
|
||||||
|
delegate_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",
|
"name": "oh-my-opencode",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
"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",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@@ -74,13 +74,13 @@
|
|||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"oh-my-opencode-darwin-arm64": "3.2.2",
|
"oh-my-opencode-darwin-arm64": "3.2.4",
|
||||||
"oh-my-opencode-darwin-x64": "3.2.2",
|
"oh-my-opencode-darwin-x64": "3.2.4",
|
||||||
"oh-my-opencode-linux-arm64": "3.2.2",
|
"oh-my-opencode-linux-arm64": "3.2.4",
|
||||||
"oh-my-opencode-linux-arm64-musl": "3.2.2",
|
"oh-my-opencode-linux-arm64-musl": "3.2.4",
|
||||||
"oh-my-opencode-linux-x64": "3.2.2",
|
"oh-my-opencode-linux-x64": "3.2.4",
|
||||||
"oh-my-opencode-linux-x64-musl": "3.2.2",
|
"oh-my-opencode-linux-x64-musl": "3.2.4",
|
||||||
"oh-my-opencode-windows-x64": "3.2.2"
|
"oh-my-opencode-windows-x64": "3.2.4"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@ast-grep/cli",
|
"@ast-grep/cli",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-darwin-arm64",
|
"name": "oh-my-opencode-darwin-arm64",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-darwin-x64",
|
"name": "oh-my-opencode-darwin-x64",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-linux-arm64-musl",
|
"name": "oh-my-opencode-linux-arm64-musl",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-linux-arm64",
|
"name": "oh-my-opencode-linux-arm64",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-linux-x64-musl",
|
"name": "oh-my-opencode-linux-x64-musl",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-linux-x64",
|
"name": "oh-my-opencode-linux-x64",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode-windows-x64",
|
"name": "oh-my-opencode-windows-x64",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import * as z from "zod"
|
import * as z from "zod"
|
||||||
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
import { OhMyOpenCodeConfigSchema } from "../src/config/schema"
|
||||||
|
|
||||||
const SCHEMA_OUTPUT_PATH = "assets/oh-my-opencode.schema.json"
|
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() {
|
async function main() {
|
||||||
console.log("Generating JSON Schema...")
|
console.log("Generating JSON Schema...")
|
||||||
|
|
||||||
const jsonSchema = z.toJSONSchema(OhMyOpenCodeConfigSchema, {
|
const jsonSchema = zodToJsonSchema(OhMyOpenCodeConfigSchema, {
|
||||||
io: "input",
|
target: "draft7",
|
||||||
target: "draft-7",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const finalSchema = {
|
const finalSchema = {
|
||||||
|
|||||||
@@ -1127,6 +1127,86 @@
|
|||||||
"created_at": "2026-02-02T16:58:50Z",
|
"created_at": "2026-02-02T16:58:50Z",
|
||||||
"repoId": 1108837393,
|
"repoId": 1108837393,
|
||||||
"pullRequestNo": 1399
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| Model | `anthropic/claude-opus-4-5` |
|
| Model | `anthropic/claude-opus-4-6` |
|
||||||
| Max Tokens | `64000` |
|
| Max Tokens | `64000` |
|
||||||
| Mode | `primary` |
|
| Mode | `primary` |
|
||||||
| Thinking | Budget: 32000 |
|
| Thinking | Budget: 32000 |
|
||||||
|
|||||||
@@ -13,36 +13,50 @@
|
|||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
```
|
```
|
||||||
agents/
|
agents/
|
||||||
├── atlas.ts # Master Orchestrator (holds todo list)
|
├── atlas/ # Master Orchestrator (holds todo list)
|
||||||
├── sisyphus.ts # Main prompt (SF Bay Area engineer identity)
|
│ ├── index.ts
|
||||||
├── hephaestus.ts # Autonomous Deep Worker (GPT 5.2 Codex, "The Legitimate Craftsman")
|
│ ├── default.ts # Claude-optimized prompt (390 lines)
|
||||||
├── sisyphus-junior.ts # Delegated task executor (category-spawned)
|
│ ├── gpt.ts # GPT-optimized prompt (330 lines)
|
||||||
|
│ └── utils.ts
|
||||||
|
├── prometheus/ # Planning Agent (Interview/Consultant mode)
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── plan-template.ts # Work plan structure (423 lines)
|
||||||
|
│ ├── interview-mode.ts # Interview flow (335 lines)
|
||||||
|
│ ├── plan-generation.ts
|
||||||
|
│ ├── high-accuracy-mode.ts
|
||||||
|
│ ├── identity-constraints.ts # Identity rules (301 lines)
|
||||||
|
│ └── behavioral-summary.ts
|
||||||
|
├── sisyphus-junior/ # Delegated task executor (category-spawned)
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── default.ts
|
||||||
|
│ └── gpt.ts
|
||||||
|
├── sisyphus.ts # Main orchestrator prompt (530 lines)
|
||||||
|
├── hephaestus.ts # Autonomous deep worker (618 lines, GPT 5.3 Codex)
|
||||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||||
├── librarian.ts # Multi-repo research (GitHub CLI, Context7)
|
├── librarian.ts # Multi-repo research (328 lines)
|
||||||
├── explore.ts # Fast contextual grep (Grok Code Fast)
|
├── explore.ts # Fast contextual grep
|
||||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||||
├── prometheus-prompt.ts # Planning (Interview/Consultant mode, 1283 lines)
|
├── metis.ts # Pre-planning analysis (347 lines)
|
||||||
├── metis.ts # Pre-planning analysis (Gap detection)
|
├── momus.ts # Plan reviewer
|
||||||
├── momus.ts # Plan reviewer (Ruthless fault-finding)
|
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation (431 lines)
|
||||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
|
|
||||||
├── types.ts # AgentModelConfig, AgentPromptMetadata
|
├── types.ts # AgentModelConfig, AgentPromptMetadata
|
||||||
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback()
|
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback() (485 lines)
|
||||||
└── index.ts # builtinAgents export
|
└── index.ts # builtinAgents export
|
||||||
```
|
```
|
||||||
|
|
||||||
## AGENT MODELS
|
## AGENT MODELS
|
||||||
| Agent | Model | Temp | Purpose |
|
| 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) |
|
| 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.2-codex | 0.1 | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.2-codex, no fallback) |
|
| Hephaestus | openai/gpt-5.3-codex | 0.1 | Autonomous deep worker, "The Legitimate Craftsman" (requires gpt-5.3-codex, no fallback) |
|
||||||
| Atlas | anthropic/claude-sonnet-4-5 | 0.1 | Master orchestrator (fallback: kimi-k2.5 → gpt-5.2) |
|
| 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 |
|
| 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) |
|
| 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) |
|
| 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 |
|
| 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) |
|
| Prometheus | anthropic/claude-opus-4-6 | 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) |
|
| 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-5) |
|
| 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 |
|
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
|
||||||
|
|
||||||
## HOW TO ADD
|
## HOW TO ADD
|
||||||
@@ -59,15 +73,17 @@ agents/
|
|||||||
| explore | write, edit, task, delegate_task, call_omo_agent |
|
| explore | write, edit, task, delegate_task, call_omo_agent |
|
||||||
| multimodal-looker | Allowlist: read only |
|
| multimodal-looker | Allowlist: read only |
|
||||||
| Sisyphus-Junior | task, delegate_task |
|
| Sisyphus-Junior | task, delegate_task |
|
||||||
|
| Atlas | task, call_omo_agent |
|
||||||
|
|
||||||
## PATTERNS
|
## PATTERNS
|
||||||
- **Factory**: `createXXXAgent(model: string): AgentConfig`
|
- **Factory**: `createXXXAgent(model: string): AgentConfig`
|
||||||
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers.
|
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers
|
||||||
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`.
|
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`
|
||||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas.
|
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas
|
||||||
|
- **Model-specific routing**: Atlas, Sisyphus-Junior have GPT vs Claude prompt variants
|
||||||
|
|
||||||
## ANTI-PATTERNS
|
## ANTI-PATTERNS
|
||||||
- **Trust reports**: NEVER trust "I'm done" - verify outputs.
|
- **Trust reports**: NEVER trust "I'm done" - verify outputs
|
||||||
- **High temp**: Don't use >0.3 for code agents.
|
- **High temp**: Don't use >0.3 for code agents
|
||||||
- **Sequential calls**: Use `delegate_task` with `run_in_background` for exploration.
|
- **Sequential calls**: Use `delegate_task` with `run_in_background` for exploration
|
||||||
- **Prometheus writing code**: Planner only - never implements.
|
- **Prometheus writing code**: Planner only - never implements
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CategoryConfig } from "../../config/schema"
|
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 { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
||||||
|
|
||||||
export const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
export const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
||||||
@@ -56,21 +56,48 @@ export function buildSkillsSection(skills: AvailableSkill[]): string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const skillRows = skills.map((s) => {
|
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||||
|
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||||
|
|
||||||
|
const builtinRows = builtinSkills.map((s) => {
|
||||||
const shortDesc = s.description.split(".")[0] || s.description
|
const shortDesc = s.description.split(".")[0] || s.description
|
||||||
return `| \`${s.name}\` | ${shortDesc} |`
|
return `| \`${s.name}\` | ${shortDesc} |`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const customRows = customSkills.map((s) => {
|
||||||
|
const shortDesc = s.description.split(".")[0] || s.description
|
||||||
|
const source = s.location === "project" ? "project" : "user"
|
||||||
|
return `| \`${s.name}\` | ${shortDesc} | ${source} |`
|
||||||
|
})
|
||||||
|
|
||||||
|
const 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 `
|
return `
|
||||||
#### 3.2.2: Skill Selection (PREPEND TO PROMPT)
|
#### 3.2.2: Skill Selection (PREPEND TO PROMPT)
|
||||||
|
|
||||||
**Skills are specialized instructions that guide subagent behavior. Consider them alongside category selection.**
|
**Skills are specialized instructions that guide subagent behavior. Consider them alongside category selection.**
|
||||||
|
|
||||||
| Skill | When to Use |
|
${skillsTable}
|
||||||
|-------|-------------|
|
|
||||||
${skillRows.join("\n")}
|
|
||||||
|
|
||||||
**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?"
|
Read each skill's description and ask: "Does this skill's domain overlap with my task?"
|
||||||
- If YES: INCLUDE in load_skills=[...]
|
- If YES: INCLUDE in load_skills=[...]
|
||||||
|
|||||||
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -20,6 +20,7 @@ export interface AvailableSkill {
|
|||||||
export interface AvailableCategory {
|
export interface AvailableCategory {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
|
model?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
||||||
@@ -166,6 +167,33 @@ export function buildDelegationTable(agents: AvailableAgent[]): string {
|
|||||||
return rows.join("\n")
|
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 {
|
export function buildCategorySkillsDelegationGuide(categories: AvailableCategory[], skills: AvailableSkill[]): string {
|
||||||
if (categories.length === 0 && skills.length === 0) return ""
|
if (categories.length === 0 && skills.length === 0) return ""
|
||||||
|
|
||||||
@@ -174,11 +202,44 @@ export function buildCategorySkillsDelegationGuide(categories: AvailableCategory
|
|||||||
return `| \`${c.name}\` | ${desc} |`
|
return `| \`${c.name}\` | ${desc} |`
|
||||||
})
|
})
|
||||||
|
|
||||||
const skillRows = skills.map((s) => {
|
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||||
|
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||||
|
|
||||||
|
const builtinRows = builtinSkills.map((s) => {
|
||||||
const desc = s.description.split(".")[0] || s.description
|
const desc = s.description.split(".")[0] || s.description
|
||||||
return `| \`${s.name}\` | ${desc} |`
|
return `| \`${s.name}\` | ${desc} |`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const customRows = customSkills.map((s) => {
|
||||||
|
const desc = s.description.split(".")[0] || s.description
|
||||||
|
const source = s.location === "project" ? "project" : "user"
|
||||||
|
return `| \`${s.name}\` | ${desc} | ${source} |`
|
||||||
|
})
|
||||||
|
|
||||||
|
const 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
|
return `### Category + Skills Delegation System
|
||||||
|
|
||||||
**delegate_task() combines categories and skills for optimal task execution.**
|
**delegate_task() combines categories and skills for optimal task execution.**
|
||||||
@@ -191,13 +252,7 @@ Each category is configured with a model optimized for that domain. Read the des
|
|||||||
|----------|-------------------|
|
|----------|-------------------|
|
||||||
${categoryRows.join("\n")}
|
${categoryRows.join("\n")}
|
||||||
|
|
||||||
#### Available Skills (Domain Expertise Injection)
|
${skillsSection}
|
||||||
|
|
||||||
Skills inject specialized instructions into the subagent. Read the description to understand when each skill applies.
|
|
||||||
|
|
||||||
| Skill | Expertise Domain |
|
|
||||||
|-------|------------------|
|
|
||||||
${skillRows.join("\n")}
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -208,12 +263,15 @@ ${skillRows.join("\n")}
|
|||||||
- Match task requirements to category domain
|
- Match task requirements to category domain
|
||||||
- Select the category whose domain BEST fits the task
|
- 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:
|
For EVERY skill listed above, ask yourself:
|
||||||
> "Does this skill's expertise domain overlap with my task?"
|
> "Does this skill's expertise domain overlap with my task?"
|
||||||
|
|
||||||
- If YES → INCLUDE in \`load_skills=[...]\`
|
- If YES → INCLUDE in \`load_skills=[...]\`
|
||||||
- If NO → You MUST justify why (see below)
|
- 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**
|
**STEP 3: Justify Omissions**
|
||||||
|
|
||||||
@@ -240,7 +298,7 @@ SKILL EVALUATION for "[skill-name]":
|
|||||||
\`\`\`typescript
|
\`\`\`typescript
|
||||||
delegate_task(
|
delegate_task(
|
||||||
category="[selected-category]",
|
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="..."
|
prompt="..."
|
||||||
)
|
)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -328,12 +386,26 @@ export function buildUltraworkSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (skills.length > 0) {
|
if (skills.length > 0) {
|
||||||
lines.push("**Skills** (combine with categories - EVALUATE ALL for relevance):")
|
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||||
for (const skill of skills) {
|
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||||
const shortDesc = skill.description.split(".")[0] || skill.description
|
|
||||||
lines.push(`- \`${skill.name}\`: ${shortDesc}`)
|
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) {
|
if (agents.length > 0) {
|
||||||
|
|||||||
@@ -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.
|
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)
|
## Hard Constraints (MUST READ FIRST - GPT 5.2 Constraint-First)
|
||||||
|
|
||||||
${hardBlocks}
|
${hardBlocks}
|
||||||
@@ -404,6 +417,13 @@ Only terminate your turn when you are SURE the problem is SOLVED.
|
|||||||
Autonomously resolve the query to the BEST of your ability.
|
Autonomously resolve the query to the BEST of your ability.
|
||||||
Do NOT guess. Do NOT ask unnecessary questions. Do NOT stop early.
|
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):**
|
**Completion Checklist (ALL must be true):**
|
||||||
1. User asked for X → X is FULLY implemented (not partial, not "basic version")
|
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)
|
2. X passes lsp_diagnostics (zero errors on ALL modified files)
|
||||||
@@ -459,9 +479,9 @@ Do NOT guess. Do NOT ask unnecessary questions. Do NOT stop early.
|
|||||||
- Each update must include concrete outcome ("Found X", "Updated Y")
|
- Each update must include concrete outcome ("Found X", "Updated Y")
|
||||||
|
|
||||||
**Scope:**
|
**Scope:**
|
||||||
- Implement EXACTLY what user requests
|
- Implement what user requests
|
||||||
- No extra features, no embellishments
|
- When blocked, autonomously try alternative approaches before asking
|
||||||
- Simplest valid interpretation for ambiguous instructions
|
- No unnecessary features, but solve blockers creatively
|
||||||
</output_contract>
|
</output_contract>
|
||||||
|
|
||||||
## Response Compaction (LONG CONTEXT HANDLING)
|
## Response Compaction (LONG CONTEXT HANDLING)
|
||||||
@@ -545,21 +565,27 @@ When working on long sessions or complex multi-file tasks:
|
|||||||
2. Re-verify after EVERY fix attempt
|
2. Re-verify after EVERY fix attempt
|
||||||
3. Never shotgun debug
|
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
|
1. **STOP** all edits
|
||||||
2. **REVERT** to last working state
|
2. **REVERT** to last working state
|
||||||
3. **DOCUMENT** what failed
|
3. **DOCUMENT** what you tried (all 3 approaches)
|
||||||
4. **CONSULT** Oracle with full context
|
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
|
**Never**: Leave code broken, delete failing tests, continue hoping
|
||||||
|
|
||||||
## Soft Guidelines
|
## Soft Guidelines
|
||||||
|
|
||||||
- Prefer existing libraries over new dependencies
|
- Prefer existing libraries over new dependencies
|
||||||
- Prefer small, focused changes over large refactors
|
- Prefer small, focused changes over large refactors`
|
||||||
- When uncertain about scope, ask`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHephaestusAgent(
|
export function createHephaestusAgent(
|
||||||
@@ -584,7 +610,7 @@ export function createHephaestusAgent(
|
|||||||
model,
|
model,
|
||||||
maxTokens: 32000,
|
maxTokens: 32000,
|
||||||
prompt,
|
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"],
|
permission: { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"],
|
||||||
reasoningEffort: "medium",
|
reasoningEffort: "medium",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import * as connectedProvidersCache from "../shared/connected-providers-cache"
|
|||||||
import * as modelAvailability from "../shared/model-availability"
|
import * as modelAvailability from "../shared/model-availability"
|
||||||
import * as shared from "../shared"
|
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", () => {
|
describe("createBuiltinAgents with model overrides", () => {
|
||||||
test("Sisyphus with default model has thinking config when all models available", async () => {
|
test("Sisyphus with default model has thinking config when all models available", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set([
|
new Set([
|
||||||
"anthropic/claude-opus-4-5",
|
"anthropic/claude-opus-4-6",
|
||||||
"kimi-for-coding/k2p5",
|
"kimi-for-coding/k2p5",
|
||||||
"opencode/kimi-k2.5-free",
|
"opencode/kimi-k2.5-free",
|
||||||
"zai-coding-plan/glm-4.7",
|
"zai-coding-plan/glm-4.7",
|
||||||
@@ -26,7 +26,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], {})
|
||||||
|
|
||||||
// #then
|
// #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.thinking).toEqual({ type: "enabled", budgetTokens: 32000 })
|
||||||
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
|
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
|
||||||
} finally {
|
} finally {
|
||||||
@@ -41,7 +41,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||||
@@ -81,7 +81,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
|
|
||||||
test("Sisyphus is created on first run when no availableModels or cache exist", async () => {
|
test("Sisyphus is created on first run when no availableModels or cache exist", async () => {
|
||||||
// #given
|
// #given
|
||||||
const systemDefaultModel = "anthropic/claude-opus-4-5"
|
const systemDefaultModel = "anthropic/claude-opus-4-6"
|
||||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus).toBeDefined()
|
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 {
|
} finally {
|
||||||
cacheSpy.mockRestore()
|
cacheSpy.mockRestore()
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
@@ -103,7 +103,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
||||||
|
|
||||||
// #when
|
// #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)
|
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
|
||||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||||
@@ -132,7 +132,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||||
@@ -148,7 +148,7 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4")
|
expect(agents.oracle.model).toBe("anthropic/claude-sonnet-4")
|
||||||
@@ -164,12 +164,25 @@ describe("createBuiltinAgents with model overrides", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, undefined, undefined, [], undefined, undefined)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
expect(agents.sisyphus.model).toBe("github-copilot/gpt-5.2")
|
||||||
expect(agents.sisyphus.temperature).toBe(0.5)
|
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")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createBuiltinAgents without systemDefaultModel", () => {
|
describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||||
@@ -205,7 +218,7 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
|||||||
])
|
])
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set([
|
new Set([
|
||||||
"anthropic/claude-opus-4-5",
|
"anthropic/claude-opus-4-6",
|
||||||
"kimi-for-coding/k2p5",
|
"kimi-for-coding/k2p5",
|
||||||
"opencode/kimi-k2.5-free",
|
"opencode/kimi-k2.5-free",
|
||||||
"zai-coding-plan/glm-4.7",
|
"zai-coding-plan/glm-4.7",
|
||||||
@@ -219,7 +232,7 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
|||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus).toBeDefined()
|
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 {
|
} finally {
|
||||||
cacheSpy.mockRestore()
|
cacheSpy.mockRestore()
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
@@ -227,12 +240,13 @@ describe("createBuiltinAgents without systemDefaultModel", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("createBuiltinAgents with requiresModel gating", () => {
|
describe("createBuiltinAgents with requiresProvider gating (hephaestus)", () => {
|
||||||
test("hephaestus is not created when gpt-5.2-codex is unavailable", async () => {
|
test("hephaestus is not created when no required provider is connected", async () => {
|
||||||
// #given
|
// #given - only anthropic models available, not in hephaestus requiresProvider
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
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 {
|
try {
|
||||||
// #when
|
// #when
|
||||||
@@ -242,13 +256,48 @@ describe("createBuiltinAgents with requiresModel gating", () => {
|
|||||||
expect(agents.hephaestus).toBeUndefined()
|
expect(agents.hephaestus).toBeUndefined()
|
||||||
} finally {
|
} finally {
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
|
cacheSpy.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("hephaestus is created when gpt-5.2-codex is available", async () => {
|
test("hephaestus is created when openai provider is connected", async () => {
|
||||||
// #given
|
// #given - openai provider has models available
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
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 {
|
try {
|
||||||
@@ -273,20 +322,20 @@ describe("createBuiltinAgents with requiresModel gating", () => {
|
|||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.hephaestus).toBeDefined()
|
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 {
|
} finally {
|
||||||
cacheSpy.mockRestore()
|
cacheSpy.mockRestore()
|
||||||
fetchSpy.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
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-5"])
|
new Set(["anthropic/claude-opus-4-6"])
|
||||||
)
|
)
|
||||||
const overrides = {
|
const overrides = {
|
||||||
hephaestus: { model: "anthropic/claude-opus-4-5" },
|
hephaestus: { model: "anthropic/claude-opus-4-6" },
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -305,7 +354,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
|||||||
test("sisyphus is created when at least one fallback model is available", async () => {
|
test("sisyphus is created when at least one fallback model is available", async () => {
|
||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["anthropic/claude-opus-4-5"])
|
new Set(["anthropic/claude-opus-4-6"])
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -330,7 +379,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
|||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(agents.sisyphus).toBeDefined()
|
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 {
|
} finally {
|
||||||
cacheSpy.mockRestore()
|
cacheSpy.mockRestore()
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
@@ -341,7 +390,7 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
|||||||
// #given
|
// #given
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(new Set())
|
||||||
const overrides = {
|
const overrides = {
|
||||||
sisyphus: { model: "anthropic/claude-opus-4-5" },
|
sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -355,11 +404,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
|
// #given - only openai/gpt-5.2 available, not in sisyphus fallback chain
|
||||||
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
const fetchSpy = spyOn(shared, "fetchAvailableModels").mockResolvedValue(
|
||||||
new Set(["openai/gpt-5.2"])
|
new Set(["openai/gpt-5.2"])
|
||||||
)
|
)
|
||||||
|
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue([])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// #when
|
// #when
|
||||||
@@ -369,13 +419,14 @@ describe("createBuiltinAgents with requiresAnyModel gating (sisyphus)", () => {
|
|||||||
expect(agents.sisyphus).toBeUndefined()
|
expect(agents.sisyphus).toBeUndefined()
|
||||||
} finally {
|
} finally {
|
||||||
fetchSpy.mockRestore()
|
fetchSpy.mockRestore()
|
||||||
|
cacheSpy.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("buildAgent with category and skills", () => {
|
describe("buildAgent with category and skills", () => {
|
||||||
const { buildAgent } = require("./utils")
|
const { buildAgent } = require("./utils")
|
||||||
const TEST_MODEL = "anthropic/claude-opus-4-5"
|
const TEST_MODEL = "anthropic/claude-opus-4-6"
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clearSkillCache()
|
clearSkillCache()
|
||||||
@@ -521,7 +572,7 @@ describe("buildAgent with category and skills", () => {
|
|||||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||||
|
|
||||||
// #then - category's built-in model and skills are applied
|
// #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.variant).toBe("xhigh")
|
||||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||||
expect(agent.prompt).toContain("Task description")
|
expect(agent.prompt).toContain("Task description")
|
||||||
@@ -634,9 +685,9 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
|||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
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).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")
|
expect(agents.oracle.variant).toBe("xhigh")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -703,9 +754,9 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
|||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
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).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")
|
expect(agents.sisyphus.variant).toBe("xhigh")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -718,9 +769,9 @@ describe("override.category expansion in createBuiltinAgents", () => {
|
|||||||
// #when
|
// #when
|
||||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
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).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")
|
expect(agents.atlas.variant).toBe("xhigh")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -740,6 +791,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", () => {
|
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
|
||||||
test("createBuiltinAgents should call fetchAvailableModels with undefined client to prevent deadlock", async () => {
|
test("createBuiltinAgents should call fetchAvailableModels with undefined client to prevent deadlock", async () => {
|
||||||
// #given - This test ensures we don't regress on issue #1301
|
// #given - This test ensures we don't regress on issue #1301
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createAtlasAgent, atlasPromptMetadata } from "./atlas"
|
|||||||
import { createMomusAgent, momusPromptMetadata } from "./momus"
|
import { createMomusAgent, momusPromptMetadata } from "./momus"
|
||||||
import { createHephaestusAgent } from "./hephaestus"
|
import { createHephaestusAgent } from "./hephaestus"
|
||||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||||
import { deepMerge, fetchAvailableModels, resolveModelPipeline, AGENT_MODEL_REQUIREMENTS, readConnectedProvidersCache, isModelAvailable, isAnyFallbackModelAvailable } from "../shared"
|
import { deepMerge, fetchAvailableModels, resolveModelPipeline, AGENT_MODEL_REQUIREMENTS, readConnectedProvidersCache, isModelAvailable, isAnyFallbackModelAvailable, isAnyProviderConnected, migrateAgentConfig } from "../shared"
|
||||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||||
import { createBuiltinSkills } from "../features/builtin-skills"
|
import { createBuiltinSkills } from "../features/builtin-skills"
|
||||||
@@ -57,7 +57,8 @@ export function buildAgent(
|
|||||||
model: string,
|
model: string,
|
||||||
categories?: CategoriesConfig,
|
categories?: CategoriesConfig,
|
||||||
gitMasterConfig?: GitMasterConfig,
|
gitMasterConfig?: GitMasterConfig,
|
||||||
browserProvider?: BrowserAutomationProvider
|
browserProvider?: BrowserAutomationProvider,
|
||||||
|
disabledSkills?: Set<string>
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
const base = isFactory(source) ? source(model) : source
|
const base = isFactory(source) ? source(model) : source
|
||||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
const categoryConfigs: Record<string, CategoryConfig> = categories
|
||||||
@@ -81,7 +82,7 @@ export function buildAgent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (agentWithCategory.skills?.length) {
|
if (agentWithCategory.skills?.length) {
|
||||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider })
|
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider, disabledSkills })
|
||||||
if (resolved.size > 0) {
|
if (resolved.size > 0) {
|
||||||
const skillContent = Array.from(resolved.values()).join("\n\n")
|
const skillContent = Array.from(resolved.values()).join("\n\n")
|
||||||
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
||||||
@@ -207,7 +208,8 @@ function mergeAgentConfig(
|
|||||||
base: AgentConfig,
|
base: AgentConfig,
|
||||||
override: AgentOverrideConfig
|
override: AgentOverrideConfig
|
||||||
): AgentConfig {
|
): AgentConfig {
|
||||||
const { prompt_append, ...rest } = override
|
const migratedOverride = migrateAgentConfig(override as Record<string, unknown>) as AgentOverrideConfig
|
||||||
|
const { prompt_append, ...rest } = migratedOverride
|
||||||
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
const merged = deepMerge(base, rest as Partial<AgentConfig>)
|
||||||
|
|
||||||
if (prompt_append && merged.prompt) {
|
if (prompt_append && merged.prompt) {
|
||||||
@@ -233,7 +235,8 @@ export async function createBuiltinAgents(
|
|||||||
discoveredSkills: LoadedSkill[] = [],
|
discoveredSkills: LoadedSkill[] = [],
|
||||||
client?: any,
|
client?: any,
|
||||||
browserProvider?: BrowserAutomationProvider,
|
browserProvider?: BrowserAutomationProvider,
|
||||||
uiSelectedModel?: string
|
uiSelectedModel?: string,
|
||||||
|
disabledSkills?: Set<string>
|
||||||
): Promise<Record<string, AgentConfig>> {
|
): Promise<Record<string, AgentConfig>> {
|
||||||
const connectedProviders = readConnectedProvidersCache()
|
const connectedProviders = readConnectedProvidersCache()
|
||||||
// IMPORTANT: Do NOT pass client to fetchAvailableModels during plugin initialization.
|
// IMPORTANT: Do NOT pass client to fetchAvailableModels during plugin initialization.
|
||||||
@@ -257,7 +260,7 @@ export async function createBuiltinAgents(
|
|||||||
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
|
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const builtinSkills = createBuiltinSkills({ browserProvider })
|
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills })
|
||||||
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
|
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
|
||||||
|
|
||||||
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
|
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
|
||||||
@@ -290,16 +293,16 @@ export async function createBuiltinAgents(
|
|||||||
const override = agentOverrides[agentName]
|
const override = agentOverrides[agentName]
|
||||||
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||||
|
|
||||||
// Check if agent requires a specific model
|
// Check if agent requires a specific model
|
||||||
if (requirement?.requiresModel && availableModels) {
|
if (requirement?.requiresModel && availableModels) {
|
||||||
if (!isModelAvailable(requirement.requiresModel, availableModels)) {
|
if (!isModelAvailable(requirement.requiresModel, availableModels)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
|
const isPrimaryAgent = isFactory(source) && source.mode === "primary"
|
||||||
|
|
||||||
const resolution = applyModelResolution({
|
const resolution = applyModelResolution({
|
||||||
uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined,
|
uiSelectedModel: isPrimaryAgent ? uiSelectedModel : undefined,
|
||||||
userModel: override?.model,
|
userModel: override?.model,
|
||||||
@@ -310,7 +313,7 @@ export async function createBuiltinAgents(
|
|||||||
if (!resolution) continue
|
if (!resolution) continue
|
||||||
const { model, variant: resolvedVariant } = resolution
|
const { model, variant: resolvedVariant } = resolution
|
||||||
|
|
||||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider)
|
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider, disabledSkills)
|
||||||
|
|
||||||
// Apply resolved variant from model fallback chain
|
// Apply resolved variant from model fallback chain
|
||||||
if (resolvedVariant) {
|
if (resolvedVariant) {
|
||||||
@@ -374,7 +377,7 @@ export async function createBuiltinAgents(
|
|||||||
availableSkills,
|
availableSkills,
|
||||||
availableCategories
|
availableCategories
|
||||||
)
|
)
|
||||||
|
|
||||||
if (sisyphusResolvedVariant) {
|
if (sisyphusResolvedVariant) {
|
||||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||||
}
|
}
|
||||||
@@ -391,13 +394,13 @@ export async function createBuiltinAgents(
|
|||||||
const hephaestusRequirement = AGENT_MODEL_REQUIREMENTS["hephaestus"]
|
const hephaestusRequirement = AGENT_MODEL_REQUIREMENTS["hephaestus"]
|
||||||
const hasHephaestusExplicitConfig = hephaestusOverride !== undefined
|
const hasHephaestusExplicitConfig = hephaestusOverride !== undefined
|
||||||
|
|
||||||
const hasRequiredModel =
|
const hasRequiredProvider =
|
||||||
!hephaestusRequirement?.requiresModel ||
|
!hephaestusRequirement?.requiresProvider ||
|
||||||
hasHephaestusExplicitConfig ||
|
hasHephaestusExplicitConfig ||
|
||||||
isFirstRunNoCache ||
|
isFirstRunNoCache ||
|
||||||
(availableModels.size > 0 && isModelAvailable(hephaestusRequirement.requiresModel, availableModels))
|
isAnyProviderConnected(hephaestusRequirement.requiresProvider, availableModels)
|
||||||
|
|
||||||
if (hasRequiredModel) {
|
if (hasRequiredProvider) {
|
||||||
let hephaestusResolution = applyModelResolution({
|
let hephaestusResolution = applyModelResolution({
|
||||||
userModel: hephaestusOverride?.model,
|
userModel: hephaestusOverride?.model,
|
||||||
requirement: hephaestusRequirement,
|
requirement: hephaestusRequirement,
|
||||||
@@ -419,7 +422,7 @@ export async function createBuiltinAgents(
|
|||||||
availableSkills,
|
availableSkills,
|
||||||
availableCategories
|
availableCategories
|
||||||
)
|
)
|
||||||
|
|
||||||
hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" }
|
hephaestusConfig = { ...hephaestusConfig, variant: hephaestusResolvedVariant ?? "medium" }
|
||||||
|
|
||||||
const hepOverrideCategory = (hephaestusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
const hepOverrideCategory = (hephaestusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||||
@@ -467,7 +470,7 @@ export async function createBuiltinAgents(
|
|||||||
availableSkills,
|
availableSkills,
|
||||||
userCategories: categories,
|
userCategories: categories,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (atlasResolvedVariant) {
|
if (atlasResolvedVariant) {
|
||||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
|
|
||||||
CLI entry: `bunx oh-my-opencode`. 4 commands with Commander.js + @clack/prompts TUI.
|
CLI entry: `bunx oh-my-opencode`. 5 commands with Commander.js + @clack/prompts TUI.
|
||||||
|
|
||||||
**Commands**: install (interactive setup), doctor (14 health checks), run (session launcher), get-local-version
|
**Commands**: install (interactive setup), doctor (14 health checks), run (session launcher), get-local-version, mcp-oauth
|
||||||
|
|
||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
|
|
||||||
```
|
```
|
||||||
cli/
|
cli/
|
||||||
├── index.ts # Commander.js entry (4 commands)
|
├── index.ts # Commander.js entry (5 commands)
|
||||||
├── install.ts # Interactive TUI (542 lines)
|
├── install.ts # Interactive TUI (542 lines)
|
||||||
├── config-manager.ts # JSONC parsing (667 lines)
|
├── config-manager.ts # JSONC parsing (667 lines)
|
||||||
├── types.ts # InstallArgs, InstallConfig
|
|
||||||
├── model-fallback.ts # Model fallback configuration
|
├── model-fallback.ts # Model fallback configuration
|
||||||
|
├── types.ts # InstallArgs, InstallConfig
|
||||||
├── doctor/
|
├── doctor/
|
||||||
│ ├── index.ts # Doctor entry
|
│ ├── index.ts # Doctor entry
|
||||||
│ ├── runner.ts # Check orchestration
|
│ ├── runner.ts # Check orchestration
|
||||||
│ ├── formatter.ts # Colored output
|
│ ├── formatter.ts # Colored output
|
||||||
│ ├── constants.ts # Check IDs, symbols
|
│ ├── constants.ts # Check IDs, symbols
|
||||||
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
|
│ ├── types.ts # CheckResult, CheckDefinition
|
||||||
│ └── checks/ # 14 checks, 23 files
|
│ └── checks/ # 14 checks, 23 files
|
||||||
│ ├── version.ts # OpenCode + plugin version
|
│ ├── version.ts # OpenCode + plugin version
|
||||||
│ ├── config.ts # JSONC validity, Zod
|
│ ├── config.ts # JSONC validity, Zod
|
||||||
@@ -28,10 +28,11 @@ cli/
|
|||||||
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
||||||
│ ├── lsp.ts # LSP connectivity
|
│ ├── lsp.ts # LSP connectivity
|
||||||
│ ├── mcp.ts # MCP validation
|
│ ├── mcp.ts # MCP validation
|
||||||
│ ├── model-resolution.ts # Model resolution check
|
│ ├── model-resolution.ts # Model resolution check (323 lines)
|
||||||
│ └── gh.ts # GitHub CLI
|
│ └── gh.ts # GitHub CLI
|
||||||
├── run/
|
├── run/
|
||||||
│ └── index.ts # Session launcher
|
│ ├── index.ts # Session launcher
|
||||||
|
│ └── events.ts # CLI run events (325 lines)
|
||||||
├── mcp-oauth/
|
├── mcp-oauth/
|
||||||
│ └── index.ts # MCP OAuth flow
|
│ └── index.ts # MCP OAuth flow
|
||||||
└── get-local-version/
|
└── get-local-version/
|
||||||
@@ -46,6 +47,7 @@ cli/
|
|||||||
| `doctor` | 14 health checks for diagnostics |
|
| `doctor` | 14 health checks for diagnostics |
|
||||||
| `run` | Launch session with todo enforcement |
|
| `run` | Launch session with todo enforcement |
|
||||||
| `get-local-version` | Version detection and update check |
|
| `get-local-version` | Version detection and update check |
|
||||||
|
| `mcp-oauth` | MCP OAuth authentication flow |
|
||||||
|
|
||||||
## DOCTOR CATEGORIES (14 Checks)
|
## DOCTOR CATEGORIES (14 Checks)
|
||||||
|
|
||||||
|
|||||||
@@ -75,26 +75,26 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
|||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"oracle": {
|
"oracle": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -113,7 +113,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
|||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"visual-engineering": {
|
"visual-engineering": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"writing": {
|
"writing": {
|
||||||
@@ -137,26 +137,26 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
|||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"oracle": {
|
"oracle": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -165,18 +165,18 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"visual-engineering": {
|
"visual-engineering": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"writing": {
|
"writing": {
|
||||||
@@ -197,7 +197,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
|||||||
"model": "opencode/gpt-5-nano",
|
"model": "opencode/gpt-5-nano",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
@@ -225,22 +225,22 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "opencode/glm-4.7-free",
|
"model": "opencode/glm-4.7-free",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"visual-engineering": {
|
"visual-engineering": {
|
||||||
@@ -264,7 +264,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
|||||||
"model": "opencode/gpt-5-nano",
|
"model": "opencode/gpt-5-nano",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
@@ -292,14 +292,14 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
|||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "opencode/glm-4.7-free",
|
"model": "opencode/glm-4.7-free",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -307,7 +307,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"visual-engineering": {
|
"visual-engineering": {
|
||||||
@@ -335,18 +335,18 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
|||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
"model": "google/gemini-3-flash",
|
"model": "google/gemini-3-flash",
|
||||||
},
|
},
|
||||||
"oracle": {
|
"oracle": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
@@ -355,14 +355,14 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
|||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "google/gemini-3-flash",
|
"model": "google/gemini-3-flash",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "google/gemini-3-flash",
|
"model": "google/gemini-3-flash",
|
||||||
@@ -395,18 +395,18 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
|||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
"model": "google/gemini-3-flash",
|
"model": "google/gemini-3-flash",
|
||||||
},
|
},
|
||||||
"oracle": {
|
"oracle": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
@@ -415,14 +415,14 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
|||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "google/gemini-3-flash",
|
"model": "google/gemini-3-flash",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "google/gemini-3-pro",
|
"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",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -473,28 +473,28 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -524,14 +524,14 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -546,32 +546,32 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
@@ -598,14 +598,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
|||||||
"model": "opencode/claude-haiku-4-5",
|
"model": "opencode/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "opencode/glm-4.7-free",
|
"model": "opencode/glm-4.7-free",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -620,28 +620,28 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "opencode/gemini-3-pro",
|
"model": "opencode/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "opencode/claude-haiku-4-5",
|
"model": "opencode/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -671,14 +671,14 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
|||||||
"model": "opencode/claude-haiku-4-5",
|
"model": "opencode/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "opencode/glm-4.7-free",
|
"model": "opencode/glm-4.7-free",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -693,32 +693,32 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "opencode/gemini-3-pro",
|
"model": "opencode/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "opencode/claude-haiku-4-5",
|
"model": "opencode/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "opencode/claude-opus-4-5",
|
"model": "opencode/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
@@ -745,14 +745,14 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
|||||||
"model": "github-copilot/gpt-5-mini",
|
"model": "github-copilot/gpt-5-mini",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "github-copilot/claude-sonnet-4.5",
|
"model": "github-copilot/claude-sonnet-4.5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -767,28 +767,28 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "github-copilot/gemini-3-pro-preview",
|
"model": "github-copilot/gemini-3-pro-preview",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "github-copilot/claude-haiku-4.5",
|
"model": "github-copilot/claude-haiku-4.5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -818,14 +818,14 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
|||||||
"model": "github-copilot/gpt-5-mini",
|
"model": "github-copilot/gpt-5-mini",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "github-copilot/claude-sonnet-4.5",
|
"model": "github-copilot/claude-sonnet-4.5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -840,32 +840,32 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "github-copilot/gemini-3-pro-preview",
|
"model": "github-copilot/gemini-3-pro-preview",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "github-copilot/claude-haiku-4.5",
|
"model": "github-copilot/claude-haiku-4.5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
@@ -1002,14 +1002,14 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "opencode/glm-4.7-free",
|
"model": "opencode/glm-4.7-free",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -1024,28 +1024,28 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "opencode/gemini-3-pro",
|
"model": "opencode/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "opencode/gpt-5.2-codex",
|
"model": "opencode/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -1075,14 +1075,14 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
|||||||
"model": "github-copilot/gpt-5-mini",
|
"model": "github-copilot/gpt-5-mini",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "github-copilot/claude-sonnet-4.5",
|
"model": "github-copilot/claude-sonnet-4.5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -1097,28 +1097,28 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "github-copilot/gemini-3-pro-preview",
|
"model": "github-copilot/gemini-3-pro-preview",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "github-copilot/claude-haiku-4.5",
|
"model": "github-copilot/claude-haiku-4.5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -1151,26 +1151,26 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
|||||||
"model": "zai-coding-plan/glm-4.7",
|
"model": "zai-coding-plan/glm-4.7",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
"model": "zai-coding-plan/glm-4.6v",
|
"model": "zai-coding-plan/glm-4.6v",
|
||||||
},
|
},
|
||||||
"oracle": {
|
"oracle": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1179,7 +1179,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -1189,7 +1189,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
|||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"visual-engineering": {
|
"visual-engineering": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"writing": {
|
"writing": {
|
||||||
@@ -1213,11 +1213,11 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
|||||||
"model": "anthropic/claude-sonnet-4-5",
|
"model": "anthropic/claude-sonnet-4-5",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"multimodal-looker": {
|
"multimodal-looker": {
|
||||||
@@ -1225,28 +1225,28 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
|||||||
},
|
},
|
||||||
"oracle": {
|
"oracle": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "anthropic/claude-sonnet-4-5",
|
"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",
|
"model": "opencode/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "zai-coding-plan/glm-4.7",
|
"model": "zai-coding-plan/glm-4.7",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -1297,28 +1297,28 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "github-copilot/claude-opus-4.5",
|
"model": "github-copilot/claude-opus-4.6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "github-copilot/gemini-3-pro-preview",
|
"model": "github-copilot/gemini-3-pro-preview",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "github-copilot/claude-haiku-4.5",
|
"model": "github-copilot/claude-haiku-4.5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "github-copilot/gpt-5.2-codex",
|
"model": "github-copilot/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -1348,14 +1348,14 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "zai-coding-plan/glm-4.7",
|
"model": "zai-coding-plan/glm-4.7",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -1370,28 +1370,28 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
@@ -1421,14 +1421,14 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
|||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"hephaestus": {
|
"hephaestus": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"librarian": {
|
"librarian": {
|
||||||
"model": "zai-coding-plan/glm-4.7",
|
"model": "zai-coding-plan/glm-4.7",
|
||||||
},
|
},
|
||||||
"metis": {
|
"metis": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"momus": {
|
"momus": {
|
||||||
@@ -1443,32 +1443,32 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
|||||||
"variant": "high",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"sisyphus": {
|
"sisyphus": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"artistry": {
|
"artistry": {
|
||||||
"model": "google/gemini-3-pro",
|
"model": "google/gemini-3-pro",
|
||||||
"variant": "max",
|
"variant": "high",
|
||||||
},
|
},
|
||||||
"deep": {
|
"deep": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "medium",
|
"variant": "medium",
|
||||||
},
|
},
|
||||||
"quick": {
|
"quick": {
|
||||||
"model": "anthropic/claude-haiku-4-5",
|
"model": "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
"ultrabrain": {
|
"ultrabrain": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openai/gpt-5.3-codex",
|
||||||
"variant": "xhigh",
|
"variant": "xhigh",
|
||||||
},
|
},
|
||||||
"unspecified-high": {
|
"unspecified-high": {
|
||||||
"model": "anthropic/claude-opus-4-5",
|
"model": "anthropic/claude-opus-4-6",
|
||||||
"variant": "max",
|
"variant": "max",
|
||||||
},
|
},
|
||||||
"unspecified-low": {
|
"unspecified-low": {
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
|||||||
// #then Sisyphus uses Claude (OR logic - at least one provider available)
|
// #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.$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).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", () => {
|
test("generates native opus models when Claude max20 subscription", () => {
|
||||||
@@ -279,7 +279,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
|||||||
const result = generateOmoConfig(config)
|
const result = generateOmoConfig(config)
|
||||||
|
|
||||||
// #then Sisyphus uses Claude (OR logic - at least one provider available)
|
// #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", () => {
|
test("uses github-copilot sonnet fallback when only copilot available", () => {
|
||||||
@@ -298,8 +298,8 @@ describe("generateOmoConfig - model fallback system", () => {
|
|||||||
// #when generating config
|
// #when generating config
|
||||||
const result = generateOmoConfig(config)
|
const result = generateOmoConfig(config)
|
||||||
|
|
||||||
// #then Sisyphus uses Copilot (OR logic - copilot is in claude-opus-4-5 providers)
|
// #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.5")
|
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", () => {
|
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
|
// #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")
|
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
|
||||||
// #then Sisyphus uses Claude (OR logic)
|
// #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", () => {
|
test("uses native OpenAI models when only ChatGPT available", () => {
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ describe("model-resolution check", () => {
|
|||||||
// then: Should have agent entries
|
// then: Should have agent entries
|
||||||
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
|
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
|
||||||
expect(sisyphus).toBeDefined()
|
expect(sisyphus).toBeDefined()
|
||||||
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-5")
|
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-6")
|
||||||
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("anthropic")
|
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("anthropic")
|
||||||
expect(sisyphus!.requirement.fallbackChain[0]?.providers).toContain("github-copilot")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns category requirements with provider chains", async () => {
|
it("returns category requirements with provider chains", async () => {
|
||||||
@@ -43,7 +42,7 @@ describe("model-resolution check", () => {
|
|||||||
// given: User has override for oracle agent
|
// given: User has override for oracle agent
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
oracle: { model: "anthropic/claude-opus-4-5" },
|
oracle: { model: "anthropic/claude-opus-4-6" },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +51,8 @@ describe("model-resolution check", () => {
|
|||||||
// then: Oracle should show the override
|
// then: Oracle should show the override
|
||||||
const oracle = info.agents.find((a) => a.name === "oracle")
|
const oracle = info.agents.find((a) => a.name === "oracle")
|
||||||
expect(oracle).toBeDefined()
|
expect(oracle).toBeDefined()
|
||||||
expect(oracle!.userOverride).toBe("anthropic/claude-opus-4-5")
|
expect(oracle!.userOverride).toBe("anthropic/claude-opus-4-6")
|
||||||
expect(oracle!.effectiveResolution).toBe("User override: anthropic/claude-opus-4-5")
|
expect(oracle!.effectiveResolution).toBe("User override: anthropic/claude-opus-4-6")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("shows user override for category when configured", async () => {
|
it("shows user override for category when configured", async () => {
|
||||||
@@ -90,6 +89,46 @@ describe("model-resolution check", () => {
|
|||||||
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
|
expect(sisyphus!.effectiveResolution).toContain("Provider fallback:")
|
||||||
expect(sisyphus!.effectiveResolution).toContain("anthropic")
|
expect(sisyphus!.effectiveResolution).toContain("anthropic")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("captures user variant for agent when configured", async () => {
|
||||||
|
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
|
||||||
|
|
||||||
|
//#given User has model with variant override for oracle agent
|
||||||
|
const mockConfig = {
|
||||||
|
agents: {
|
||||||
|
oracle: { model: "openai/gpt-5.2", variant: "xhigh" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when getting resolution info with config
|
||||||
|
const info = getModelResolutionInfoWithOverrides(mockConfig)
|
||||||
|
|
||||||
|
//#then Oracle should have userVariant set
|
||||||
|
const oracle = info.agents.find((a) => a.name === "oracle")
|
||||||
|
expect(oracle).toBeDefined()
|
||||||
|
expect(oracle!.userOverride).toBe("openai/gpt-5.2")
|
||||||
|
expect(oracle!.userVariant).toBe("xhigh")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("captures user variant for category when configured", async () => {
|
||||||
|
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
|
||||||
|
|
||||||
|
//#given User has model with variant override for visual-engineering category
|
||||||
|
const mockConfig = {
|
||||||
|
categories: {
|
||||||
|
"visual-engineering": { model: "google/gemini-3-flash-preview", variant: "high" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when getting resolution info with config
|
||||||
|
const info = getModelResolutionInfoWithOverrides(mockConfig)
|
||||||
|
|
||||||
|
//#then visual-engineering should have userVariant set
|
||||||
|
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||||
|
expect(visual).toBeDefined()
|
||||||
|
expect(visual!.userOverride).toBe("google/gemini-3-flash-preview")
|
||||||
|
expect(visual!.userVariant).toBe("high")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("checkModelResolution", () => {
|
describe("checkModelResolution", () => {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export interface AgentResolutionInfo {
|
|||||||
name: string
|
name: string
|
||||||
requirement: ModelRequirement
|
requirement: ModelRequirement
|
||||||
userOverride?: string
|
userOverride?: string
|
||||||
|
userVariant?: string
|
||||||
effectiveModel: string
|
effectiveModel: string
|
||||||
effectiveResolution: string
|
effectiveResolution: string
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ export interface CategoryResolutionInfo {
|
|||||||
name: string
|
name: string
|
||||||
requirement: ModelRequirement
|
requirement: ModelRequirement
|
||||||
userOverride?: string
|
userOverride?: string
|
||||||
|
userVariant?: string
|
||||||
effectiveModel: string
|
effectiveModel: string
|
||||||
effectiveResolution: string
|
effectiveResolution: string
|
||||||
}
|
}
|
||||||
@@ -69,8 +71,8 @@ export interface ModelResolutionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OmoConfig {
|
interface OmoConfig {
|
||||||
agents?: Record<string, { model?: string }>
|
agents?: Record<string, { model?: string; variant?: string; category?: string }>
|
||||||
categories?: Record<string, { model?: string }>
|
categories?: Record<string, { model?: string; variant?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig(): OmoConfig | null {
|
function loadConfig(): OmoConfig | null {
|
||||||
@@ -152,10 +154,12 @@ export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelRes
|
|||||||
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
|
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
|
||||||
([name, requirement]) => {
|
([name, requirement]) => {
|
||||||
const userOverride = config.agents?.[name]?.model
|
const userOverride = config.agents?.[name]?.model
|
||||||
|
const userVariant = config.agents?.[name]?.variant
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
requirement,
|
requirement,
|
||||||
userOverride,
|
userOverride,
|
||||||
|
userVariant,
|
||||||
effectiveModel: getEffectiveModel(requirement, userOverride),
|
effectiveModel: getEffectiveModel(requirement, userOverride),
|
||||||
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
|
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
|
||||||
}
|
}
|
||||||
@@ -165,10 +169,12 @@ export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelRes
|
|||||||
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
|
const categories: CategoryResolutionInfo[] = Object.entries(CATEGORY_MODEL_REQUIREMENTS).map(
|
||||||
([name, requirement]) => {
|
([name, requirement]) => {
|
||||||
const userOverride = config.categories?.[name]?.model
|
const userOverride = config.categories?.[name]?.model
|
||||||
|
const userVariant = config.categories?.[name]?.variant
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
requirement,
|
requirement,
|
||||||
userOverride,
|
userOverride,
|
||||||
|
userVariant,
|
||||||
effectiveModel: getEffectiveModel(requirement, userOverride),
|
effectiveModel: getEffectiveModel(requirement, userOverride),
|
||||||
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
|
effectiveResolution: buildEffectiveResolution(requirement, userOverride),
|
||||||
}
|
}
|
||||||
@@ -182,7 +188,44 @@ function formatModelWithVariant(model: string, variant?: string): string {
|
|||||||
return variant ? `${model} (${variant})` : model
|
return variant ? `${model} (${variant})` : model
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveVariant(requirement: ModelRequirement): string | undefined {
|
function getAgentOverride(
|
||||||
|
agentName: string,
|
||||||
|
config: OmoConfig,
|
||||||
|
): { variant?: string; category?: string } | undefined {
|
||||||
|
const agentOverrides = config.agents
|
||||||
|
if (!agentOverrides) return undefined
|
||||||
|
|
||||||
|
// Direct lookup first, then case-insensitive lookup (matches agent-variant.ts)
|
||||||
|
return (
|
||||||
|
agentOverrides[agentName] ??
|
||||||
|
Object.entries(agentOverrides).find(
|
||||||
|
([key]) => key.toLowerCase() === agentName.toLowerCase()
|
||||||
|
)?.[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveVariant(
|
||||||
|
name: string,
|
||||||
|
requirement: ModelRequirement,
|
||||||
|
config: OmoConfig,
|
||||||
|
): string | undefined {
|
||||||
|
const agentOverride = getAgentOverride(name, config)
|
||||||
|
|
||||||
|
// Priority 1: Agent's direct variant override
|
||||||
|
if (agentOverride?.variant) {
|
||||||
|
return agentOverride.variant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Agent's category -> category's variant (matches agent-variant.ts)
|
||||||
|
const categoryName = agentOverride?.category
|
||||||
|
if (categoryName) {
|
||||||
|
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||||
|
if (categoryVariant) {
|
||||||
|
return categoryVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Fall back to requirement's fallback chain
|
||||||
const firstEntry = requirement.fallbackChain[0]
|
const firstEntry = requirement.fallbackChain[0]
|
||||||
return firstEntry?.variant ?? requirement.variant
|
return firstEntry?.variant ?? requirement.variant
|
||||||
}
|
}
|
||||||
@@ -193,7 +236,20 @@ interface AvailableModelsInfo {
|
|||||||
cacheExists: boolean
|
cacheExists: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo): string[] {
|
function getCategoryEffectiveVariant(
|
||||||
|
categoryName: string,
|
||||||
|
requirement: ModelRequirement,
|
||||||
|
config: OmoConfig,
|
||||||
|
): string | undefined {
|
||||||
|
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||||
|
if (categoryVariant) {
|
||||||
|
return categoryVariant
|
||||||
|
}
|
||||||
|
const firstEntry = requirement.fallbackChain[0]
|
||||||
|
return firstEntry?.variant ?? requirement.variant
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo, config: OmoConfig): string[] {
|
||||||
const details: string[] = []
|
const details: string[] = []
|
||||||
|
|
||||||
details.push("═══ Available Models (from cache) ═══")
|
details.push("═══ Available Models (from cache) ═══")
|
||||||
@@ -215,14 +271,17 @@ function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModels
|
|||||||
details.push("Agents:")
|
details.push("Agents:")
|
||||||
for (const agent of info.agents) {
|
for (const agent of info.agents) {
|
||||||
const marker = agent.userOverride ? "●" : "○"
|
const marker = agent.userOverride ? "●" : "○"
|
||||||
const display = formatModelWithVariant(agent.effectiveModel, getEffectiveVariant(agent.requirement))
|
const display = formatModelWithVariant(agent.effectiveModel, getEffectiveVariant(agent.name, agent.requirement, config))
|
||||||
details.push(` ${marker} ${agent.name}: ${display}`)
|
details.push(` ${marker} ${agent.name}: ${display}`)
|
||||||
}
|
}
|
||||||
details.push("")
|
details.push("")
|
||||||
details.push("Categories:")
|
details.push("Categories:")
|
||||||
for (const category of info.categories) {
|
for (const category of info.categories) {
|
||||||
const marker = category.userOverride ? "●" : "○"
|
const marker = category.userOverride ? "●" : "○"
|
||||||
const display = formatModelWithVariant(category.effectiveModel, getEffectiveVariant(category.requirement))
|
const display = formatModelWithVariant(
|
||||||
|
category.effectiveModel,
|
||||||
|
getCategoryEffectiveVariant(category.name, category.requirement, config)
|
||||||
|
)
|
||||||
details.push(` ${marker} ${category.name}: ${display}`)
|
details.push(` ${marker} ${category.name}: ${display}`)
|
||||||
}
|
}
|
||||||
details.push("")
|
details.push("")
|
||||||
@@ -249,7 +308,7 @@ export async function checkModelResolution(): Promise<CheckResult> {
|
|||||||
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
|
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
|
||||||
status: available.cacheExists ? "pass" : "warn",
|
status: available.cacheExists ? "pass" : "warn",
|
||||||
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
|
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
|
||||||
details: buildDetailsArray(info, available),
|
details: buildDetailsArray(info, available, config),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi):
|
|||||||
OpenAI Native openai/ models (GPT-5.2 for Oracle)
|
OpenAI Native openai/ models (GPT-5.2 for Oracle)
|
||||||
Gemini Native google/ models (Gemini 3 Pro, Flash)
|
Gemini Native google/ models (Gemini 3 Pro, Flash)
|
||||||
Copilot github-copilot/ models (fallback)
|
Copilot github-copilot/ models (fallback)
|
||||||
OpenCode Zen opencode/ models (opencode/claude-opus-4-5, etc.)
|
OpenCode Zen opencode/ models (opencode/claude-opus-4-6, etc.)
|
||||||
Z.ai zai-coding-plan/glm-4.7 (Librarian priority)
|
Z.ai zai-coding-plan/glm-4.7 (Librarian priority)
|
||||||
Kimi kimi-for-coding/k2p5 (Sisyphus/Prometheus fallback)
|
Kimi kimi-for-coding/k2p5 (Sisyphus/Prometheus fallback)
|
||||||
`)
|
`)
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
|
|||||||
message: "Do you have access to OpenCode Zen (opencode/ models)?",
|
message: "Do you have access to OpenCode Zen (opencode/ models)?",
|
||||||
options: [
|
options: [
|
||||||
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
|
{ value: "no" as const, label: "No", hint: "Will use other configured providers" },
|
||||||
{ value: "yes" as const, label: "Yes", hint: "opencode/claude-opus-4-5, opencode/gpt-5.2, etc." },
|
{ value: "yes" as const, label: "Yes", hint: "opencode/claude-opus-4-6, opencode/gpt-5.2, etc." },
|
||||||
],
|
],
|
||||||
initialValue: initial.opencodeZen,
|
initialValue: initial.opencodeZen,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ describe("generateModelConfig", () => {
|
|||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
|
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-6")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Sisyphus is created when multiple fallback providers are available", () => {
|
test("Sisyphus is created when multiple fallback providers are available", () => {
|
||||||
@@ -393,7 +393,7 @@ describe("generateModelConfig", () => {
|
|||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
|
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-6")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Sisyphus is omitted when no fallback provider is available (OpenAI not in chain)", () => {
|
test("Sisyphus is omitted when no fallback provider is available (OpenAI not in chain)", () => {
|
||||||
@@ -409,7 +409,7 @@ describe("generateModelConfig", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("Hephaestus agent special cases", () => {
|
describe("Hephaestus agent special cases", () => {
|
||||||
test("Hephaestus is created when OpenAI is available (has gpt-5.2-codex)", () => {
|
test("Hephaestus is created when OpenAI is available (openai provider connected)", () => {
|
||||||
// #given
|
// #given
|
||||||
const config = createConfig({ hasOpenAI: true })
|
const config = createConfig({ hasOpenAI: true })
|
||||||
|
|
||||||
@@ -417,11 +417,11 @@ describe("generateModelConfig", () => {
|
|||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(result.agents?.hephaestus?.model).toBe("openai/gpt-5.2-codex")
|
expect(result.agents?.hephaestus?.model).toBe("openai/gpt-5.3-codex")
|
||||||
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Hephaestus is created when Copilot is available (has gpt-5.2-codex)", () => {
|
test("Hephaestus is created when Copilot is available (github-copilot provider connected)", () => {
|
||||||
// #given
|
// #given
|
||||||
const config = createConfig({ hasCopilot: true })
|
const config = createConfig({ hasCopilot: true })
|
||||||
|
|
||||||
@@ -429,11 +429,11 @@ describe("generateModelConfig", () => {
|
|||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(result.agents?.hephaestus?.model).toBe("github-copilot/gpt-5.2-codex")
|
expect(result.agents?.hephaestus?.model).toBe("github-copilot/gpt-5.3-codex")
|
||||||
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Hephaestus is created when OpenCode Zen is available (has gpt-5.2-codex)", () => {
|
test("Hephaestus is created when OpenCode Zen is available (opencode provider connected)", () => {
|
||||||
// #given
|
// #given
|
||||||
const config = createConfig({ hasOpencodeZen: true })
|
const config = createConfig({ hasOpencodeZen: true })
|
||||||
|
|
||||||
@@ -441,11 +441,11 @@ describe("generateModelConfig", () => {
|
|||||||
const result = generateModelConfig(config)
|
const result = generateModelConfig(config)
|
||||||
|
|
||||||
// #then
|
// #then
|
||||||
expect(result.agents?.hephaestus?.model).toBe("opencode/gpt-5.2-codex")
|
expect(result.agents?.hephaestus?.model).toBe("opencode/gpt-5.3-codex")
|
||||||
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
expect(result.agents?.hephaestus?.variant).toBe("medium")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Hephaestus is omitted when only Claude is available (no gpt-5.2-codex)", () => {
|
test("Hephaestus is omitted when only Claude is available (no required provider connected)", () => {
|
||||||
// #given
|
// #given
|
||||||
const config = createConfig({ hasClaude: true })
|
const config = createConfig({ hasClaude: true })
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ describe("generateModelConfig", () => {
|
|||||||
expect(result.agents?.hephaestus).toBeUndefined()
|
expect(result.agents?.hephaestus).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Hephaestus is omitted when only Gemini is available (no gpt-5.2-codex)", () => {
|
test("Hephaestus is omitted when only Gemini is available (no required provider connected)", () => {
|
||||||
// #given
|
// #given
|
||||||
const config = createConfig({ hasGemini: true })
|
const config = createConfig({ hasGemini: true })
|
||||||
|
|
||||||
@@ -467,7 +467,7 @@ describe("generateModelConfig", () => {
|
|||||||
expect(result.agents?.hephaestus).toBeUndefined()
|
expect(result.agents?.hephaestus).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Hephaestus is omitted when only ZAI is available (no gpt-5.2-codex)", () => {
|
test("Hephaestus is omitted when only ZAI is available (no required provider connected)", () => {
|
||||||
// #given
|
// #given
|
||||||
const config = createConfig({ hasZaiCodingPlan: true })
|
const config = createConfig({ hasZaiCodingPlan: true })
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function isProviderAvailable(provider: string, avail: ProviderAvailability): boo
|
|||||||
function transformModelForProvider(provider: string, model: string): string {
|
function transformModelForProvider(provider: string, model: string): string {
|
||||||
if (provider === "github-copilot") {
|
if (provider === "github-copilot") {
|
||||||
return model
|
return model
|
||||||
.replace("claude-opus-4-5", "claude-opus-4.5")
|
.replace("claude-opus-4-6", "claude-opus-4.6")
|
||||||
.replace("claude-sonnet-4-5", "claude-sonnet-4.5")
|
.replace("claude-sonnet-4-5", "claude-sonnet-4.5")
|
||||||
.replace("claude-haiku-4-5", "claude-haiku-4.5")
|
.replace("claude-haiku-4-5", "claude-haiku-4.5")
|
||||||
.replace("claude-sonnet-4", "claude-sonnet-4")
|
.replace("claude-sonnet-4", "claude-sonnet-4")
|
||||||
@@ -122,6 +122,13 @@ function isRequiredModelAvailable(
|
|||||||
return matchingEntry.providers.some((provider) => isProviderAvailable(provider, avail))
|
return matchingEntry.providers.some((provider) => isProviderAvailable(provider, avail))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRequiredProviderAvailable(
|
||||||
|
requiredProviders: string[],
|
||||||
|
avail: ProviderAvailability
|
||||||
|
): boolean {
|
||||||
|
return requiredProviders.some((provider) => isProviderAvailable(provider, avail))
|
||||||
|
}
|
||||||
|
|
||||||
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||||
const avail = toProviderAvailability(config)
|
const avail = toProviderAvailability(config)
|
||||||
const hasAnyProvider =
|
const hasAnyProvider =
|
||||||
@@ -185,6 +192,9 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
|||||||
if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) {
|
if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (req.requiresProvider && !isRequiredProviderAvailable(req.requiresProvider, avail)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const resolved = resolveModelFromChain(req.fallbackChain, avail)
|
const resolved = resolveModelFromChain(req.fallbackChain, avail)
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
@@ -205,6 +215,9 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
|||||||
if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) {
|
if (req.requiresModel && !isRequiredModelAvailable(req.requiresModel, req.fallbackChain, avail)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (req.requiresProvider && !isRequiredProviderAvailable(req.requiresProvider, avail)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const resolved = resolveModelFromChain(fallbackChain, avail)
|
const resolved = resolveModelFromChain(fallbackChain, avail)
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { checkCompletionConditions } from "./completion"
|
|||||||
import { createEventState, processEvents, serializeError } from "./events"
|
import { createEventState, processEvents, serializeError } from "./events"
|
||||||
import type { OhMyOpenCodeConfig } from "../../config"
|
import type { OhMyOpenCodeConfig } from "../../config"
|
||||||
import { loadPluginConfig } from "../../plugin-config"
|
import { loadPluginConfig } from "../../plugin-config"
|
||||||
|
import { getAvailableServerPort, DEFAULT_SERVER_PORT } from "../../shared/port-utils"
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 500
|
const POLL_INTERVAL_MS = 500
|
||||||
const DEFAULT_TIMEOUT_MS = 0
|
const DEFAULT_TIMEOUT_MS = 0
|
||||||
@@ -89,7 +90,7 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
const pluginConfig = loadPluginConfig(directory, { command: "run" })
|
const pluginConfig = loadPluginConfig(directory, { command: "run" })
|
||||||
const resolvedAgent = resolveRunAgent(options, pluginConfig)
|
const resolvedAgent = resolveRunAgent(options, pluginConfig)
|
||||||
|
|
||||||
console.log(pc.cyan("Starting opencode server..."))
|
console.log(pc.cyan("Starting opencode server (auto port selection enabled)..."))
|
||||||
|
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -103,18 +104,24 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Support custom OpenCode server port via environment variable
|
const envPort = process.env.OPENCODE_SERVER_PORT
|
||||||
// This allows Open Agent and other orchestrators to run multiple
|
|
||||||
// concurrent missions without port conflicts
|
|
||||||
const serverPort = process.env.OPENCODE_SERVER_PORT
|
|
||||||
? parseInt(process.env.OPENCODE_SERVER_PORT, 10)
|
? parseInt(process.env.OPENCODE_SERVER_PORT, 10)
|
||||||
: undefined
|
: undefined
|
||||||
const serverHostname = process.env.OPENCODE_SERVER_HOSTNAME || undefined
|
const serverHostname = process.env.OPENCODE_SERVER_HOSTNAME || "127.0.0.1"
|
||||||
|
const preferredPort = envPort && !isNaN(envPort) ? envPort : DEFAULT_SERVER_PORT
|
||||||
|
|
||||||
|
const { port: serverPort, wasAutoSelected } = await getAvailableServerPort(preferredPort, serverHostname)
|
||||||
|
|
||||||
|
if (wasAutoSelected) {
|
||||||
|
console.log(pc.yellow(`Port ${preferredPort} is busy, using port ${serverPort} instead`))
|
||||||
|
} else {
|
||||||
|
console.log(pc.dim(`Using port ${serverPort}`))
|
||||||
|
}
|
||||||
|
|
||||||
const { client, server } = await createOpencode({
|
const { client, server } = await createOpencode({
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
...(serverPort && !isNaN(serverPort) ? { port: serverPort } : {}),
|
port: serverPort,
|
||||||
...(serverHostname ? { hostname: serverHostname } : {}),
|
hostname: serverHostname,
|
||||||
})
|
})
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const BuiltinAgentNameSchema = z.enum([
|
|||||||
export const BuiltinSkillNameSchema = z.enum([
|
export const BuiltinSkillNameSchema = z.enum([
|
||||||
"playwright",
|
"playwright",
|
||||||
"agent-browser",
|
"agent-browser",
|
||||||
|
"dev-browser",
|
||||||
"frontend-ui-ux",
|
"frontend-ui-ux",
|
||||||
"git-master",
|
"git-master",
|
||||||
])
|
])
|
||||||
@@ -63,10 +64,12 @@ export const HookNameSchema = z.enum([
|
|||||||
"comment-checker",
|
"comment-checker",
|
||||||
"grep-output-truncator",
|
"grep-output-truncator",
|
||||||
"tool-output-truncator",
|
"tool-output-truncator",
|
||||||
|
"question-label-truncator",
|
||||||
"directory-agents-injector",
|
"directory-agents-injector",
|
||||||
"directory-readme-injector",
|
"directory-readme-injector",
|
||||||
"empty-task-response-detector",
|
"empty-task-response-detector",
|
||||||
"think-mode",
|
"think-mode",
|
||||||
|
"subagent-question-blocker",
|
||||||
"anthropic-context-window-limit-recovery",
|
"anthropic-context-window-limit-recovery",
|
||||||
"preemptive-compaction",
|
"preemptive-compaction",
|
||||||
"rules-injector",
|
"rules-injector",
|
||||||
@@ -92,13 +95,21 @@ export const HookNameSchema = z.enum([
|
|||||||
"start-work",
|
"start-work",
|
||||||
"atlas",
|
"atlas",
|
||||||
"unstable-agent-babysitter",
|
"unstable-agent-babysitter",
|
||||||
|
"task-reminder",
|
||||||
|
"task-resume-info",
|
||||||
"stop-continuation-guard",
|
"stop-continuation-guard",
|
||||||
"tasks-todowrite-disabler",
|
"tasks-todowrite-disabler",
|
||||||
|
"write-existing-file-guard",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const BuiltinCommandNameSchema = z.enum([
|
export const BuiltinCommandNameSchema = z.enum([
|
||||||
"init-deep",
|
"init-deep",
|
||||||
|
"ralph-loop",
|
||||||
|
"ulw-loop",
|
||||||
|
"cancel-ralph",
|
||||||
|
"refactor",
|
||||||
"start-work",
|
"start-work",
|
||||||
|
"stop-continuation",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const AgentOverrideConfigSchema = z.object({
|
export const AgentOverrideConfigSchema = z.object({
|
||||||
@@ -340,6 +351,17 @@ export const BrowserAutomationConfigSchema = z.object({
|
|||||||
provider: BrowserAutomationProviderSchema.default("playwright"),
|
provider: BrowserAutomationProviderSchema.default("playwright"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const WebsearchProviderSchema = z.enum(["exa", "tavily"])
|
||||||
|
|
||||||
|
export const WebsearchConfigSchema = z.object({
|
||||||
|
/**
|
||||||
|
* Websearch provider to use.
|
||||||
|
* - "exa": Uses Exa websearch (default, works without API key)
|
||||||
|
* - "tavily": Uses Tavily websearch (requires TAVILY_API_KEY)
|
||||||
|
*/
|
||||||
|
provider: WebsearchProviderSchema.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export const TmuxLayoutSchema = z.enum([
|
export const TmuxLayoutSchema = z.enum([
|
||||||
'main-horizontal', // main pane top, agent panes bottom stack
|
'main-horizontal', // main pane top, agent panes bottom stack
|
||||||
'main-vertical', // main pane left, agent panes right stack (default)
|
'main-vertical', // main pane left, agent panes right stack (default)
|
||||||
@@ -357,8 +379,10 @@ export const TmuxConfigSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const SisyphusTasksConfigSchema = z.object({
|
export const SisyphusTasksConfigSchema = z.object({
|
||||||
/** Storage path for tasks (default: .sisyphus/tasks) */
|
/** Absolute or relative storage path override. When set, bypasses global config dir. */
|
||||||
storage_path: z.string().default(".sisyphus/tasks"),
|
storage_path: z.string().optional(),
|
||||||
|
/** Force task list ID (alternative to env ULTRAWORK_TASK_LIST_ID) */
|
||||||
|
task_list_id: z.string().optional(),
|
||||||
/** Enable Claude Code path compatibility mode */
|
/** Enable Claude Code path compatibility mode */
|
||||||
claude_code_compat: z.boolean().default(false),
|
claude_code_compat: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
@@ -393,6 +417,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
|||||||
babysitting: BabysittingConfigSchema.optional(),
|
babysitting: BabysittingConfigSchema.optional(),
|
||||||
git_master: GitMasterConfigSchema.optional(),
|
git_master: GitMasterConfigSchema.optional(),
|
||||||
browser_automation_engine: BrowserAutomationConfigSchema.optional(),
|
browser_automation_engine: BrowserAutomationConfigSchema.optional(),
|
||||||
|
websearch: WebsearchConfigSchema.optional(),
|
||||||
tmux: TmuxConfigSchema.optional(),
|
tmux: TmuxConfigSchema.optional(),
|
||||||
sisyphus: SisyphusConfigSchema.optional(),
|
sisyphus: SisyphusConfigSchema.optional(),
|
||||||
})
|
})
|
||||||
@@ -420,6 +445,8 @@ export type BuiltinCategoryName = z.infer<typeof BuiltinCategoryNameSchema>
|
|||||||
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>
|
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>
|
||||||
export type BrowserAutomationProvider = z.infer<typeof BrowserAutomationProviderSchema>
|
export type BrowserAutomationProvider = z.infer<typeof BrowserAutomationProviderSchema>
|
||||||
export type BrowserAutomationConfig = z.infer<typeof BrowserAutomationConfigSchema>
|
export type BrowserAutomationConfig = z.infer<typeof BrowserAutomationConfigSchema>
|
||||||
|
export type WebsearchProvider = z.infer<typeof WebsearchProviderSchema>
|
||||||
|
export type WebsearchConfig = z.infer<typeof WebsearchConfigSchema>
|
||||||
export type TmuxConfig = z.infer<typeof TmuxConfigSchema>
|
export type TmuxConfig = z.infer<typeof TmuxConfigSchema>
|
||||||
export type TmuxLayout = z.infer<typeof TmuxLayoutSchema>
|
export type TmuxLayout = z.infer<typeof TmuxLayoutSchema>
|
||||||
export type SisyphusTasksConfig = z.infer<typeof SisyphusTasksConfigSchema>
|
export type SisyphusTasksConfig = z.infer<typeof SisyphusTasksConfigSchema>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
|
|
||||||
20 feature modules: background agents, skill MCPs, builtin skills/commands, Claude Code compatibility layer.
|
17 feature modules: background agents, skill MCPs, builtin skills/commands, Claude Code compatibility layer, task management.
|
||||||
|
|
||||||
**Feature Types**: Task orchestration, Skill definitions, Command templates, Claude Code loaders, Supporting utilities
|
**Feature Types**: Task orchestration, Skill definitions, Command templates, Claude Code loaders, Supporting utilities
|
||||||
|
|
||||||
@@ -10,27 +10,25 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
features/
|
features/
|
||||||
├── background-agent/ # Task lifecycle (1418 lines)
|
├── background-agent/ # Task lifecycle (1556 lines)
|
||||||
│ ├── manager.ts # Launch → poll → complete
|
│ ├── manager.ts # Launch → poll → complete
|
||||||
│ └── concurrency.ts # Per-provider limits
|
│ └── concurrency.ts # Per-provider limits
|
||||||
├── builtin-skills/ # Core skills (1729 lines)
|
├── builtin-skills/ # Core skills
|
||||||
│ └── skills.ts # playwright, dev-browser, frontend-ui-ux, git-master, typescript-programmer
|
│ └── skills/ # playwright, agent-browser, frontend-ui-ux, git-master, dev-browser
|
||||||
├── builtin-commands/ # ralph-loop, refactor, ulw-loop, init-deep, start-work, cancel-ralph, stop-continuation
|
├── builtin-commands/ # ralph-loop, refactor, ulw-loop, init-deep, start-work, cancel-ralph, stop-continuation
|
||||||
├── claude-code-agent-loader/ # ~/.claude/agents/*.md
|
├── claude-code-agent-loader/ # ~/.claude/agents/*.md
|
||||||
├── claude-code-command-loader/ # ~/.claude/commands/*.md
|
├── claude-code-command-loader/ # ~/.claude/commands/*.md
|
||||||
├── claude-code-mcp-loader/ # .mcp.json with ${VAR} expansion
|
├── claude-code-mcp-loader/ # .mcp.json with ${VAR} expansion
|
||||||
├── claude-code-plugin-loader/ # installed_plugins.json
|
├── claude-code-plugin-loader/ # installed_plugins.json (486 lines)
|
||||||
├── claude-code-session-state/ # Session persistence
|
├── claude-code-session-state/ # Session persistence
|
||||||
├── opencode-skill-loader/ # Skills from 6 directories
|
├── opencode-skill-loader/ # Skills from 6 directories (loader.ts 311 lines)
|
||||||
├── context-injector/ # AGENTS.md/README.md injection
|
├── context-injector/ # AGENTS.md/README.md injection
|
||||||
├── boulder-state/ # Todo state persistence
|
├── boulder-state/ # Todo state persistence
|
||||||
├── hook-message-injector/ # Message injection
|
├── hook-message-injector/ # Message injection
|
||||||
├── task-toast-manager/ # Background task notifications
|
├── task-toast-manager/ # Background task notifications
|
||||||
├── skill-mcp-manager/ # MCP client lifecycle (617 lines)
|
├── skill-mcp-manager/ # MCP client lifecycle (640 lines)
|
||||||
├── tmux-subagent/ # Tmux session management
|
├── tmux-subagent/ # Tmux session management (472 lines)
|
||||||
├── mcp-oauth/ # MCP OAuth handling
|
├── mcp-oauth/ # MCP OAuth handling
|
||||||
├── sisyphus-swarm/ # Swarm coordination
|
|
||||||
├── sisyphus-tasks/ # Task tracking
|
|
||||||
└── claude-tasks/ # Task schema/storage - see AGENTS.md
|
└── claude-tasks/ # Task schema/storage - see AGENTS.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ describe("ConcurrencyManager.getConcurrencyLimit", () => {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
const modelLimit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
|
const modelLimit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
|
||||||
const providerLimit = manager.getConcurrencyLimit("anthropic/claude-opus-4-5")
|
const providerLimit = manager.getConcurrencyLimit("anthropic/claude-opus-4-6")
|
||||||
const defaultLimit = manager.getConcurrencyLimit("google/gemini-3-pro")
|
const defaultLimit = manager.getConcurrencyLimit("google/gemini-3-pro")
|
||||||
|
|
||||||
// then
|
// then
|
||||||
|
|||||||
@@ -783,7 +783,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
|||||||
}
|
}
|
||||||
const currentMessage: CurrentMessage = {
|
const currentMessage: CurrentMessage = {
|
||||||
agent: "sisyphus",
|
agent: "sisyphus",
|
||||||
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@@ -791,7 +791,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
|||||||
|
|
||||||
// then - uses currentMessage values, not task.parentModel/parentAgent
|
// then - uses currentMessage values, not task.parentModel/parentAgent
|
||||||
expect(promptBody.agent).toBe("sisyphus")
|
expect(promptBody.agent).toBe("sisyphus")
|
||||||
expect(promptBody.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-5" })
|
expect(promptBody.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should fallback to parentAgent when currentMessage.agent is undefined", async () => {
|
test("should fallback to parentAgent when currentMessage.agent is undefined", async () => {
|
||||||
@@ -875,6 +875,90 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("BackgroundManager.notifyParentSession - aborted parent", () => {
|
||||||
|
test("should skip notification when parent session is aborted", async () => {
|
||||||
|
//#given
|
||||||
|
let promptCalled = false
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
prompt: async () => {
|
||||||
|
promptCalled = true
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
abort: async () => ({}),
|
||||||
|
messages: async () => {
|
||||||
|
const error = new Error("User aborted")
|
||||||
|
error.name = "MessageAbortedError"
|
||||||
|
throw error
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: "task-aborted-parent",
|
||||||
|
sessionID: "session-child",
|
||||||
|
parentSessionID: "session-parent",
|
||||||
|
parentMessageID: "msg-parent",
|
||||||
|
description: "task aborted parent",
|
||||||
|
prompt: "test",
|
||||||
|
agent: "explore",
|
||||||
|
status: "completed",
|
||||||
|
startedAt: new Date(),
|
||||||
|
completedAt: new Date(),
|
||||||
|
}
|
||||||
|
getPendingByParent(manager).set("session-parent", new Set([task.id, "task-remaining"]))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await (manager as unknown as { notifyParentSession: (task: BackgroundTask) => Promise<void> })
|
||||||
|
.notifyParentSession(task)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptCalled).toBe(false)
|
||||||
|
|
||||||
|
manager.shutdown()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should swallow aborted error from prompt", async () => {
|
||||||
|
//#given
|
||||||
|
let promptCalled = false
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
prompt: async () => {
|
||||||
|
promptCalled = true
|
||||||
|
const error = new Error("User aborted")
|
||||||
|
error.name = "MessageAbortedError"
|
||||||
|
throw error
|
||||||
|
},
|
||||||
|
abort: async () => ({}),
|
||||||
|
messages: async () => ({ data: [] }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||||
|
const task: BackgroundTask = {
|
||||||
|
id: "task-aborted-prompt",
|
||||||
|
sessionID: "session-child",
|
||||||
|
parentSessionID: "session-parent",
|
||||||
|
parentMessageID: "msg-parent",
|
||||||
|
description: "task aborted prompt",
|
||||||
|
prompt: "test",
|
||||||
|
agent: "explore",
|
||||||
|
status: "completed",
|
||||||
|
startedAt: new Date(),
|
||||||
|
completedAt: new Date(),
|
||||||
|
}
|
||||||
|
getPendingByParent(manager).set("session-parent", new Set([task.id]))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await (manager as unknown as { notifyParentSession: (task: BackgroundTask) => Promise<void> })
|
||||||
|
.notifyParentSession(task)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptCalled).toBe(true)
|
||||||
|
|
||||||
|
manager.shutdown()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function buildNotificationPromptBody(
|
function buildNotificationPromptBody(
|
||||||
task: BackgroundTask,
|
task: BackgroundTask,
|
||||||
currentMessage: CurrentMessage | null
|
currentMessage: CurrentMessage | null
|
||||||
@@ -913,7 +997,7 @@ describe("BackgroundManager.tryCompleteTask", () => {
|
|||||||
|
|
||||||
test("should release concurrency and clear key on completion", async () => {
|
test("should release concurrency and clear key on completion", async () => {
|
||||||
// given
|
// given
|
||||||
const concurrencyKey = "anthropic/claude-opus-4-5"
|
const concurrencyKey = "anthropic/claude-opus-4-6"
|
||||||
const concurrencyManager = getConcurrencyManager(manager)
|
const concurrencyManager = getConcurrencyManager(manager)
|
||||||
await concurrencyManager.acquire(concurrencyKey)
|
await concurrencyManager.acquire(concurrencyKey)
|
||||||
|
|
||||||
@@ -942,7 +1026,7 @@ describe("BackgroundManager.tryCompleteTask", () => {
|
|||||||
|
|
||||||
test("should prevent double completion and double release", async () => {
|
test("should prevent double completion and double release", async () => {
|
||||||
// given
|
// given
|
||||||
const concurrencyKey = "anthropic/claude-opus-4-5"
|
const concurrencyKey = "anthropic/claude-opus-4-6"
|
||||||
const concurrencyManager = getConcurrencyManager(manager)
|
const concurrencyManager = getConcurrencyManager(manager)
|
||||||
await concurrencyManager.acquire(concurrencyKey)
|
await concurrencyManager.acquire(concurrencyKey)
|
||||||
|
|
||||||
@@ -1573,7 +1657,7 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
|
|||||||
description: "Task 1",
|
description: "Task 1",
|
||||||
prompt: "Do something",
|
prompt: "Do something",
|
||||||
agent: "test-agent",
|
agent: "test-agent",
|
||||||
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
|
||||||
parentSessionID: "parent-session",
|
parentSessionID: "parent-session",
|
||||||
parentMessageID: "parent-message",
|
parentMessageID: "parent-message",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,11 @@ export class BackgroundManager {
|
|||||||
existingTask.concurrencyKey = undefined
|
existingTask.concurrencyKey = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Abort the session to prevent infinite polling hang
|
||||||
|
this.client.session.abort({
|
||||||
|
path: { id: sessionID },
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
this.markForNotification(existingTask)
|
this.markForNotification(existingTask)
|
||||||
this.notifyParentSession(existingTask).catch(err => {
|
this.notifyParentSession(existingTask).catch(err => {
|
||||||
log("[background-agent] Failed to notify on error:", err)
|
log("[background-agent] Failed to notify on error:", err)
|
||||||
@@ -600,6 +605,14 @@ export class BackgroundManager {
|
|||||||
this.concurrencyManager.release(existingTask.concurrencyKey)
|
this.concurrencyManager.release(existingTask.concurrencyKey)
|
||||||
existingTask.concurrencyKey = undefined
|
existingTask.concurrencyKey = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Abort the session to prevent infinite polling hang
|
||||||
|
if (existingTask.sessionID) {
|
||||||
|
this.client.session.abort({
|
||||||
|
path: { id: existingTask.sessionID },
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
this.markForNotification(existingTask)
|
this.markForNotification(existingTask)
|
||||||
this.notifyParentSession(existingTask).catch(err => {
|
this.notifyParentSession(existingTask).catch(err => {
|
||||||
log("[background-agent] Failed to notify on resume error:", err)
|
log("[background-agent] Failed to notify on resume error:", err)
|
||||||
@@ -1110,7 +1123,14 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
if (this.isAbortedSessionError(error)) {
|
||||||
|
log("[background-agent] Parent session aborted, skipping notification:", {
|
||||||
|
taskId: task.id,
|
||||||
|
parentSessionID: task.parentSessionID,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
const messageDir = getMessageDir(task.parentSessionID)
|
const messageDir = getMessageDir(task.parentSessionID)
|
||||||
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
const currentMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
|
||||||
agent = currentMessage?.agent ?? task.parentAgent
|
agent = currentMessage?.agent ?? task.parentAgent
|
||||||
@@ -1141,6 +1161,13 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
noReply: !allComplete,
|
noReply: !allComplete,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (this.isAbortedSessionError(error)) {
|
||||||
|
log("[background-agent] Parent session aborted, skipping notification:", {
|
||||||
|
taskId: task.id,
|
||||||
|
parentSessionID: task.parentSessionID,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
log("[background-agent] Failed to send notification:", error)
|
log("[background-agent] Failed to send notification:", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,6 +1206,28 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|||||||
return `${seconds}s`
|
return `${seconds}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isAbortedSessionError(error: unknown): boolean {
|
||||||
|
const message = this.getErrorText(error)
|
||||||
|
return message.toLowerCase().includes("aborted")
|
||||||
|
}
|
||||||
|
|
||||||
|
private getErrorText(error: unknown): string {
|
||||||
|
if (!error) return ""
|
||||||
|
if (typeof error === "string") return error
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return `${error.name}: ${error.message}`
|
||||||
|
}
|
||||||
|
if (typeof error === "object" && error !== null) {
|
||||||
|
if ("message" in error && typeof error.message === "string") {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
if ("name" in error && typeof error.name === "string") {
|
||||||
|
return error.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
private hasRunningTasks(): boolean {
|
private hasRunningTasks(): boolean {
|
||||||
for (const task of this.tasks.values()) {
|
for (const task of this.tasks.values()) {
|
||||||
if (task.status === "running") return true
|
if (task.status === "running") return true
|
||||||
|
|||||||
@@ -246,5 +246,33 @@ describe("boulder-state", () => {
|
|||||||
expect(state.plan_name).toBe("auth-refactor")
|
expect(state.plan_name).toBe("auth-refactor")
|
||||||
expect(state.started_at).toBeDefined()
|
expect(state.started_at).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should include agent field when provided", () => {
|
||||||
|
//#given - plan path, session id, and agent type
|
||||||
|
const planPath = "/path/to/feature.md"
|
||||||
|
const sessionId = "ses-xyz789"
|
||||||
|
const agent = "atlas"
|
||||||
|
|
||||||
|
//#when - createBoulderState is called with agent
|
||||||
|
const state = createBoulderState(planPath, sessionId, agent)
|
||||||
|
|
||||||
|
//#then - state should include the agent field
|
||||||
|
expect(state.agent).toBe("atlas")
|
||||||
|
expect(state.active_plan).toBe(planPath)
|
||||||
|
expect(state.session_ids).toEqual([sessionId])
|
||||||
|
expect(state.plan_name).toBe("feature")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should allow agent to be undefined", () => {
|
||||||
|
//#given - plan path and session id without agent
|
||||||
|
const planPath = "/path/to/legacy.md"
|
||||||
|
const sessionId = "ses-legacy"
|
||||||
|
|
||||||
|
//#when - createBoulderState is called without agent
|
||||||
|
const state = createBoulderState(planPath, sessionId)
|
||||||
|
|
||||||
|
//#then - state should not have agent field (backward compatible)
|
||||||
|
expect(state.agent).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -139,12 +139,14 @@ export function getPlanName(planPath: string): string {
|
|||||||
*/
|
*/
|
||||||
export function createBoulderState(
|
export function createBoulderState(
|
||||||
planPath: string,
|
planPath: string,
|
||||||
sessionId: string
|
sessionId: string,
|
||||||
|
agent?: string
|
||||||
): BoulderState {
|
): BoulderState {
|
||||||
return {
|
return {
|
||||||
active_plan: planPath,
|
active_plan: planPath,
|
||||||
started_at: new Date().toISOString(),
|
started_at: new Date().toISOString(),
|
||||||
session_ids: [sessionId],
|
session_ids: [sessionId],
|
||||||
plan_name: getPlanName(planPath),
|
plan_name: getPlanName(planPath),
|
||||||
|
...(agent !== undefined ? { agent } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface BoulderState {
|
|||||||
session_ids: string[]
|
session_ids: string[]
|
||||||
/** Plan name derived from filename */
|
/** Plan name derived from filename */
|
||||||
plan_name: string
|
plan_name: string
|
||||||
|
/** Agent type to use when resuming (e.g., 'atlas') */
|
||||||
|
agent?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanProgress {
|
export interface PlanProgress {
|
||||||
|
|||||||
@@ -86,4 +86,58 @@ describe("createBuiltinSkills", () => {
|
|||||||
expect(defaultSkills).toHaveLength(4)
|
expect(defaultSkills).toHaveLength(4)
|
||||||
expect(agentBrowserSkills).toHaveLength(4)
|
expect(agentBrowserSkills).toHaveLength(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should exclude playwright when it is in disabledSkills", () => {
|
||||||
|
// #given
|
||||||
|
const options = { disabledSkills: new Set(["playwright"]) }
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const skills = createBuiltinSkills(options)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(skills.map((s) => s.name)).not.toContain("playwright")
|
||||||
|
expect(skills.map((s) => s.name)).toContain("frontend-ui-ux")
|
||||||
|
expect(skills.map((s) => s.name)).toContain("git-master")
|
||||||
|
expect(skills.map((s) => s.name)).toContain("dev-browser")
|
||||||
|
expect(skills.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should exclude multiple skills when they are in disabledSkills", () => {
|
||||||
|
// #given
|
||||||
|
const options = { disabledSkills: new Set(["playwright", "git-master"]) }
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const skills = createBuiltinSkills(options)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(skills.map((s) => s.name)).not.toContain("playwright")
|
||||||
|
expect(skills.map((s) => s.name)).not.toContain("git-master")
|
||||||
|
expect(skills.map((s) => s.name)).toContain("frontend-ui-ux")
|
||||||
|
expect(skills.map((s) => s.name)).toContain("dev-browser")
|
||||||
|
expect(skills.length).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return an empty array when all skills are disabled", () => {
|
||||||
|
// #given
|
||||||
|
const options = {
|
||||||
|
disabledSkills: new Set(["playwright", "frontend-ui-ux", "git-master", "dev-browser"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const skills = createBuiltinSkills(options)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(skills.length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should return all skills when disabledSkills set is empty", () => {
|
||||||
|
// #given
|
||||||
|
const options = { disabledSkills: new Set<string>() }
|
||||||
|
|
||||||
|
// #when
|
||||||
|
const skills = createBuiltinSkills(options)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(skills.length).toBe(4)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,12 +11,19 @@ import {
|
|||||||
|
|
||||||
export interface CreateBuiltinSkillsOptions {
|
export interface CreateBuiltinSkillsOptions {
|
||||||
browserProvider?: BrowserAutomationProvider
|
browserProvider?: BrowserAutomationProvider
|
||||||
|
disabledSkills?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuiltinSkills(options: CreateBuiltinSkillsOptions = {}): BuiltinSkill[] {
|
export function createBuiltinSkills(options: CreateBuiltinSkillsOptions = {}): BuiltinSkill[] {
|
||||||
const { browserProvider = "playwright" } = options
|
const { browserProvider = "playwright", disabledSkills } = options
|
||||||
|
|
||||||
const browserSkill = browserProvider === "agent-browser" ? agentBrowserSkill : playwrightSkill
|
const browserSkill = browserProvider === "agent-browser" ? agentBrowserSkill : playwrightSkill
|
||||||
|
|
||||||
return [browserSkill, frontendUiUxSkill, gitMasterSkill, devBrowserSkill]
|
const skills = [browserSkill, frontendUiUxSkill, gitMasterSkill, devBrowserSkill]
|
||||||
|
|
||||||
|
if (!disabledSkills) {
|
||||||
|
return skills
|
||||||
|
}
|
||||||
|
|
||||||
|
return skills.filter((skill) => !disabledSkills.has(skill.name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ claude-tasks/
|
|||||||
├── types.test.ts # Schema validation tests (8 tests)
|
├── types.test.ts # Schema validation tests (8 tests)
|
||||||
├── storage.ts # File operations
|
├── storage.ts # File operations
|
||||||
├── storage.test.ts # Storage tests (14 tests)
|
├── storage.test.ts # Storage tests (14 tests)
|
||||||
|
├── todo-sync.ts # Task → Todo synchronization
|
||||||
└── index.ts # Barrel exports
|
└── index.ts # Barrel exports
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,67 +45,21 @@ interface Task {
|
|||||||
|
|
||||||
## TODO SYNC
|
## TODO SYNC
|
||||||
|
|
||||||
The task system includes a sync layer (`todo-sync.ts`) that automatically mirrors task state to the project's Todo system.
|
Task system includes sync layer (`todo-sync.ts`) that automatically mirrors task state to the project's Todo system.
|
||||||
|
|
||||||
- **Creation**: Creating a task via `task_create` adds a corresponding item to the Todo list.
|
- **Creation**: `task_create` adds corresponding Todo item
|
||||||
- **Updates**: Updating a task's `status` or `subject` via `task_update` reflects in the Todo list.
|
- **Updates**: `task_update` reflects in Todo list
|
||||||
- **Completion**: Marking a task as `completed` automatically marks the Todo item as done.
|
- **Completion**: `completed` status marks Todo item done
|
||||||
|
|
||||||
## STORAGE UTILITIES
|
## STORAGE UTILITIES
|
||||||
|
|
||||||
### getTaskDir(config)
|
| Function | Purpose |
|
||||||
|
|----------|---------|
|
||||||
Returns: `.sisyphus/tasks` (or custom path from config)
|
| `getTaskDir(config)` | Returns task storage directory path |
|
||||||
|
| `resolveTaskListId(config)` | Resolves task list ID (env → config → cwd basename) |
|
||||||
### readJsonSafe(filePath, schema)
|
| `readJsonSafe(path, schema)` | Parse + validate, returns null on failure |
|
||||||
|
| `writeJsonAtomic(path, data)` | Atomic write via temp file + rename |
|
||||||
- Returns parsed & validated data or `null`
|
| `acquireLock(dirPath)` | File-based lock with 30s stale threshold |
|
||||||
- Safe for missing files, invalid JSON, schema violations
|
|
||||||
|
|
||||||
### writeJsonAtomic(filePath, data)
|
|
||||||
|
|
||||||
- Atomic write via temp file + rename
|
|
||||||
- Creates parent directories automatically
|
|
||||||
- Cleans up temp file on error
|
|
||||||
|
|
||||||
### acquireLock(dirPath)
|
|
||||||
|
|
||||||
- File-based lock: `.lock` file with timestamp
|
|
||||||
- 30-second stale threshold
|
|
||||||
- Returns `{ acquired: boolean, release: () => void }`
|
|
||||||
|
|
||||||
## TESTING
|
|
||||||
|
|
||||||
**types.test.ts** (8 tests):
|
|
||||||
- Valid status enum values
|
|
||||||
- Required vs optional fields
|
|
||||||
- Array validation (blocks, blockedBy)
|
|
||||||
- Schema rejection for invalid data
|
|
||||||
|
|
||||||
**storage.test.ts** (14 tests):
|
|
||||||
- Path construction
|
|
||||||
- Safe JSON reading (missing files, invalid JSON, schema failures)
|
|
||||||
- Atomic writes (directory creation, overwrites)
|
|
||||||
- Lock acquisition (fresh locks, stale locks, release)
|
|
||||||
|
|
||||||
## USAGE
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { TaskSchema, getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock } from "./features/claude-tasks"
|
|
||||||
|
|
||||||
const taskDir = getTaskDir(config)
|
|
||||||
const lock = acquireLock(taskDir)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const task = readJsonSafe(join(taskDir, "1.json"), TaskSchema)
|
|
||||||
if (task) {
|
|
||||||
task.status = "completed"
|
|
||||||
writeJsonAtomic(join(taskDir, "1.json"), task)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
lock.release()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ANTI-PATTERNS
|
## ANTI-PATTERNS
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,99 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"
|
||||||
import { join } from "path"
|
import { join, basename } from "path"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { getTaskDir, readJsonSafe, writeJsonAtomic, acquireLock, generateTaskId, listTaskFiles } from "./storage"
|
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||||
|
import {
|
||||||
|
getTaskDir,
|
||||||
|
readJsonSafe,
|
||||||
|
writeJsonAtomic,
|
||||||
|
acquireLock,
|
||||||
|
generateTaskId,
|
||||||
|
listTaskFiles,
|
||||||
|
resolveTaskListId,
|
||||||
|
sanitizePathSegment,
|
||||||
|
} from "./storage"
|
||||||
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||||
|
|
||||||
const TEST_DIR = ".test-claude-tasks"
|
const TEST_DIR = ".test-claude-tasks"
|
||||||
const TEST_DIR_ABS = join(process.cwd(), TEST_DIR)
|
const TEST_DIR_ABS = join(process.cwd(), TEST_DIR)
|
||||||
|
|
||||||
describe("getTaskDir", () => {
|
describe("getTaskDir", () => {
|
||||||
test("returns correct path for default config", () => {
|
const originalTaskListId = process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (originalTaskListId === undefined) {
|
||||||
|
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
} else {
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalTaskListId === undefined) {
|
||||||
|
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
} else {
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns global config path for default config", () => {
|
||||||
//#given
|
//#given
|
||||||
const config: Partial<OhMyOpenCodeConfig> = {}
|
const config: Partial<OhMyOpenCodeConfig> = {}
|
||||||
|
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||||
|
const expectedListId = sanitizePathSegment(basename(process.cwd()))
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const result = getTaskDir(config)
|
const result = getTaskDir(config)
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toBe(join(process.cwd(), ".sisyphus/tasks"))
|
expect(result).toBe(join(configDir, "tasks", expectedListId))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns correct path with custom storage_path", () => {
|
test("respects ULTRAWORK_TASK_LIST_ID env var", () => {
|
||||||
|
//#given
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = "custom list/id"
|
||||||
|
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = getTaskDir()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(join(configDir, "tasks", "custom-list-id"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to sanitized cwd basename when env var not set", () => {
|
||||||
|
//#given
|
||||||
|
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||||
|
const expectedListId = sanitizePathSegment(basename(process.cwd()))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = getTaskDir()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(join(configDir, "tasks", expectedListId))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns absolute storage_path without joining cwd", () => {
|
||||||
|
//#given
|
||||||
|
const config: Partial<OhMyOpenCodeConfig> = {
|
||||||
|
sisyphus: {
|
||||||
|
tasks: {
|
||||||
|
storage_path: "/tmp/custom-task-path",
|
||||||
|
claude_code_compat: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = getTaskDir(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("/tmp/custom-task-path")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("joins relative storage_path with cwd", () => {
|
||||||
//#given
|
//#given
|
||||||
const config: Partial<OhMyOpenCodeConfig> = {
|
const config: Partial<OhMyOpenCodeConfig> = {
|
||||||
sisyphus: {
|
sisyphus: {
|
||||||
@@ -37,13 +110,59 @@ describe("getTaskDir", () => {
|
|||||||
//#then
|
//#then
|
||||||
expect(result).toBe(join(process.cwd(), ".custom/tasks"))
|
expect(result).toBe(join(process.cwd(), ".custom/tasks"))
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveTaskListId", () => {
|
||||||
|
const originalTaskListId = process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (originalTaskListId === undefined) {
|
||||||
|
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
} else {
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalTaskListId === undefined) {
|
||||||
|
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
} else {
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = originalTaskListId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns env var when set", () => {
|
||||||
|
//#given
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = "custom-list"
|
||||||
|
|
||||||
test("returns correct path with default config parameter", () => {
|
|
||||||
//#when
|
//#when
|
||||||
const result = getTaskDir()
|
const result = resolveTaskListId()
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(result).toBe(join(process.cwd(), ".sisyphus/tasks"))
|
expect(result).toBe("custom-list")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sanitizes special characters", () => {
|
||||||
|
//#given
|
||||||
|
process.env.ULTRAWORK_TASK_LIST_ID = "custom list/id"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveTaskListId()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe("custom-list-id")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns sanitized cwd basename when env var not set", () => {
|
||||||
|
//#given
|
||||||
|
delete process.env.ULTRAWORK_TASK_LIST_ID
|
||||||
|
const expected = sanitizePathSegment(basename(process.cwd()))
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = resolveTaskListId()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result).toBe(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,35 @@
|
|||||||
import { join, dirname } from "path"
|
import { join, dirname, basename, isAbsolute } from "path"
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync } from "fs"
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync } from "fs"
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto"
|
||||||
|
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
import type { OhMyOpenCodeConfig } from "../../config/schema"
|
||||||
|
|
||||||
export function getTaskDir(config: Partial<OhMyOpenCodeConfig> = {}): string {
|
export function getTaskDir(config: Partial<OhMyOpenCodeConfig> = {}): string {
|
||||||
const tasksConfig = config.sisyphus?.tasks
|
const tasksConfig = config.sisyphus?.tasks
|
||||||
const storagePath = tasksConfig?.storage_path ?? ".sisyphus/tasks"
|
const storagePath = tasksConfig?.storage_path
|
||||||
return join(process.cwd(), storagePath)
|
|
||||||
|
if (storagePath) {
|
||||||
|
return isAbsolute(storagePath) ? storagePath : join(process.cwd(), storagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const configDir = getOpenCodeConfigDir({ binary: "opencode" })
|
||||||
|
const listId = resolveTaskListId(config)
|
||||||
|
return join(configDir, "tasks", listId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizePathSegment(value: string): string {
|
||||||
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTaskListId(config: Partial<OhMyOpenCodeConfig> = {}): string {
|
||||||
|
const envId = process.env.ULTRAWORK_TASK_LIST_ID?.trim()
|
||||||
|
if (envId) return sanitizePathSegment(envId)
|
||||||
|
|
||||||
|
const configId = config.sisyphus?.tasks?.task_list_id?.trim()
|
||||||
|
if (configId) return sanitizePathSegment(configId)
|
||||||
|
|
||||||
|
return sanitizePathSegment(basename(process.cwd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureDir(dirPath: string): void {
|
export function ensureDir(dirPath: string): void {
|
||||||
|
|||||||
@@ -387,4 +387,177 @@ Skill body.
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("deduplication", () => {
|
||||||
|
it("deduplicates skills by name across scopes, keeping higher priority (opencode-project > opencode > project)", async () => {
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
const originalOpenCodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
||||||
|
const originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
|
|
||||||
|
// given: same skill name in multiple scopes
|
||||||
|
const opencodeProjectSkillsDir = join(TEST_DIR, ".opencode", "skills")
|
||||||
|
const opencodeConfigDir = join(TEST_DIR, "opencode-global")
|
||||||
|
const opencodeGlobalSkillsDir = join(opencodeConfigDir, "skills")
|
||||||
|
const projectClaudeSkillsDir = join(TEST_DIR, ".claude", "skills")
|
||||||
|
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = join(TEST_DIR, "claude-user")
|
||||||
|
|
||||||
|
mkdirSync(join(opencodeProjectSkillsDir, "duplicate-skill"), { recursive: true })
|
||||||
|
mkdirSync(join(opencodeGlobalSkillsDir, "duplicate-skill"), { recursive: true })
|
||||||
|
mkdirSync(join(projectClaudeSkillsDir, "duplicate-skill"), { recursive: true })
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(opencodeProjectSkillsDir, "duplicate-skill", "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: duplicate-skill
|
||||||
|
description: From opencode-project (highest priority)
|
||||||
|
---
|
||||||
|
opencode-project body.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(opencodeGlobalSkillsDir, "duplicate-skill", "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: duplicate-skill
|
||||||
|
description: From opencode-global (middle priority)
|
||||||
|
---
|
||||||
|
opencode-global body.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(projectClaudeSkillsDir, "duplicate-skill", "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: duplicate-skill
|
||||||
|
description: From claude project (lowest priority among these)
|
||||||
|
---
|
||||||
|
claude project body.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const { discoverSkills } = await import("./loader")
|
||||||
|
process.chdir(TEST_DIR)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const skills = await discoverSkills()
|
||||||
|
const duplicates = skills.filter(s => s.name === "duplicate-skill")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(duplicates).toHaveLength(1)
|
||||||
|
expect(duplicates[0]?.scope).toBe("opencode-project")
|
||||||
|
expect(duplicates[0]?.definition.description).toContain("opencode-project")
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
if (originalOpenCodeConfigDir === undefined) {
|
||||||
|
delete process.env.OPENCODE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
|
||||||
|
}
|
||||||
|
if (originalClaudeConfigDir === undefined) {
|
||||||
|
delete process.env.CLAUDE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("prioritizes OpenCode global skills over legacy Claude project skills", async () => {
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
const originalOpenCodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
||||||
|
const originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||||
|
|
||||||
|
const opencodeConfigDir = join(TEST_DIR, "opencode-global")
|
||||||
|
const opencodeGlobalSkillsDir = join(opencodeConfigDir, "skills")
|
||||||
|
const projectClaudeSkillsDir = join(TEST_DIR, ".claude", "skills")
|
||||||
|
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = join(TEST_DIR, "claude-user")
|
||||||
|
|
||||||
|
mkdirSync(join(opencodeGlobalSkillsDir, "global-over-project"), { recursive: true })
|
||||||
|
mkdirSync(join(projectClaudeSkillsDir, "global-over-project"), { recursive: true })
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(opencodeGlobalSkillsDir, "global-over-project", "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: global-over-project
|
||||||
|
description: From opencode-global (should win)
|
||||||
|
---
|
||||||
|
opencode-global body.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(projectClaudeSkillsDir, "global-over-project", "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: global-over-project
|
||||||
|
description: From claude project (should lose)
|
||||||
|
---
|
||||||
|
claude project body.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
const { discoverSkills } = await import("./loader")
|
||||||
|
process.chdir(TEST_DIR)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const skills = await discoverSkills()
|
||||||
|
const matches = skills.filter(s => s.name === "global-over-project")
|
||||||
|
|
||||||
|
expect(matches).toHaveLength(1)
|
||||||
|
expect(matches[0]?.scope).toBe("opencode")
|
||||||
|
expect(matches[0]?.definition.description).toContain("opencode-global")
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
if (originalOpenCodeConfigDir === undefined) {
|
||||||
|
delete process.env.OPENCODE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
|
||||||
|
}
|
||||||
|
if (originalClaudeConfigDir === undefined) {
|
||||||
|
delete process.env.CLAUDE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns no duplicates from discoverSkills", async () => {
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
const originalOpenCodeConfigDir = process.env.OPENCODE_CONFIG_DIR
|
||||||
|
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = join(TEST_DIR, "opencode-global")
|
||||||
|
|
||||||
|
// given
|
||||||
|
const skillContent = `---
|
||||||
|
name: unique-test-skill
|
||||||
|
description: A unique skill for dedup test
|
||||||
|
---
|
||||||
|
Skill body.
|
||||||
|
`
|
||||||
|
createTestSkill("unique-test-skill", skillContent)
|
||||||
|
|
||||||
|
// when
|
||||||
|
const { discoverSkills } = await import("./loader")
|
||||||
|
process.chdir(TEST_DIR)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
||||||
|
|
||||||
|
// then
|
||||||
|
const names = skills.map(s => s.name)
|
||||||
|
const uniqueNames = [...new Set(names)]
|
||||||
|
expect(names.length).toBe(uniqueNames.length)
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
if (originalOpenCodeConfigDir === undefined) {
|
||||||
|
delete process.env.OPENCODE_CONFIG_DIR
|
||||||
|
} else {
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ async function loadSkillFromPath(
|
|||||||
skillPath: string,
|
skillPath: string,
|
||||||
resolvedPath: string,
|
resolvedPath: string,
|
||||||
defaultName: string,
|
defaultName: string,
|
||||||
scope: SkillScope
|
scope: SkillScope,
|
||||||
|
namePrefix: string = ""
|
||||||
): Promise<LoadedSkill | null> {
|
): Promise<LoadedSkill | null> {
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(skillPath, "utf-8")
|
const content = await fs.readFile(skillPath, "utf-8")
|
||||||
@@ -75,7 +76,10 @@ async function loadSkillFromPath(
|
|||||||
const mcpJsonMcp = await loadMcpJsonFromDir(resolvedPath)
|
const mcpJsonMcp = await loadMcpJsonFromDir(resolvedPath)
|
||||||
const mcpConfig = mcpJsonMcp || frontmatterMcp
|
const mcpConfig = mcpJsonMcp || frontmatterMcp
|
||||||
|
|
||||||
const skillName = data.name || defaultName
|
// For nested skills, use the full path as the name (e.g., "superpowers/brainstorming")
|
||||||
|
// For flat skills, use frontmatter name or directory name
|
||||||
|
const baseName = data.name || defaultName
|
||||||
|
const skillName = namePrefix ? `${namePrefix}/${baseName}` : baseName
|
||||||
const originalDescription = data.description || ""
|
const originalDescription = data.description || ""
|
||||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||||
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
const formattedDescription = `(${scope} - Skill) ${originalDescription}`
|
||||||
@@ -128,48 +132,67 @@ $ARGUMENTS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSkillsFromDir(skillsDir: string, scope: SkillScope): Promise<LoadedSkill[]> {
|
async function loadSkillsFromDir(
|
||||||
|
skillsDir: string,
|
||||||
|
scope: SkillScope,
|
||||||
|
namePrefix: string = "",
|
||||||
|
depth: number = 0,
|
||||||
|
maxDepth: number = 2
|
||||||
|
): Promise<LoadedSkill[]> {
|
||||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
|
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => [])
|
||||||
const skills: LoadedSkill[] = []
|
const skillMap = new Map<string, LoadedSkill>()
|
||||||
|
|
||||||
for (const entry of entries) {
|
const directories = entries.filter(e => !e.name.startsWith(".") && (e.isDirectory() || e.isSymbolicLink()))
|
||||||
if (entry.name.startsWith(".")) continue
|
const files = entries.filter(e => !e.name.startsWith(".") && !e.isDirectory() && !e.isSymbolicLink() && isMarkdownFile(e))
|
||||||
|
|
||||||
|
for (const entry of directories) {
|
||||||
const entryPath = join(skillsDir, entry.name)
|
const entryPath = join(skillsDir, entry.name)
|
||||||
|
const resolvedPath = await resolveSymlinkAsync(entryPath)
|
||||||
|
const dirName = entry.name
|
||||||
|
|
||||||
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
const skillMdPath = join(resolvedPath, "SKILL.md")
|
||||||
const resolvedPath = await resolveSymlinkAsync(entryPath)
|
try {
|
||||||
const dirName = entry.name
|
await fs.access(skillMdPath)
|
||||||
|
const skill = await loadSkillFromPath(skillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||||
const skillMdPath = join(resolvedPath, "SKILL.md")
|
if (skill && !skillMap.has(skill.name)) {
|
||||||
try {
|
skillMap.set(skill.name, skill)
|
||||||
await fs.access(skillMdPath)
|
|
||||||
const skill = await loadSkillFromPath(skillMdPath, resolvedPath, dirName, scope)
|
|
||||||
if (skill) skills.push(skill)
|
|
||||||
continue
|
|
||||||
} catch {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
|
||||||
try {
|
|
||||||
await fs.access(namedSkillMdPath)
|
|
||||||
const skill = await loadSkillFromPath(namedSkillMdPath, resolvedPath, dirName, scope)
|
|
||||||
if (skill) skills.push(skill)
|
|
||||||
continue
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
} catch {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMarkdownFile(entry)) {
|
const namedSkillMdPath = join(resolvedPath, `${dirName}.md`)
|
||||||
const skillName = basename(entry.name, ".md")
|
try {
|
||||||
const skill = await loadSkillFromPath(entryPath, skillsDir, skillName, scope)
|
await fs.access(namedSkillMdPath)
|
||||||
if (skill) skills.push(skill)
|
const skill = await loadSkillFromPath(namedSkillMdPath, resolvedPath, dirName, scope, namePrefix)
|
||||||
|
if (skill && !skillMap.has(skill.name)) {
|
||||||
|
skillMap.set(skill.name, skill)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth < maxDepth) {
|
||||||
|
const newPrefix = namePrefix ? `${namePrefix}/${dirName}` : dirName
|
||||||
|
const nestedSkills = await loadSkillsFromDir(resolvedPath, scope, newPrefix, depth + 1, maxDepth)
|
||||||
|
for (const nestedSkill of nestedSkills) {
|
||||||
|
if (!skillMap.has(nestedSkill.name)) {
|
||||||
|
skillMap.set(nestedSkill.name, nestedSkill)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return skills
|
for (const entry of files) {
|
||||||
|
const entryPath = join(skillsDir, entry.name)
|
||||||
|
const baseName = basename(entry.name, ".md")
|
||||||
|
const skill = await loadSkillFromPath(entryPath, skillsDir, baseName, scope, namePrefix)
|
||||||
|
if (skill && !skillMap.has(skill.name)) {
|
||||||
|
skillMap.set(skill.name, skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(skillMap.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition> {
|
function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition> {
|
||||||
@@ -210,15 +233,33 @@ export interface DiscoverSkillsOptions {
|
|||||||
includeClaudeCodePaths?: boolean
|
includeClaudeCodePaths?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplicates skills by name, keeping the first occurrence (higher priority).
|
||||||
|
* Priority order: opencode-project > opencode > project > user
|
||||||
|
* (OpenCode Global skills take precedence over legacy Claude project skills)
|
||||||
|
*/
|
||||||
|
function deduplicateSkills(skills: LoadedSkill[]): LoadedSkill[] {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const result: LoadedSkill[] = []
|
||||||
|
for (const skill of skills) {
|
||||||
|
if (!seen.has(skill.name)) {
|
||||||
|
seen.add(skill.name)
|
||||||
|
result.push(skill)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export async function discoverAllSkills(): Promise<LoadedSkill[]> {
|
export async function discoverAllSkills(): Promise<LoadedSkill[]> {
|
||||||
const [opencodeProjectSkills, projectSkills, opencodeGlobalSkills, userSkills] = await Promise.all([
|
const [opencodeProjectSkills, opencodeGlobalSkills, projectSkills, userSkills] = await Promise.all([
|
||||||
discoverOpencodeProjectSkills(),
|
discoverOpencodeProjectSkills(),
|
||||||
discoverProjectClaudeSkills(),
|
|
||||||
discoverOpencodeGlobalSkills(),
|
discoverOpencodeGlobalSkills(),
|
||||||
|
discoverProjectClaudeSkills(),
|
||||||
discoverUserClaudeSkills(),
|
discoverUserClaudeSkills(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return [...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]
|
// Priority: opencode-project > opencode > project > user
|
||||||
|
return deduplicateSkills([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
|
export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
|
||||||
@@ -230,7 +271,8 @@ export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promi
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (!includeClaudeCodePaths) {
|
if (!includeClaudeCodePaths) {
|
||||||
return [...opencodeProjectSkills, ...opencodeGlobalSkills]
|
// Priority: opencode-project > opencode
|
||||||
|
return deduplicateSkills([...opencodeProjectSkills, ...opencodeGlobalSkills])
|
||||||
}
|
}
|
||||||
|
|
||||||
const [projectSkills, userSkills] = await Promise.all([
|
const [projectSkills, userSkills] = await Promise.all([
|
||||||
@@ -238,7 +280,8 @@ export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promi
|
|||||||
discoverUserClaudeSkills(),
|
discoverUserClaudeSkills(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return [...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]
|
// Priority: opencode-project > opencode > project > user
|
||||||
|
return deduplicateSkills([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills])
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> {
|
export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> {
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
import { describe, it, expect } from "bun:test"
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
||||||
|
import { join } from "node:path"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
import { resolveSkillContent, resolveMultipleSkills, resolveSkillContentAsync, resolveMultipleSkillsAsync } from "./skill-content"
|
import { resolveSkillContent, resolveMultipleSkills, resolveSkillContentAsync, resolveMultipleSkillsAsync } from "./skill-content"
|
||||||
|
|
||||||
|
let originalEnv: Record<string, string | undefined>
|
||||||
|
let testConfigDir: string
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = {
|
||||||
|
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
||||||
|
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
||||||
|
}
|
||||||
|
const unique = `skill-content-test-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||||
|
testConfigDir = join(tmpdir(), unique)
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = testConfigDir
|
||||||
|
process.env.OPENCODE_CONFIG_DIR = testConfigDir
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const [key, value] of Object.entries(originalEnv)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
process.env[key] = value
|
||||||
|
} else {
|
||||||
|
delete process.env[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe("resolveSkillContent", () => {
|
describe("resolveSkillContent", () => {
|
||||||
it("should return template for existing skill", () => {
|
it("should return template for existing skill", () => {
|
||||||
// given: builtin skills with 'frontend-ui-ux' skill
|
// given: builtin skills with 'frontend-ui-ux' skill
|
||||||
@@ -33,10 +61,12 @@ describe("resolveSkillContent", () => {
|
|||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return null for empty string", () => {
|
it("should return null for disabled skill", () => {
|
||||||
// given: builtin skills
|
// given: frontend-ui-ux skill disabled
|
||||||
// when: resolving content for empty string
|
const options = { disabledSkills: new Set(["frontend-ui-ux"]) }
|
||||||
const result = resolveSkillContent("")
|
|
||||||
|
// when: resolving content for disabled skill
|
||||||
|
const result = resolveSkillContent("frontend-ui-ux", options)
|
||||||
|
|
||||||
// then: returns null
|
// then: returns null
|
||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
@@ -96,6 +126,20 @@ describe("resolveMultipleSkills", () => {
|
|||||||
expect(result.notFound).toEqual(["skill-one", "skill-two", "skill-three"])
|
expect(result.notFound).toEqual(["skill-one", "skill-two", "skill-three"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should treat disabled skills as not found", () => {
|
||||||
|
// #given: frontend-ui-ux disabled, playwright not disabled
|
||||||
|
const skillNames = ["frontend-ui-ux", "playwright"]
|
||||||
|
const options = { disabledSkills: new Set(["frontend-ui-ux"]) }
|
||||||
|
|
||||||
|
// #when: resolving multiple skills with disabled one
|
||||||
|
const result = resolveMultipleSkills(skillNames, options)
|
||||||
|
|
||||||
|
// #then: frontend-ui-ux in notFound, playwright resolved
|
||||||
|
expect(result.resolved.size).toBe(1)
|
||||||
|
expect(result.resolved.has("playwright")).toBe(true)
|
||||||
|
expect(result.notFound).toEqual(["frontend-ui-ux"])
|
||||||
|
})
|
||||||
|
|
||||||
it("should preserve skill order in resolved map", () => {
|
it("should preserve skill order in resolved map", () => {
|
||||||
// given: list of skill names in specific order
|
// given: list of skill names in specific order
|
||||||
const skillNames = ["playwright", "frontend-ui-ux"]
|
const skillNames = ["playwright", "frontend-ui-ux"]
|
||||||
@@ -111,21 +155,24 @@ describe("resolveMultipleSkills", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("resolveSkillContentAsync", () => {
|
describe("resolveSkillContentAsync", () => {
|
||||||
it("should return template for builtin skill", async () => {
|
it("should return template for builtin skill async", async () => {
|
||||||
// given: builtin skill 'frontend-ui-ux'
|
// given: builtin skill 'frontend-ui-ux'
|
||||||
// when: resolving content async
|
// when: resolving content async
|
||||||
const result = await resolveSkillContentAsync("frontend-ui-ux")
|
const options = { disabledSkills: new Set(["frontend-ui-ux"]) }
|
||||||
|
const result = await resolveSkillContentAsync("git-master", options)
|
||||||
|
|
||||||
// then: returns template string
|
// then: returns template string
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(typeof result).toBe("string")
|
expect(typeof result).toBe("string")
|
||||||
expect(result).toContain("Role: Designer-Turned-Developer")
|
expect(result).toContain("Git Master Agent")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return null for non-existent skill", async () => {
|
it("should return null for disabled skill async", async () => {
|
||||||
// given: non-existent skill name
|
// given: frontend-ui-ux disabled
|
||||||
// when: resolving content async
|
const options = { disabledSkills: new Set(["frontend-ui-ux"]) }
|
||||||
const result = await resolveSkillContentAsync("definitely-not-a-skill-12345")
|
|
||||||
|
// when: resolving content async for disabled skill
|
||||||
|
const result = await resolveSkillContentAsync("frontend-ui-ux", options)
|
||||||
|
|
||||||
// then: returns null
|
// then: returns null
|
||||||
expect(result).toBeNull()
|
expect(result).toBeNull()
|
||||||
@@ -133,9 +180,9 @@ describe("resolveSkillContentAsync", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("resolveMultipleSkillsAsync", () => {
|
describe("resolveMultipleSkillsAsync", () => {
|
||||||
it("should resolve builtin skills", async () => {
|
it("should resolve builtin skills async", async () => {
|
||||||
// given: builtin skill names
|
// given: builtin skill names
|
||||||
const skillNames = ["playwright", "frontend-ui-ux"]
|
const skillNames = ["playwright", "git-master"]
|
||||||
|
|
||||||
// when: resolving multiple skills async
|
// when: resolving multiple skills async
|
||||||
const result = await resolveMultipleSkillsAsync(skillNames)
|
const result = await resolveMultipleSkillsAsync(skillNames)
|
||||||
@@ -144,10 +191,10 @@ describe("resolveMultipleSkillsAsync", () => {
|
|||||||
expect(result.resolved.size).toBe(2)
|
expect(result.resolved.size).toBe(2)
|
||||||
expect(result.notFound).toEqual([])
|
expect(result.notFound).toEqual([])
|
||||||
expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation")
|
expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation")
|
||||||
expect(result.resolved.get("frontend-ui-ux")).toContain("Designer-Turned-Developer")
|
expect(result.resolved.get("git-master")).toContain("Git Master Agent")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle partial success with non-existent skills", async () => {
|
it("should handle partial success with non-existent skills async", async () => {
|
||||||
// given: mix of existing and non-existing skills
|
// given: mix of existing and non-existing skills
|
||||||
const skillNames = ["playwright", "nonexistent-skill-12345"]
|
const skillNames = ["playwright", "nonexistent-skill-12345"]
|
||||||
|
|
||||||
@@ -160,6 +207,20 @@ describe("resolveMultipleSkillsAsync", () => {
|
|||||||
expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation")
|
expect(result.resolved.get("playwright")).toContain("Playwright Browser Automation")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should treat disabled skills as not found async", async () => {
|
||||||
|
// #given: frontend-ui-ux disabled
|
||||||
|
const skillNames = ["frontend-ui-ux", "playwright"]
|
||||||
|
const options = { disabledSkills: new Set(["frontend-ui-ux"]) }
|
||||||
|
|
||||||
|
// #when: resolving multiple skills async with disabled one
|
||||||
|
const result = await resolveMultipleSkillsAsync(skillNames, options)
|
||||||
|
|
||||||
|
// #then: frontend-ui-ux in notFound, playwright resolved
|
||||||
|
expect(result.resolved.size).toBe(1)
|
||||||
|
expect(result.resolved.has("playwright")).toBe(true)
|
||||||
|
expect(result.notFound).toEqual(["frontend-ui-ux"])
|
||||||
|
})
|
||||||
|
|
||||||
it("should NOT inject watermark when both options are disabled", async () => {
|
it("should NOT inject watermark when both options are disabled", async () => {
|
||||||
// given: git-master skill with watermark disabled
|
// given: git-master skill with watermark disabled
|
||||||
const skillNames = ["git-master"]
|
const skillNames = ["git-master"]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { GitMasterConfig, BrowserAutomationProvider } from "../../config/sc
|
|||||||
export interface SkillResolutionOptions {
|
export interface SkillResolutionOptions {
|
||||||
gitMasterConfig?: GitMasterConfig
|
gitMasterConfig?: GitMasterConfig
|
||||||
browserProvider?: BrowserAutomationProvider
|
browserProvider?: BrowserAutomationProvider
|
||||||
|
disabledSkills?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedSkillsByProvider = new Map<string, LoadedSkill[]>()
|
const cachedSkillsByProvider = new Map<string, LoadedSkill[]>()
|
||||||
@@ -18,12 +19,22 @@ function clearSkillCache(): void {
|
|||||||
|
|
||||||
async function getAllSkills(options?: SkillResolutionOptions): Promise<LoadedSkill[]> {
|
async function getAllSkills(options?: SkillResolutionOptions): Promise<LoadedSkill[]> {
|
||||||
const cacheKey = options?.browserProvider ?? "playwright"
|
const cacheKey = options?.browserProvider ?? "playwright"
|
||||||
const cached = cachedSkillsByProvider.get(cacheKey)
|
const hasDisabledSkills = options?.disabledSkills && options.disabledSkills.size > 0
|
||||||
if (cached) return cached
|
|
||||||
|
// Skip cache if disabledSkills is provided (varies between calls)
|
||||||
|
if (!hasDisabledSkills) {
|
||||||
|
const cached = cachedSkillsByProvider.get(cacheKey)
|
||||||
|
if (cached) return cached
|
||||||
|
}
|
||||||
|
|
||||||
const [discoveredSkills, builtinSkillDefs] = await Promise.all([
|
const [discoveredSkills, builtinSkillDefs] = await Promise.all([
|
||||||
discoverSkills({ includeClaudeCodePaths: true }),
|
discoverSkills({ includeClaudeCodePaths: true }),
|
||||||
Promise.resolve(createBuiltinSkills({ browserProvider: options?.browserProvider })),
|
Promise.resolve(
|
||||||
|
createBuiltinSkills({
|
||||||
|
browserProvider: options?.browserProvider,
|
||||||
|
disabledSkills: options?.disabledSkills,
|
||||||
|
})
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
const builtinSkillsAsLoaded: LoadedSkill[] = builtinSkillDefs.map((skill) => ({
|
const builtinSkillsAsLoaded: LoadedSkill[] = builtinSkillDefs.map((skill) => ({
|
||||||
@@ -47,8 +58,15 @@ async function getAllSkills(options?: SkillResolutionOptions): Promise<LoadedSki
|
|||||||
const discoveredNames = new Set(discoveredSkills.map((s) => s.name))
|
const discoveredNames = new Set(discoveredSkills.map((s) => s.name))
|
||||||
const uniqueBuiltins = builtinSkillsAsLoaded.filter((s) => !discoveredNames.has(s.name))
|
const uniqueBuiltins = builtinSkillsAsLoaded.filter((s) => !discoveredNames.has(s.name))
|
||||||
|
|
||||||
const allSkills = [...discoveredSkills, ...uniqueBuiltins]
|
let allSkills = [...discoveredSkills, ...uniqueBuiltins]
|
||||||
cachedSkillsByProvider.set(cacheKey, allSkills)
|
|
||||||
|
// Filter discovered skills by disabledSkills (builtin skills are already filtered by createBuiltinSkills)
|
||||||
|
if (hasDisabledSkills) {
|
||||||
|
allSkills = allSkills.filter((s) => !options!.disabledSkills!.has(s.name))
|
||||||
|
} else {
|
||||||
|
cachedSkillsByProvider.set(cacheKey, allSkills)
|
||||||
|
}
|
||||||
|
|
||||||
return allSkills
|
return allSkills
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +140,10 @@ export function injectGitMasterConfig(template: string, config?: GitMasterConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSkillContent(skillName: string, options?: SkillResolutionOptions): string | null {
|
export function resolveSkillContent(skillName: string, options?: SkillResolutionOptions): string | null {
|
||||||
const skills = createBuiltinSkills({ browserProvider: options?.browserProvider })
|
const skills = createBuiltinSkills({
|
||||||
|
browserProvider: options?.browserProvider,
|
||||||
|
disabledSkills: options?.disabledSkills,
|
||||||
|
})
|
||||||
const skill = skills.find((s) => s.name === skillName)
|
const skill = skills.find((s) => s.name === skillName)
|
||||||
if (!skill) return null
|
if (!skill) return null
|
||||||
|
|
||||||
@@ -137,7 +158,10 @@ export function resolveMultipleSkills(skillNames: string[], options?: SkillResol
|
|||||||
resolved: Map<string, string>
|
resolved: Map<string, string>
|
||||||
notFound: string[]
|
notFound: string[]
|
||||||
} {
|
} {
|
||||||
const skills = createBuiltinSkills({ browserProvider: options?.browserProvider })
|
const skills = createBuiltinSkills({
|
||||||
|
browserProvider: options?.browserProvider,
|
||||||
|
disabledSkills: options?.disabledSkills,
|
||||||
|
})
|
||||||
const skillMap = new Map(skills.map((s) => [s.name, s.template]))
|
const skillMap = new Map(skills.map((s) => [s.name, s.template]))
|
||||||
|
|
||||||
const resolved = new Map<string, string>()
|
const resolved = new Map<string, string>()
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ describe("TaskToastManager", () => {
|
|||||||
description: "Task with inherited model",
|
description: "Task with inherited model",
|
||||||
agent: "sisyphus-junior",
|
agent: "sisyphus-junior",
|
||||||
isBackground: false,
|
isBackground: false,
|
||||||
modelInfo: { model: "cliproxy/claude-opus-4-5", type: "inherited" as const },
|
modelInfo: { model: "cliproxy/claude-opus-4-6", type: "inherited" as const },
|
||||||
}
|
}
|
||||||
|
|
||||||
// when - addTask is called
|
// when - addTask is called
|
||||||
@@ -202,7 +202,7 @@ describe("TaskToastManager", () => {
|
|||||||
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
expect(mockClient.tui.showToast).toHaveBeenCalled()
|
||||||
const call = mockClient.tui.showToast.mock.calls[0][0]
|
const call = mockClient.tui.showToast.mock.calls[0][0]
|
||||||
expect(call.body.message).toContain("[FALLBACK]")
|
expect(call.body.message).toContain("[FALLBACK]")
|
||||||
expect(call.body.message).toContain("cliproxy/claude-opus-4-5")
|
expect(call.body.message).toContain("cliproxy/claude-opus-4-6")
|
||||||
expect(call.body.message).toContain("(inherited from parent)")
|
expect(call.body.message).toContain("(inherited from parent)")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
|
|
||||||
34 lifecycle hooks intercepting/modifying agent behavior across 5 events.
|
40+ lifecycle hooks intercepting/modifying agent behavior across 5 events.
|
||||||
|
|
||||||
**Event Types**:
|
**Event Types**:
|
||||||
- `UserPromptSubmit` (`chat.message`) - Can block
|
- `UserPromptSubmit` (`chat.message`) - Can block
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
```
|
```
|
||||||
hooks/
|
hooks/
|
||||||
├── atlas/ # Main orchestration (757 lines)
|
├── atlas/ # Main orchestration (770 lines)
|
||||||
├── anthropic-context-window-limit-recovery/ # Auto-summarize
|
├── anthropic-context-window-limit-recovery/ # Auto-summarize
|
||||||
├── todo-continuation-enforcer.ts # Force TODO completion
|
├── todo-continuation-enforcer.ts # Force TODO completion (517 lines)
|
||||||
├── ralph-loop/ # Self-referential dev loop
|
├── ralph-loop/ # Self-referential dev loop (428 lines)
|
||||||
├── claude-code-hooks/ # settings.json compat layer - see AGENTS.md
|
├── claude-code-hooks/ # settings.json compat layer - see AGENTS.md
|
||||||
├── comment-checker/ # Prevents AI slop
|
├── comment-checker/ # Prevents AI slop
|
||||||
├── auto-slash-command/ # Detects /command patterns
|
├── auto-slash-command/ # Detects /command patterns
|
||||||
@@ -27,13 +27,14 @@ hooks/
|
|||||||
├── edit-error-recovery/ # Recovers from failures
|
├── edit-error-recovery/ # Recovers from failures
|
||||||
├── thinking-block-validator/ # Ensures valid <thinking>
|
├── thinking-block-validator/ # Ensures valid <thinking>
|
||||||
├── context-window-monitor.ts # Reminds of headroom
|
├── context-window-monitor.ts # Reminds of headroom
|
||||||
├── session-recovery/ # Auto-recovers from crashes
|
├── session-recovery/ # Auto-recovers from crashes (436 lines)
|
||||||
|
├── session-notification.ts # Session event notifications (337 lines)
|
||||||
├── think-mode/ # Dynamic thinking budget
|
├── think-mode/ # Dynamic thinking budget
|
||||||
├── keyword-detector/ # ultrawork/search/analyze modes
|
├── keyword-detector/ # ultrawork/search/analyze modes
|
||||||
├── background-notification/ # OS notification
|
├── background-notification/ # OS notification
|
||||||
├── prometheus-md-only/ # Planner read-only mode
|
├── prometheus-md-only/ # Planner read-only mode
|
||||||
├── agent-usage-reminder/ # Specialized agent hints
|
├── agent-usage-reminder/ # Specialized agent hints
|
||||||
├── auto-update-checker/ # Plugin update check
|
├── auto-update-checker/ # Plugin update check (304 lines)
|
||||||
├── tool-output-truncator.ts # Prevents context bloat
|
├── tool-output-truncator.ts # Prevents context bloat
|
||||||
├── compaction-context-injector/ # Injects context on compaction
|
├── compaction-context-injector/ # Injects context on compaction
|
||||||
├── delegate-task-retry/ # Retries failed delegations
|
├── delegate-task-retry/ # Retries failed delegations
|
||||||
@@ -47,6 +48,11 @@ hooks/
|
|||||||
├── sisyphus-junior-notepad/ # Sisyphus Junior notepad
|
├── sisyphus-junior-notepad/ # Sisyphus Junior notepad
|
||||||
├── stop-continuation-guard/ # Guards stop continuation
|
├── stop-continuation-guard/ # Guards stop continuation
|
||||||
├── subagent-question-blocker/ # Blocks subagent questions
|
├── subagent-question-blocker/ # Blocks subagent questions
|
||||||
|
├── task-reminder/ # Task progress reminders
|
||||||
|
├── tasks-todowrite-disabler/ # Disables TodoWrite when task system active
|
||||||
|
├── unstable-agent-babysitter/ # Monitors unstable agent behavior
|
||||||
|
├── write-existing-file-guard/ # Guards against overwriting existing files
|
||||||
|
├── preemptive-compaction.ts # Preemptive context compaction
|
||||||
└── index.ts # Hook aggregation + registration
|
└── index.ts # Hook aggregation + registration
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,8 +67,8 @@ hooks/
|
|||||||
|
|
||||||
## EXECUTION ORDER
|
## EXECUTION ORDER
|
||||||
- **UserPromptSubmit**: keywordDetector → claudeCodeHooks → autoSlashCommand → startWork
|
- **UserPromptSubmit**: keywordDetector → claudeCodeHooks → autoSlashCommand → startWork
|
||||||
- **PreToolUse**: subagentQuestionBlocker → questionLabelTruncator → claudeCodeHooks → nonInteractiveEnv → commentChecker → directoryAgentsInjector → directoryReadmeInjector → rulesInjector → prometheusMdOnly → sisyphusJuniorNotepad → atlasHook
|
- **PreToolUse**: subagentQuestionBlocker → questionLabelTruncator → claudeCodeHooks → nonInteractiveEnv → commentChecker → directoryAgentsInjector → directoryReadmeInjector → rulesInjector → prometheusMdOnly → sisyphusJuniorNotepad → writeExistingFileGuard → atlasHook
|
||||||
- **PostToolUse**: claudeCodeHooks → toolOutputTruncator → contextWindowMonitor → commentChecker → directoryAgentsInjector → directoryReadmeInjector → rulesInjector → emptyTaskResponseDetector → agentUsageReminder → interactiveBashSession → editErrorRecovery → delegateTaskRetry → atlasHook → taskResumeInfo
|
- **PostToolUse**: claudeCodeHooks → toolOutputTruncator → contextWindowMonitor → commentChecker → directoryAgentsInjector → directoryReadmeInjector → rulesInjector → emptyTaskResponseDetector → agentUsageReminder → interactiveBashSession → editErrorRecovery → delegateTaskRetry → atlasHook → taskResumeInfo → taskReminder
|
||||||
|
|
||||||
## HOW TO ADD
|
## HOW TO ADD
|
||||||
1. Create `src/hooks/name/` with `index.ts` exporting `createMyHook(ctx)`
|
1. Create `src/hooks/name/` with `index.ts` exporting `createMyHook(ctx)`
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ describe("executeCompact lock management", () => {
|
|||||||
let fakeTimeouts: FakeTimeouts
|
let fakeTimeouts: FakeTimeouts
|
||||||
const sessionID = "test-session-123"
|
const sessionID = "test-session-123"
|
||||||
const directory = "/test/dir"
|
const directory = "/test/dir"
|
||||||
const msg = { providerID: "anthropic", modelID: "claude-opus-4-5" }
|
const msg = { providerID: "anthropic", modelID: "claude-opus-4-6" }
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// given: Fresh state for each test
|
// given: Fresh state for each test
|
||||||
@@ -332,7 +332,7 @@ describe("executeCompact lock management", () => {
|
|||||||
expect(mockClient.session.summarize).toHaveBeenCalledWith(
|
expect(mockClient.session.summarize).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: { providerID: "anthropic", modelID: "claude-opus-4-5", auto: true },
|
body: { providerID: "anthropic", modelID: "claude-opus-4-6", auto: true },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe("atlas hook", () => {
|
|||||||
}
|
}
|
||||||
const messageData = {
|
const messageData = {
|
||||||
agent,
|
agent,
|
||||||
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
|
||||||
}
|
}
|
||||||
writeFileSync(join(messageDir, "msg_test001.json"), JSON.stringify(messageData))
|
writeFileSync(join(messageDir, "msg_test001.json"), JSON.stringify(messageData))
|
||||||
}
|
}
|
||||||
@@ -624,6 +624,11 @@ describe("atlas hook", () => {
|
|||||||
describe("session.idle handler (boulder continuation)", () => {
|
describe("session.idle handler (boulder continuation)", () => {
|
||||||
const MAIN_SESSION_ID = "main-session-123"
|
const MAIN_SESSION_ID = "main-session-123"
|
||||||
|
|
||||||
|
async function flushMicrotasks(): Promise<void> {
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mock.module("../../features/claude-code-session-state", () => ({
|
mock.module("../../features/claude-code-session-state", () => ({
|
||||||
getMainSessionID: () => MAIN_SESSION_ID,
|
getMainSessionID: () => MAIN_SESSION_ID,
|
||||||
@@ -858,8 +863,8 @@ describe("atlas hook", () => {
|
|||||||
expect(callArgs.body.parts[0].text).toContain("2 remaining")
|
expect(callArgs.body.parts[0].text).toContain("2 remaining")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should not inject when last agent is not Atlas", async () => {
|
test("should not inject when last agent does not match boulder agent", async () => {
|
||||||
// given - boulder state with incomplete plan, but last agent is NOT Atlas
|
// given - boulder state with incomplete plan, but last agent does NOT match
|
||||||
const planPath = join(TEST_DIR, "test-plan.md")
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
||||||
|
|
||||||
@@ -868,10 +873,11 @@ describe("atlas hook", () => {
|
|||||||
started_at: "2026-01-02T10:00:00Z",
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
session_ids: [MAIN_SESSION_ID],
|
session_ids: [MAIN_SESSION_ID],
|
||||||
plan_name: "test-plan",
|
plan_name: "test-plan",
|
||||||
|
agent: "atlas",
|
||||||
}
|
}
|
||||||
writeBoulderState(TEST_DIR, state)
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
// given - last agent is NOT Atlas
|
// given - last agent is NOT the boulder agent
|
||||||
cleanupMessageStorage(MAIN_SESSION_ID)
|
cleanupMessageStorage(MAIN_SESSION_ID)
|
||||||
setupMessageStorage(MAIN_SESSION_ID, "sisyphus")
|
setupMessageStorage(MAIN_SESSION_ID, "sisyphus")
|
||||||
|
|
||||||
@@ -886,10 +892,44 @@ describe("atlas hook", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// then - should NOT call prompt because agent is not Atlas
|
// then - should NOT call prompt because agent does not match
|
||||||
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
expect(mockInput._promptMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should inject when last agent matches boulder agent even if non-Atlas", async () => {
|
||||||
|
// given - boulder state expects sisyphus and last agent is sisyphus
|
||||||
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
||||||
|
|
||||||
|
const state: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [MAIN_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
agent: "sisyphus",
|
||||||
|
}
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
|
cleanupMessageStorage(MAIN_SESSION_ID)
|
||||||
|
setupMessageStorage(MAIN_SESSION_ID, "sisyphus")
|
||||||
|
|
||||||
|
const mockInput = createMockPluginInput()
|
||||||
|
const hook = createAtlasHook(mockInput)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook.handler({
|
||||||
|
event: {
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: MAIN_SESSION_ID },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// then - should call prompt for sisyphus
|
||||||
|
expect(mockInput._promptMock).toHaveBeenCalled()
|
||||||
|
const callArgs = mockInput._promptMock.mock.calls[0][0]
|
||||||
|
expect(callArgs.body.agent).toBe("sisyphus")
|
||||||
|
})
|
||||||
|
|
||||||
test("should debounce rapid continuation injections (prevent infinite loop)", async () => {
|
test("should debounce rapid continuation injections (prevent infinite loop)", async () => {
|
||||||
// given - boulder state with incomplete plan
|
// given - boulder state with incomplete plan
|
||||||
const planPath = join(TEST_DIR, "test-plan.md")
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
@@ -930,6 +970,135 @@ describe("atlas hook", () => {
|
|||||||
expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
|
expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should stop continuation after 2 consecutive prompt failures (issue #1355)", async () => {
|
||||||
|
//#given - boulder state with incomplete plan and prompt always fails
|
||||||
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
||||||
|
|
||||||
|
const state: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [MAIN_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
}
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
|
const promptMock = mock(() => Promise.reject(new Error("Bad Request")))
|
||||||
|
const mockInput = createMockPluginInput({ promptMock })
|
||||||
|
const hook = createAtlasHook(mockInput)
|
||||||
|
|
||||||
|
const originalDateNow = Date.now
|
||||||
|
let now = 0
|
||||||
|
Date.now = () => now
|
||||||
|
|
||||||
|
try {
|
||||||
|
//#when - idle fires repeatedly, past cooldown each time
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
now += 6000
|
||||||
|
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
now += 6000
|
||||||
|
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
|
||||||
|
//#then - should attempt only twice, then disable continuation
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(2)
|
||||||
|
} finally {
|
||||||
|
Date.now = originalDateNow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should reset prompt failure counter on success and only stop after 2 consecutive failures", async () => {
|
||||||
|
//#given - boulder state with incomplete plan
|
||||||
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
||||||
|
|
||||||
|
const state: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [MAIN_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
}
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
|
const promptMock = mock(() => Promise.resolve())
|
||||||
|
promptMock.mockImplementationOnce(() => Promise.reject(new Error("Bad Request")))
|
||||||
|
promptMock.mockImplementationOnce(() => Promise.resolve())
|
||||||
|
promptMock.mockImplementationOnce(() => Promise.reject(new Error("Bad Request")))
|
||||||
|
promptMock.mockImplementationOnce(() => Promise.reject(new Error("Bad Request")))
|
||||||
|
|
||||||
|
const mockInput = createMockPluginInput({ promptMock })
|
||||||
|
const hook = createAtlasHook(mockInput)
|
||||||
|
|
||||||
|
const originalDateNow = Date.now
|
||||||
|
let now = 0
|
||||||
|
Date.now = () => now
|
||||||
|
|
||||||
|
try {
|
||||||
|
//#when - fail, succeed (reset), then fail twice (disable), then attempt again
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
now += 6000
|
||||||
|
}
|
||||||
|
|
||||||
|
//#then - 4 prompt attempts; 5th idle is skipped after 2 consecutive failures
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(4)
|
||||||
|
} finally {
|
||||||
|
Date.now = originalDateNow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should reset continuation failure state on session.compacted event", async () => {
|
||||||
|
//#given - boulder state with incomplete plan and prompt always fails
|
||||||
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
|
||||||
|
|
||||||
|
const state: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [MAIN_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
}
|
||||||
|
writeBoulderState(TEST_DIR, state)
|
||||||
|
|
||||||
|
const promptMock = mock(() => Promise.reject(new Error("Bad Request")))
|
||||||
|
const mockInput = createMockPluginInput({ promptMock })
|
||||||
|
const hook = createAtlasHook(mockInput)
|
||||||
|
|
||||||
|
const originalDateNow = Date.now
|
||||||
|
let now = 0
|
||||||
|
Date.now = () => now
|
||||||
|
|
||||||
|
try {
|
||||||
|
//#when - two failures disables continuation, then compaction resets it
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
now += 6000
|
||||||
|
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
now += 6000
|
||||||
|
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
|
||||||
|
await hook.handler({ event: { type: "session.compacted", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
now += 6000
|
||||||
|
|
||||||
|
await hook.handler({ event: { type: "session.idle", properties: { sessionID: MAIN_SESSION_ID } } })
|
||||||
|
await flushMicrotasks()
|
||||||
|
|
||||||
|
//#then - 2 attempts + 1 after compaction (3 total)
|
||||||
|
expect(promptMock).toHaveBeenCalledTimes(3)
|
||||||
|
} finally {
|
||||||
|
Date.now = originalDateNow
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("should cleanup on session.deleted", async () => {
|
test("should cleanup on session.deleted", async () => {
|
||||||
// given - boulder state
|
// given - boulder state
|
||||||
const planPath = join(TEST_DIR, "test-plan.md")
|
const planPath = join(TEST_DIR, "test-plan.md")
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ function isSisyphusPath(filePath: string): boolean {
|
|||||||
|
|
||||||
const WRITE_EDIT_TOOLS = ["Write", "Edit", "write", "edit"]
|
const WRITE_EDIT_TOOLS = ["Write", "Edit", "write", "edit"]
|
||||||
|
|
||||||
|
function getLastAgentFromSession(sessionID: string): string | null {
|
||||||
|
const messageDir = getMessageDir(sessionID)
|
||||||
|
if (!messageDir) return null
|
||||||
|
const nearest = findNearestMessageWithFields(messageDir)
|
||||||
|
return nearest?.agent?.toLowerCase() ?? null
|
||||||
|
}
|
||||||
|
|
||||||
const DIRECT_WORK_REMINDER = `
|
const DIRECT_WORK_REMINDER = `
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -384,6 +391,7 @@ interface ToolExecuteAfterOutput {
|
|||||||
interface SessionState {
|
interface SessionState {
|
||||||
lastEventWasAbortError?: boolean
|
lastEventWasAbortError?: boolean
|
||||||
lastContinuationInjectedAt?: number
|
lastContinuationInjectedAt?: number
|
||||||
|
promptFailureCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTINUATION_COOLDOWN_MS = 5000
|
const CONTINUATION_COOLDOWN_MS = 5000
|
||||||
@@ -425,13 +433,14 @@ export function createAtlasHook(
|
|||||||
function getState(sessionID: string): SessionState {
|
function getState(sessionID: string): SessionState {
|
||||||
let state = sessions.get(sessionID)
|
let state = sessions.get(sessionID)
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = {}
|
state = { promptFailureCount: 0 }
|
||||||
sessions.set(sessionID, state)
|
sessions.set(sessionID, state)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
async function injectContinuation(sessionID: string, planName: string, remaining: number, total: number): Promise<void> {
|
async function injectContinuation(sessionID: string, planName: string, remaining: number, total: number, agent?: string): Promise<void> {
|
||||||
|
const state = getState(sessionID)
|
||||||
const hasRunningBgTasks = backgroundManager
|
const hasRunningBgTasks = backgroundManager
|
||||||
? backgroundManager.getTasksByParentSession(sessionID).some(t => t.status === "running")
|
? backgroundManager.getTasksByParentSession(sessionID).some(t => t.status === "running")
|
||||||
: false
|
: false
|
||||||
@@ -474,21 +483,28 @@ export function createAtlasHook(
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.client.session.prompt({
|
await ctx.client.session.prompt({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: {
|
body: {
|
||||||
agent: "atlas",
|
agent: agent ?? "atlas",
|
||||||
...(model !== undefined ? { model } : {}),
|
...(model !== undefined ? { model } : {}),
|
||||||
parts: [{ type: "text", text: prompt }],
|
parts: [{ type: "text", text: prompt }],
|
||||||
},
|
},
|
||||||
query: { directory: ctx.directory },
|
query: { directory: ctx.directory },
|
||||||
})
|
})
|
||||||
|
|
||||||
log(`[${HOOK_NAME}] Boulder continuation injected`, { sessionID })
|
state.promptFailureCount = 0
|
||||||
} catch (err) {
|
|
||||||
log(`[${HOOK_NAME}] Boulder continuation failed`, { sessionID, error: String(err) })
|
log(`[${HOOK_NAME}] Boulder continuation injected`, { sessionID })
|
||||||
}
|
} catch (err) {
|
||||||
}
|
state.promptFailureCount += 1
|
||||||
|
log(`[${HOOK_NAME}] Boulder continuation failed`, {
|
||||||
|
sessionID,
|
||||||
|
error: String(err),
|
||||||
|
promptFailureCount: state.promptFailureCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handler: async ({ event }: { event: { type: string; properties?: unknown } }): Promise<void> => {
|
handler: async ({ event }: { event: { type: string; properties?: unknown } }): Promise<void> => {
|
||||||
@@ -534,6 +550,14 @@ export function createAtlasHook(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.promptFailureCount >= 2) {
|
||||||
|
log(`[${HOOK_NAME}] Skipped: continuation disabled after repeated prompt failures`, {
|
||||||
|
sessionID,
|
||||||
|
promptFailureCount: state.promptFailureCount,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const hasRunningBgTasks = backgroundManager
|
const hasRunningBgTasks = backgroundManager
|
||||||
? backgroundManager.getTasksByParentSession(sessionID).some(t => t.status === "running")
|
? backgroundManager.getTasksByParentSession(sessionID).some(t => t.status === "running")
|
||||||
: false
|
: false
|
||||||
@@ -549,8 +573,14 @@ export function createAtlasHook(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isCallerOrchestrator(sessionID)) {
|
const requiredAgent = (boulderState.agent ?? "atlas").toLowerCase()
|
||||||
log(`[${HOOK_NAME}] Skipped: last agent is not Atlas`, { sessionID })
|
const lastAgent = getLastAgentFromSession(sessionID)
|
||||||
|
if (!lastAgent || lastAgent !== requiredAgent) {
|
||||||
|
log(`[${HOOK_NAME}] Skipped: last agent does not match boulder agent`, {
|
||||||
|
sessionID,
|
||||||
|
lastAgent: lastAgent ?? "unknown",
|
||||||
|
requiredAgent,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +598,7 @@ export function createAtlasHook(
|
|||||||
|
|
||||||
state.lastContinuationInjectedAt = now
|
state.lastContinuationInjectedAt = now
|
||||||
const remaining = progress.total - progress.completed
|
const remaining = progress.total - progress.completed
|
||||||
injectContinuation(sessionID, boulderState.plan_name, remaining, progress.total)
|
injectContinuation(sessionID, boulderState.plan_name, remaining, progress.total, boulderState.agent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +648,17 @@ export function createAtlasHook(
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.type === "session.compacted") {
|
||||||
|
const sessionID = (props?.sessionID ?? (props?.info as { id?: string } | undefined)?.id) as
|
||||||
|
| string
|
||||||
|
| undefined
|
||||||
|
if (sessionID) {
|
||||||
|
sessions.delete(sessionID)
|
||||||
|
log(`[${HOOK_NAME}] Session compacted: cleaned up`, { sessionID })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"tool.execute.before": async (
|
"tool.execute.before": async (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as fs from "node:fs"
|
import * as fs from "node:fs"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import { CACHE_DIR, PACKAGE_NAME } from "./constants"
|
import { PACKAGE_NAME, USER_CONFIG_DIR } from "./constants"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
interface BunLockfile {
|
interface BunLockfile {
|
||||||
@@ -17,7 +17,7 @@ function stripTrailingCommas(json: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeFromBunLock(packageName: string): boolean {
|
function removeFromBunLock(packageName: string): boolean {
|
||||||
const lockPath = path.join(CACHE_DIR, "bun.lock")
|
const lockPath = path.join(USER_CONFIG_DIR, "bun.lock")
|
||||||
if (!fs.existsSync(lockPath)) return false
|
if (!fs.existsSync(lockPath)) return false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -48,8 +48,8 @@ function removeFromBunLock(packageName: string): boolean {
|
|||||||
|
|
||||||
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
|
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
|
||||||
try {
|
try {
|
||||||
const pkgDir = path.join(CACHE_DIR, "node_modules", packageName)
|
const pkgDir = path.join(USER_CONFIG_DIR, "node_modules", packageName)
|
||||||
const pkgJsonPath = path.join(CACHE_DIR, "package.json")
|
const pkgJsonPath = path.join(USER_CONFIG_DIR, "package.json")
|
||||||
|
|
||||||
let packageRemoved = false
|
let packageRemoved = false
|
||||||
let dependencyRemoved = false
|
let dependencyRemoved = false
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ function getCacheDir(): string {
|
|||||||
|
|
||||||
export const CACHE_DIR = getCacheDir()
|
export const CACHE_DIR = getCacheDir()
|
||||||
export const VERSION_FILE = path.join(CACHE_DIR, "version")
|
export const VERSION_FILE = path.join(CACHE_DIR, "version")
|
||||||
export const INSTALLED_PACKAGE_JSON = path.join(
|
|
||||||
CACHE_DIR,
|
|
||||||
"node_modules",
|
|
||||||
PACKAGE_NAME,
|
|
||||||
"package.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
export function getWindowsAppdataDir(): string | null {
|
export function getWindowsAppdataDir(): string | null {
|
||||||
if (process.platform !== "win32") return null
|
if (process.platform !== "win32") return null
|
||||||
@@ -31,3 +25,10 @@ export function getWindowsAppdataDir(): string | null {
|
|||||||
export const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
|
export const USER_CONFIG_DIR = getOpenCodeConfigDir({ binary: "opencode" })
|
||||||
export const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode.json")
|
export const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode.json")
|
||||||
export const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode.jsonc")
|
export const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode.jsonc")
|
||||||
|
|
||||||
|
export const INSTALLED_PACKAGE_JSON = path.join(
|
||||||
|
USER_CONFIG_DIR,
|
||||||
|
"node_modules",
|
||||||
|
PACKAGE_NAME,
|
||||||
|
"package.json"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test"
|
||||||
import { createCategorySkillReminderHook } from "./index"
|
import { createCategorySkillReminderHook } from "./index"
|
||||||
import { updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state"
|
import { updateSessionAgent, clearSessionAgent, _resetForTesting } from "../../features/claude-code-session-state"
|
||||||
|
import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder"
|
||||||
import * as sharedModule from "../../shared"
|
import * as sharedModule from "../../shared"
|
||||||
|
|
||||||
describe("category-skill-reminder hook", () => {
|
describe("category-skill-reminder hook", () => {
|
||||||
@@ -29,10 +30,14 @@ describe("category-skill-reminder hook", () => {
|
|||||||
} as any
|
} as any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createHook(availableSkills: AvailableSkill[] = []) {
|
||||||
|
return createCategorySkillReminderHook(createMockPluginInput(), availableSkills)
|
||||||
|
}
|
||||||
|
|
||||||
describe("target agent detection", () => {
|
describe("target agent detection", () => {
|
||||||
test("should inject reminder for sisyphus agent after 3 tool calls", async () => {
|
test("should inject reminder for sisyphus agent after 3 tool calls", async () => {
|
||||||
// given - sisyphus agent session with multiple tool calls
|
// given - sisyphus agent session with multiple tool calls
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "sisyphus-session"
|
const sessionID = "sisyphus-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should inject reminder for atlas agent", async () => {
|
test("should inject reminder for atlas agent", async () => {
|
||||||
// given - atlas agent session
|
// given - atlas agent session
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "atlas-session"
|
const sessionID = "atlas-session"
|
||||||
updateSessionAgent(sessionID, "Atlas")
|
updateSessionAgent(sessionID, "Atlas")
|
||||||
|
|
||||||
@@ -71,7 +76,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should inject reminder for sisyphus-junior agent", async () => {
|
test("should inject reminder for sisyphus-junior agent", async () => {
|
||||||
// given - sisyphus-junior agent session
|
// given - sisyphus-junior agent session
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "junior-session"
|
const sessionID = "junior-session"
|
||||||
updateSessionAgent(sessionID, "sisyphus-junior")
|
updateSessionAgent(sessionID, "sisyphus-junior")
|
||||||
|
|
||||||
@@ -90,7 +95,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should NOT inject reminder for non-target agents", async () => {
|
test("should NOT inject reminder for non-target agents", async () => {
|
||||||
// given - librarian agent session (not a target)
|
// given - librarian agent session (not a target)
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "librarian-session"
|
const sessionID = "librarian-session"
|
||||||
updateSessionAgent(sessionID, "librarian")
|
updateSessionAgent(sessionID, "librarian")
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should detect agent from input.agent when session state is empty", async () => {
|
test("should detect agent from input.agent when session state is empty", async () => {
|
||||||
// given - no session state, agent provided in input
|
// given - no session state, agent provided in input
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "input-agent-session"
|
const sessionID = "input-agent-session"
|
||||||
|
|
||||||
const output = { title: "", output: "result", metadata: {} }
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
@@ -127,7 +132,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
describe("delegation tool tracking", () => {
|
describe("delegation tool tracking", () => {
|
||||||
test("should NOT inject reminder if delegate_task is used", async () => {
|
test("should NOT inject reminder if delegate_task is used", async () => {
|
||||||
// given - sisyphus agent that uses delegate_task
|
// given - sisyphus agent that uses delegate_task
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "delegation-session"
|
const sessionID = "delegation-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -147,7 +152,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should NOT inject reminder if call_omo_agent is used", async () => {
|
test("should NOT inject reminder if call_omo_agent is used", async () => {
|
||||||
// given - sisyphus agent that uses call_omo_agent
|
// given - sisyphus agent that uses call_omo_agent
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "omo-agent-session"
|
const sessionID = "omo-agent-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -167,7 +172,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should NOT inject reminder if task tool is used", async () => {
|
test("should NOT inject reminder if task tool is used", async () => {
|
||||||
// given - sisyphus agent that uses task tool
|
// given - sisyphus agent that uses task tool
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "task-session"
|
const sessionID = "task-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -189,7 +194,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
describe("tool call counting", () => {
|
describe("tool call counting", () => {
|
||||||
test("should NOT inject reminder before 3 tool calls", async () => {
|
test("should NOT inject reminder before 3 tool calls", async () => {
|
||||||
// given - sisyphus agent with only 2 tool calls
|
// given - sisyphus agent with only 2 tool calls
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "few-calls-session"
|
const sessionID = "few-calls-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -207,7 +212,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should only inject reminder once per session", async () => {
|
test("should only inject reminder once per session", async () => {
|
||||||
// given - sisyphus agent session
|
// given - sisyphus agent session
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "once-session"
|
const sessionID = "once-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -231,7 +236,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should only count delegatable work tools", async () => {
|
test("should only count delegatable work tools", async () => {
|
||||||
// given - sisyphus agent with mixed tool calls
|
// given - sisyphus agent with mixed tool calls
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "mixed-tools-session"
|
const sessionID = "mixed-tools-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -252,7 +257,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
describe("event handling", () => {
|
describe("event handling", () => {
|
||||||
test("should reset state on session.deleted event", async () => {
|
test("should reset state on session.deleted event", async () => {
|
||||||
// given - sisyphus agent with reminder already shown
|
// given - sisyphus agent with reminder already shown
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "delete-session"
|
const sessionID = "delete-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -278,7 +283,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should reset state on session.compacted event", async () => {
|
test("should reset state on session.compacted event", async () => {
|
||||||
// given - sisyphus agent with reminder already shown
|
// given - sisyphus agent with reminder already shown
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "compact-session"
|
const sessionID = "compact-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -306,7 +311,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
describe("case insensitivity", () => {
|
describe("case insensitivity", () => {
|
||||||
test("should handle tool names case-insensitively", async () => {
|
test("should handle tool names case-insensitively", async () => {
|
||||||
// given - sisyphus agent with mixed case tool names
|
// given - sisyphus agent with mixed case tool names
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "case-session"
|
const sessionID = "case-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -325,7 +330,7 @@ describe("category-skill-reminder hook", () => {
|
|||||||
|
|
||||||
test("should handle delegation tool names case-insensitively", async () => {
|
test("should handle delegation tool names case-insensitively", async () => {
|
||||||
// given - sisyphus agent using DELEGATE_TASK in uppercase
|
// given - sisyphus agent using DELEGATE_TASK in uppercase
|
||||||
const hook = createCategorySkillReminderHook(createMockPluginInput())
|
const hook = createHook()
|
||||||
const sessionID = "case-delegate-session"
|
const sessionID = "case-delegate-session"
|
||||||
updateSessionAgent(sessionID, "Sisyphus")
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
|
||||||
@@ -343,4 +348,71 @@ describe("category-skill-reminder hook", () => {
|
|||||||
clearSessionAgent(sessionID)
|
clearSessionAgent(sessionID)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("dynamic skills reminder message", () => {
|
||||||
|
test("shows built-in skills when only built-in skills are available", async () => {
|
||||||
|
// given
|
||||||
|
const availableSkills: AvailableSkill[] = [
|
||||||
|
{ name: "frontend-ui-ux", description: "Frontend UI/UX work", location: "plugin" },
|
||||||
|
{ name: "git-master", description: "Git operations", location: "plugin" },
|
||||||
|
{ name: "playwright", description: "Browser automation", location: "plugin" },
|
||||||
|
]
|
||||||
|
const hook = createHook(availableSkills)
|
||||||
|
const sessionID = "builtins-only"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "edit", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(output.output).toContain("**Built-in**:")
|
||||||
|
expect(output.output).toContain("frontend-ui-ux")
|
||||||
|
expect(output.output).toContain("**⚡ YOUR SKILLS (PRIORITY)**")
|
||||||
|
expect(output.output).toContain("load_skills=[\"frontend-ui-ux\"")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("emphasizes user skills with PRIORITY and uses first user skill in example", async () => {
|
||||||
|
// given
|
||||||
|
const availableSkills: AvailableSkill[] = [
|
||||||
|
{ name: "frontend-ui-ux", description: "Frontend UI/UX work", location: "plugin" },
|
||||||
|
{ name: "react-19", description: "React 19 expertise", location: "user" },
|
||||||
|
{ name: "web-designer", description: "Visual design", location: "user" },
|
||||||
|
]
|
||||||
|
const hook = createHook(availableSkills)
|
||||||
|
const sessionID = "user-skills"
|
||||||
|
updateSessionAgent(sessionID, "Atlas")
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "bash", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(output.output).toContain("**⚡ YOUR SKILLS (PRIORITY)**")
|
||||||
|
expect(output.output).toContain("react-19")
|
||||||
|
expect(output.output).toContain("> User-installed skills OVERRIDE")
|
||||||
|
expect(output.output).toContain("load_skills=[\"react-19\"")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("still injects a generic reminder when no skills are provided", async () => {
|
||||||
|
// given
|
||||||
|
const hook = createHook([])
|
||||||
|
const sessionID = "no-skills"
|
||||||
|
updateSessionAgent(sessionID, "Sisyphus")
|
||||||
|
const output = { title: "", output: "result", metadata: {} }
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["tool.execute.after"]({ tool: "read", sessionID, callID: "1" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "read", sessionID, callID: "2" }, output)
|
||||||
|
await hook["tool.execute.after"]({ tool: "read", sessionID, callID: "3" }, output)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(output.output).toContain("[Category+Skill Reminder]")
|
||||||
|
expect(output.output).toContain("load_skills=[]")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import type { AvailableSkill } from "../../agents/dynamic-agent-prompt-builder"
|
||||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
|
||||||
@@ -34,33 +35,41 @@ const DELEGATION_TOOLS = new Set([
|
|||||||
"task",
|
"task",
|
||||||
])
|
])
|
||||||
|
|
||||||
const REMINDER_MESSAGE = `
|
function formatSkillNames(skills: AvailableSkill[], limit: number): string {
|
||||||
[Category+Skill Reminder]
|
if (skills.length === 0) return "(none)"
|
||||||
|
const shown = skills.slice(0, limit).map((s) => s.name)
|
||||||
|
const remaining = skills.length - shown.length
|
||||||
|
const suffix = remaining > 0 ? ` (+${remaining} more)` : ""
|
||||||
|
return shown.join(", ") + suffix
|
||||||
|
}
|
||||||
|
|
||||||
You are an orchestrator agent. Consider whether this work should be delegated:
|
function buildReminderMessage(availableSkills: AvailableSkill[]): string {
|
||||||
|
const builtinSkills = availableSkills.filter((s) => s.location === "plugin")
|
||||||
|
const customSkills = availableSkills.filter((s) => s.location !== "plugin")
|
||||||
|
|
||||||
**DELEGATE when:**
|
const builtinText = formatSkillNames(builtinSkills, 8)
|
||||||
- UI/Frontend work → category: "visual-engineering", skills: ["frontend-ui-ux"]
|
const customText = formatSkillNames(customSkills, 8)
|
||||||
- Complex logic/architecture → category: "ultrabrain"
|
|
||||||
- Quick/trivial tasks → category: "quick"
|
|
||||||
- Git operations → skills: ["git-master"]
|
|
||||||
- Browser automation → skills: ["playwright"] or ["agent-browser"]
|
|
||||||
|
|
||||||
**DO IT YOURSELF when:**
|
const exampleSkillName = customSkills[0]?.name ?? builtinSkills[0]?.name
|
||||||
- Gathering context/exploring codebase
|
const loadSkills = exampleSkillName ? `["${exampleSkillName}"]` : "[]"
|
||||||
- Simple edits that are part of a larger task you're coordinating
|
|
||||||
- Tasks requiring your full context understanding
|
|
||||||
|
|
||||||
Example delegation:
|
const lines = [
|
||||||
\`\`\`
|
"",
|
||||||
delegate_task(
|
"[Category+Skill Reminder]",
|
||||||
category="visual-engineering",
|
"",
|
||||||
load_skills=["frontend-ui-ux"],
|
`**Built-in**: ${builtinText}`,
|
||||||
description="Implement responsive navbar with animations",
|
`**⚡ YOUR SKILLS (PRIORITY)**: ${customText}`,
|
||||||
run_in_background=true
|
"",
|
||||||
)
|
"> User-installed skills OVERRIDE built-in defaults. ALWAYS prefer YOUR SKILLS when domain matches.",
|
||||||
\`\`\`
|
"",
|
||||||
`
|
"```typescript",
|
||||||
|
`delegate_task(category=\"visual-engineering\", load_skills=${loadSkills}, run_in_background=true)`,
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
interface ToolExecuteInput {
|
interface ToolExecuteInput {
|
||||||
tool: string
|
tool: string
|
||||||
@@ -81,8 +90,12 @@ interface SessionState {
|
|||||||
toolCallCount: number
|
toolCallCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCategorySkillReminderHook(_ctx: PluginInput) {
|
export function createCategorySkillReminderHook(
|
||||||
|
_ctx: PluginInput,
|
||||||
|
availableSkills: AvailableSkill[] = []
|
||||||
|
) {
|
||||||
const sessionStates = new Map<string, SessionState>()
|
const sessionStates = new Map<string, SessionState>()
|
||||||
|
const reminderMessage = buildReminderMessage(availableSkills)
|
||||||
|
|
||||||
function getOrCreateState(sessionID: string): SessionState {
|
function getOrCreateState(sessionID: string): SessionState {
|
||||||
if (!sessionStates.has(sessionID)) {
|
if (!sessionStates.has(sessionID)) {
|
||||||
@@ -130,7 +143,7 @@ export function createCategorySkillReminderHook(_ctx: PluginInput) {
|
|||||||
state.toolCallCount++
|
state.toolCallCount++
|
||||||
|
|
||||||
if (state.toolCallCount >= 3 && !state.delegationUsed && !state.reminderShown) {
|
if (state.toolCallCount >= 3 && !state.delegationUsed && !state.reminderShown) {
|
||||||
output.output += REMINDER_MESSAGE
|
output.output += reminderMessage
|
||||||
state.reminderShown = true
|
state.reminderShown = true
|
||||||
log("[category-skill-reminder] Reminder injected", {
|
log("[category-skill-reminder] Reminder injected", {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
Full Claude Code `settings.json` hook compatibility layer. Intercepts OpenCode events to execute external scripts/commands.
|
Full Claude Code `settings.json` hook compatibility layer. Intercepts OpenCode events to execute external scripts/commands.
|
||||||
|
|
||||||
**Config Sources** (priority): `.claude/settings.json` (project) > `~/.claude/settings.json` (global)
|
**Config Sources** (priority): `.claude/settings.local.json` > `.claude/settings.json` (project) > `~/.claude/settings.json` (global)
|
||||||
|
|
||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
```
|
```
|
||||||
claude-code-hooks/
|
claude-code-hooks/
|
||||||
├── index.ts # Main factory (401 lines)
|
├── index.ts # Main factory (421 lines)
|
||||||
├── config.ts # Loads ~/.claude/settings.json
|
├── config.ts # Loads ~/.claude/settings.json
|
||||||
├── config-loader.ts # Extended config (disabledHooks)
|
├── config-loader.ts # Extended config (disabledHooks)
|
||||||
├── pre-tool-use.ts # PreToolUse executor
|
├── pre-tool-use.ts # PreToolUse executor
|
||||||
@@ -19,6 +19,7 @@ claude-code-hooks/
|
|||||||
├── pre-compact.ts # PreCompact executor
|
├── pre-compact.ts # PreCompact executor
|
||||||
├── transcript.ts # Tool use recording
|
├── transcript.ts # Tool use recording
|
||||||
├── tool-input-cache.ts # Pre→post input caching
|
├── tool-input-cache.ts # Pre→post input caching
|
||||||
|
├── todo.ts # Todo integration
|
||||||
└── types.ts # Hook & IO type definitions
|
└── types.ts # Hook & IO type definitions
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,22 +32,16 @@ claude-code-hooks/
|
|||||||
| Stop | Session idle/end | Inject | sessionId, parentSessionId, cwd |
|
| Stop | Session idle/end | Inject | sessionId, parentSessionId, cwd |
|
||||||
| PreCompact | Before summarize | No | sessionId, cwd |
|
| PreCompact | Before summarize | No | sessionId, cwd |
|
||||||
|
|
||||||
## CONFIG SOURCES
|
|
||||||
Priority (highest first):
|
|
||||||
1. `.claude/settings.local.json` (Project-local, git-ignored)
|
|
||||||
2. `.claude/settings.json` (Project)
|
|
||||||
3. `~/.claude/settings.json` (Global user)
|
|
||||||
|
|
||||||
## HOOK EXECUTION
|
## HOOK EXECUTION
|
||||||
- **Matchers**: Hooks filter by tool name or event type via regex/glob.
|
- **Matchers**: Hooks filter by tool name or event type via regex/glob
|
||||||
- **Commands**: Executed via subprocess with env vars (`$SESSION_ID`, `$TOOL_NAME`).
|
- **Commands**: Executed via subprocess with env vars (`$SESSION_ID`, `$TOOL_NAME`)
|
||||||
- **Exit Codes**:
|
- **Exit Codes**:
|
||||||
- `0`: Pass (Success)
|
- `0`: Pass (Success)
|
||||||
- `1`: Warn (Continue with system message)
|
- `1`: Warn (Continue with system message)
|
||||||
- `2`: Block (Abort operation/prompt)
|
- `2`: Block (Abort operation/prompt)
|
||||||
|
|
||||||
## ANTI-PATTERNS
|
## ANTI-PATTERNS
|
||||||
- **Heavy PreToolUse**: Runs before EVERY tool; keep logic light to avoid latency.
|
- **Heavy PreToolUse**: Runs before EVERY tool; keep logic light to avoid latency
|
||||||
- **Blocking non-critical**: Prefer PostToolUse warnings for non-fatal issues.
|
- **Blocking non-critical**: Prefer PostToolUse warnings for non-fatal issues
|
||||||
- **Direct state mutation**: Use `updatedInput` in PreToolUse instead of side effects.
|
- **Direct state mutation**: Use `updatedInput` in PreToolUse instead of side effects
|
||||||
- **Ignoring Exit Codes**: Ensure scripts return `2` to properly block sensitive tools.
|
- **Ignoring Exit Codes**: Ensure scripts return `2` to properly block sensitive tools
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export function getClaudeSettingsPaths(customPath?: string): string[] {
|
|||||||
paths.unshift(customPath)
|
paths.unshift(customPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths
|
// Deduplicate paths to prevent loading the same file multiple times
|
||||||
|
// (e.g., when cwd is the home directory)
|
||||||
|
return [...new Set(paths)]
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeHooksConfig(
|
function mergeHooksConfig(
|
||||||
|
|||||||
@@ -1,14 +1,4 @@
|
|||||||
import { describe, expect, it, mock, beforeEach } from "bun:test"
|
import { describe, expect, it, mock } from "bun:test"
|
||||||
|
|
||||||
// Mock dependencies before importing
|
|
||||||
const mockInjectHookMessage = mock(() => true)
|
|
||||||
mock.module("../../features/hook-message-injector", () => ({
|
|
||||||
injectHookMessage: mockInjectHookMessage,
|
|
||||||
}))
|
|
||||||
|
|
||||||
mock.module("../../shared/logger", () => ({
|
|
||||||
log: () => {},
|
|
||||||
}))
|
|
||||||
|
|
||||||
mock.module("../../shared/system-directive", () => ({
|
mock.module("../../shared/system-directive", () => ({
|
||||||
createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`,
|
createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`,
|
||||||
@@ -25,78 +15,45 @@ mock.module("../../shared/system-directive", () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
import { createCompactionContextInjector } from "./index"
|
import { createCompactionContextInjector } from "./index"
|
||||||
import type { SummarizeContext } from "./index"
|
|
||||||
|
|
||||||
describe("createCompactionContextInjector", () => {
|
describe("createCompactionContextInjector", () => {
|
||||||
beforeEach(() => {
|
|
||||||
mockInjectHookMessage.mockClear()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Agent Verification State preservation", () => {
|
describe("Agent Verification State preservation", () => {
|
||||||
it("includes Agent Verification State section in compaction prompt", async () => {
|
it("includes Agent Verification State section in compaction prompt", async () => {
|
||||||
// given
|
//#given
|
||||||
const injector = createCompactionContextInjector()
|
const injector = createCompactionContextInjector()
|
||||||
const context: SummarizeContext = {
|
|
||||||
sessionID: "test-session",
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-sonnet-4-5",
|
|
||||||
usageRatio: 0.85,
|
|
||||||
directory: "/test/dir",
|
|
||||||
}
|
|
||||||
|
|
||||||
// when
|
//#when
|
||||||
await injector(context)
|
const prompt = injector()
|
||||||
|
|
||||||
// then
|
//#then
|
||||||
expect(mockInjectHookMessage).toHaveBeenCalledTimes(1)
|
expect(prompt).toContain("Agent Verification State")
|
||||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
expect(prompt).toContain("Current Agent")
|
||||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
expect(prompt).toContain("Verification Progress")
|
||||||
expect(injectedPrompt).toContain("Agent Verification State")
|
|
||||||
expect(injectedPrompt).toContain("Current Agent")
|
|
||||||
expect(injectedPrompt).toContain("Verification Progress")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("includes Momus-specific context for reviewer agents", async () => {
|
it("includes reviewer-agent continuity fields", async () => {
|
||||||
// given
|
//#given
|
||||||
const injector = createCompactionContextInjector()
|
const injector = createCompactionContextInjector()
|
||||||
const context: SummarizeContext = {
|
|
||||||
sessionID: "test-session",
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-sonnet-4-5",
|
|
||||||
usageRatio: 0.9,
|
|
||||||
directory: "/test/dir",
|
|
||||||
}
|
|
||||||
|
|
||||||
// when
|
//#when
|
||||||
await injector(context)
|
const prompt = injector()
|
||||||
|
|
||||||
// then
|
//#then
|
||||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
expect(prompt).toContain("Previous Rejections")
|
||||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
expect(prompt).toContain("Acceptance Status")
|
||||||
expect(injectedPrompt).toContain("Previous Rejections")
|
expect(prompt).toContain("reviewer agents")
|
||||||
expect(injectedPrompt).toContain("Acceptance Status")
|
|
||||||
expect(injectedPrompt).toContain("reviewer agents")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("preserves file verification progress in compaction prompt", async () => {
|
it("preserves file verification progress fields", async () => {
|
||||||
// given
|
//#given
|
||||||
const injector = createCompactionContextInjector()
|
const injector = createCompactionContextInjector()
|
||||||
const context: SummarizeContext = {
|
|
||||||
sessionID: "test-session",
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-sonnet-4-5",
|
|
||||||
usageRatio: 0.95,
|
|
||||||
directory: "/test/dir",
|
|
||||||
}
|
|
||||||
|
|
||||||
// when
|
//#when
|
||||||
await injector(context)
|
const prompt = injector()
|
||||||
|
|
||||||
// then
|
//#then
|
||||||
const calls = mockInjectHookMessage.mock.calls as unknown as [string, string, unknown][]
|
expect(prompt).toContain("Pending Verifications")
|
||||||
const injectedPrompt = calls[0]?.[1] ?? ""
|
expect(prompt).toContain("Files already verified")
|
||||||
expect(injectedPrompt).toContain("Pending Verifications")
|
|
||||||
expect(injectedPrompt).toContain("Files already verified")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
|
||||||
import { log } from "../../shared/logger"
|
|
||||||
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
|
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
|
||||||
|
|
||||||
export interface SummarizeContext {
|
const COMPACTION_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
|
||||||
sessionID: string
|
|
||||||
providerID: string
|
|
||||||
modelID: string
|
|
||||||
usageRatio: number
|
|
||||||
directory: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUMMARIZE_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
|
|
||||||
|
|
||||||
When summarizing this session, you MUST include the following sections in your summary:
|
When summarizing this session, you MUST include the following sections in your summary:
|
||||||
|
|
||||||
@@ -58,19 +48,5 @@ This context is critical for maintaining continuity after compaction.
|
|||||||
`
|
`
|
||||||
|
|
||||||
export function createCompactionContextInjector() {
|
export function createCompactionContextInjector() {
|
||||||
return async (ctx: SummarizeContext): Promise<void> => {
|
return (): string => COMPACTION_CONTEXT_PROMPT
|
||||||
log("[compaction-context-injector] injecting context", { sessionID: ctx.sessionID })
|
|
||||||
|
|
||||||
const success = injectHookMessage(ctx.sessionID, SUMMARIZE_CONTEXT_PROMPT, {
|
|
||||||
agent: "general",
|
|
||||||
model: { providerID: ctx.providerID, modelID: ctx.modelID },
|
|
||||||
path: { cwd: ctx.directory },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
log("[compaction-context-injector] context injected", { sessionID: ctx.sessionID })
|
|
||||||
} else {
|
|
||||||
log("[compaction-context-injector] injection failed", { sessionID: ctx.sessionID })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ export { createDelegateTaskRetryHook } from "./delegate-task-retry";
|
|||||||
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
export { createQuestionLabelTruncatorHook } from "./question-label-truncator";
|
||||||
export { createSubagentQuestionBlockerHook } from "./subagent-question-blocker";
|
export { createSubagentQuestionBlockerHook } from "./subagent-question-blocker";
|
||||||
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
export { createStopContinuationGuardHook, type StopContinuationGuard } from "./stop-continuation-guard";
|
||||||
export { createCompactionContextInjector, type SummarizeContext } from "./compaction-context-injector";
|
export { createCompactionContextInjector } from "./compaction-context-injector";
|
||||||
export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
|
export { createUnstableAgentBabysitterHook } from "./unstable-agent-babysitter";
|
||||||
export { createPreemptiveCompactionHook } from "./preemptive-compaction";
|
export { createPreemptiveCompactionHook } from "./preemptive-compaction";
|
||||||
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler";
|
||||||
|
export { createWriteExistingFileGuardHook } from "./write-existing-file-guard";
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function createKeywordDetectorHook(ctx: PluginInput, collector?: ContextC
|
|||||||
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
|
// Remove system-reminder content to prevent automated system messages from triggering mode keywords
|
||||||
const cleanText = removeSystemReminders(promptText)
|
const cleanText = removeSystemReminders(promptText)
|
||||||
const modelID = input.model?.modelID
|
const modelID = input.model?.modelID
|
||||||
let detectedKeywords = detectKeywordsWithType(removeCodeBlocks(cleanText), currentAgent, modelID)
|
let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID)
|
||||||
|
|
||||||
if (isPlannerAgent(currentAgent)) {
|
if (isPlannerAgent(currentAgent)) {
|
||||||
detectedKeywords = detectedKeywords.filter((k) => k.type !== "ultrawork")
|
detectedKeywords = detectedKeywords.filter((k) => k.type !== "ultrawork")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import type { ShellType } from "../../shared"
|
|
||||||
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
|
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
|
||||||
import { log, buildEnvPrefix } from "../../shared"
|
import { log, buildEnvPrefix } from "../../shared"
|
||||||
|
|
||||||
@@ -54,10 +53,8 @@ export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
|||||||
// for git commands to prevent interactive prompts.
|
// for git commands to prevent interactive prompts.
|
||||||
|
|
||||||
// The bash tool always runs in a Unix-like shell (bash/sh), even on Windows
|
// The bash tool always runs in a Unix-like shell (bash/sh), even on Windows
|
||||||
// (via Git Bash, WSL, etc.), so we always use unix export syntax.
|
// (via Git Bash, WSL, etc.), so always use unix export syntax.
|
||||||
// This fixes GitHub issues #983 and #889.
|
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, "unix")
|
||||||
const shellType: ShellType = "unix"
|
|
||||||
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
|
|
||||||
output.args.command = `${envPrefix} ${command}`
|
output.args.command = `${envPrefix} ${command}`
|
||||||
|
|
||||||
log(`[${HOOK_NAME}] Prepended non-interactive env vars to git command`, {
|
log(`[${HOOK_NAME}] Prepended non-interactive env vars to git command`, {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe("preemptive-compaction", () => {
|
|||||||
info: {
|
info: {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
providerID: "anthropic",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-5",
|
modelID: "claude-opus-4-6",
|
||||||
tokens: {
|
tokens: {
|
||||||
input: 180000,
|
input: 180000,
|
||||||
output: 0,
|
output: 0,
|
||||||
@@ -60,6 +60,41 @@ describe("preemptive-compaction", () => {
|
|||||||
expect(summarize).toHaveBeenCalled()
|
expect(summarize).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("triggers summarize for non-anthropic providers when usage exceeds threshold", async () => {
|
||||||
|
//#given
|
||||||
|
const messages = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
role: "assistant",
|
||||||
|
providerID: "openai",
|
||||||
|
modelID: "gpt-5.2",
|
||||||
|
tokens: {
|
||||||
|
input: 180000,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const summarize = mock(() => Promise.resolve())
|
||||||
|
const hook = createPreemptiveCompactionHook(createMockCtx({ messages, summarize }))
|
||||||
|
const output = { title: "", output: "", metadata: {} }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await hook["tool.execute.after"](
|
||||||
|
{ tool: "Read", sessionID, callID: "call-3" },
|
||||||
|
output
|
||||||
|
)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(summarize).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
test("does not summarize when usage is below threshold", async () => {
|
test("does not summarize when usage is below threshold", async () => {
|
||||||
// #given
|
// #given
|
||||||
const messages = mock(() =>
|
const messages = mock(() =>
|
||||||
@@ -69,7 +104,7 @@ describe("preemptive-compaction", () => {
|
|||||||
info: {
|
info: {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
providerID: "anthropic",
|
providerID: "anthropic",
|
||||||
modelID: "claude-opus-4-5",
|
modelID: "claude-opus-4-6",
|
||||||
tokens: {
|
tokens: {
|
||||||
input: 100000,
|
input: 100000,
|
||||||
output: 0,
|
output: 0,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
const DEFAULT_ACTUAL_LIMIT = 200_000
|
||||||
|
|
||||||
const ANTHROPIC_ACTUAL_LIMIT =
|
const ANTHROPIC_ACTUAL_LIMIT =
|
||||||
process.env.ANTHROPIC_1M_CONTEXT === "true" ||
|
process.env.ANTHROPIC_1M_CONTEXT === "true" ||
|
||||||
process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true"
|
process.env.VERTEX_ANTHROPIC_1M_CONTEXT === "true"
|
||||||
? 1_000_000
|
? 1_000_000
|
||||||
: 200_000
|
: DEFAULT_ACTUAL_LIMIT
|
||||||
|
|
||||||
const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78
|
const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78
|
||||||
|
|
||||||
@@ -59,11 +61,14 @@ export function createPreemptiveCompactionHook(ctx: PluginInput) {
|
|||||||
if (assistantMessages.length === 0) return
|
if (assistantMessages.length === 0) return
|
||||||
|
|
||||||
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
||||||
if (lastAssistant.providerID !== "anthropic") return
|
const actualLimit =
|
||||||
|
lastAssistant.providerID === "anthropic"
|
||||||
|
? ANTHROPIC_ACTUAL_LIMIT
|
||||||
|
: DEFAULT_ACTUAL_LIMIT
|
||||||
|
|
||||||
const lastTokens = lastAssistant.tokens
|
const lastTokens = lastAssistant.tokens
|
||||||
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
const totalInputTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
||||||
const usageRatio = totalInputTokens / ANTHROPIC_ACTUAL_LIMIT
|
const usageRatio = totalInputTokens / actualLimit
|
||||||
|
|
||||||
if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD) return
|
if (usageRatio < PREEMPTIVE_COMPACTION_THRESHOLD) return
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const ALLOWED_EXTENSIONS = [".md"]
|
|||||||
|
|
||||||
export const ALLOWED_PATH_PREFIX = ".sisyphus"
|
export const ALLOWED_PATH_PREFIX = ".sisyphus"
|
||||||
|
|
||||||
export const BLOCKED_TOOLS = ["Write", "Edit", "write", "edit"]
|
export const BLOCKED_TOOLS = ["Write", "Edit", "write", "edit", "bash"]
|
||||||
|
|
||||||
export const PLANNING_CONSULT_WARNING = `
|
export const PLANNING_CONSULT_WARNING = `
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,25 @@ describe("prometheus-md-only", () => {
|
|||||||
).rejects.toThrow("can only write/edit .md files")
|
).rejects.toThrow("can only write/edit .md files")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should not affect non-Write/Edit tools", async () => {
|
test("should block bash commands from Prometheus", async () => {
|
||||||
|
// given
|
||||||
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
|
const input = {
|
||||||
|
tool: "bash",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { command: "echo test" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// when / #then
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).rejects.toThrow("cannot execute bash commands")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not affect non-blocked tools", async () => {
|
||||||
// given
|
// given
|
||||||
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
const input = {
|
const input = {
|
||||||
@@ -334,6 +352,121 @@ describe("prometheus-md-only", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("boulder state priority over message files (fixes #927)", () => {
|
||||||
|
const BOULDER_DIR = join(tmpdir(), `boulder-test-${randomUUID()}`)
|
||||||
|
const BOULDER_FILE = join(BOULDER_DIR, ".sisyphus", "boulder.json")
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mkdirSync(join(BOULDER_DIR, ".sisyphus"), { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(BOULDER_DIR, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
//#given session was started with prometheus (first message), but /start-work set boulder agent to atlas
|
||||||
|
//#when user types "continue" after interruption (memory cleared, falls back to message files)
|
||||||
|
//#then should use boulder state agent (atlas), not message file agent (prometheus)
|
||||||
|
test("should prioritize boulder agent over message file agent", async () => {
|
||||||
|
// given - prometheus in message files (from /plan)
|
||||||
|
setupMessageStorage(TEST_SESSION_ID, "prometheus")
|
||||||
|
|
||||||
|
// given - atlas in boulder state (from /start-work)
|
||||||
|
writeFileSync(BOULDER_FILE, JSON.stringify({
|
||||||
|
active_plan: "/test/plan.md",
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
session_ids: [TEST_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
agent: "atlas"
|
||||||
|
}))
|
||||||
|
|
||||||
|
const hook = createPrometheusMdOnlyHook({
|
||||||
|
client: {},
|
||||||
|
directory: BOULDER_DIR,
|
||||||
|
} as never)
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
tool: "Write",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { filePath: "/path/to/code.ts" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// when / then - should NOT block because boulder says atlas, not prometheus
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should use prometheus from boulder state when set", async () => {
|
||||||
|
// given - atlas in message files (from some other agent)
|
||||||
|
setupMessageStorage(TEST_SESSION_ID, "atlas")
|
||||||
|
|
||||||
|
// given - prometheus in boulder state (edge case, but should honor it)
|
||||||
|
writeFileSync(BOULDER_FILE, JSON.stringify({
|
||||||
|
active_plan: "/test/plan.md",
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
session_ids: [TEST_SESSION_ID],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
agent: "prometheus"
|
||||||
|
}))
|
||||||
|
|
||||||
|
const hook = createPrometheusMdOnlyHook({
|
||||||
|
client: {},
|
||||||
|
directory: BOULDER_DIR,
|
||||||
|
} as never)
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
tool: "Write",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { filePath: "/path/to/code.ts" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// when / then - should block because boulder says prometheus
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).rejects.toThrow("can only write/edit .md files")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should fall back to message files when session not in boulder", async () => {
|
||||||
|
// given - prometheus in message files
|
||||||
|
setupMessageStorage(TEST_SESSION_ID, "prometheus")
|
||||||
|
|
||||||
|
// given - boulder state exists but for different session
|
||||||
|
writeFileSync(BOULDER_FILE, JSON.stringify({
|
||||||
|
active_plan: "/test/plan.md",
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
session_ids: ["other-session-id"],
|
||||||
|
plan_name: "test-plan",
|
||||||
|
agent: "atlas"
|
||||||
|
}))
|
||||||
|
|
||||||
|
const hook = createPrometheusMdOnlyHook({
|
||||||
|
client: {},
|
||||||
|
directory: BOULDER_DIR,
|
||||||
|
} as never)
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
tool: "Write",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { filePath: "/path/to/code.ts" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// when / then - should block because falls back to message files (prometheus)
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).rejects.toThrow("can only write/edit .md files")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("without message storage", () => {
|
describe("without message storage", () => {
|
||||||
test("should handle missing session gracefully (no agent found)", async () => {
|
test("should handle missing session gracefully (no agent found)", async () => {
|
||||||
// given
|
// given
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join, resolve, relative, isAbsolute } from "node:path"
|
|||||||
import { HOOK_NAME, PROMETHEUS_AGENT, ALLOWED_EXTENSIONS, ALLOWED_PATH_PREFIX, BLOCKED_TOOLS, PLANNING_CONSULT_WARNING, PROMETHEUS_WORKFLOW_REMINDER } from "./constants"
|
import { HOOK_NAME, PROMETHEUS_AGENT, ALLOWED_EXTENSIONS, ALLOWED_PATH_PREFIX, BLOCKED_TOOLS, PLANNING_CONSULT_WARNING, PROMETHEUS_WORKFLOW_REMINDER } from "./constants"
|
||||||
import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
||||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||||
|
import { readBoulderState } from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||||
import { getAgentDisplayName } from "../../shared/agent-display-names"
|
import { getAgentDisplayName } from "../../shared/agent-display-names"
|
||||||
@@ -70,8 +71,31 @@ function getAgentFromMessageFiles(sessionID: string): string | undefined {
|
|||||||
return findFirstMessageWithAgent(messageDir) ?? findNearestMessageWithFields(messageDir)?.agent
|
return findFirstMessageWithAgent(messageDir) ?? findNearestMessageWithFields(messageDir)?.agent
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAgentFromSession(sessionID: string): string | undefined {
|
/**
|
||||||
return getSessionAgent(sessionID) ?? getAgentFromMessageFiles(sessionID)
|
* Get the effective agent for the session.
|
||||||
|
* Priority order:
|
||||||
|
* 1. In-memory session agent (most recent, set by /start-work)
|
||||||
|
* 2. Boulder state agent (persisted across restarts, fixes #927)
|
||||||
|
* 3. Message files (fallback for sessions without boulder state)
|
||||||
|
*
|
||||||
|
* This fixes issue #927 where after interruption:
|
||||||
|
* - In-memory map is cleared (process restart)
|
||||||
|
* - Message files return "prometheus" (oldest message from /plan)
|
||||||
|
* - But boulder.json has agent: "atlas" (set by /start-work)
|
||||||
|
*/
|
||||||
|
function getAgentFromSession(sessionID: string, directory: string): string | undefined {
|
||||||
|
// Check in-memory first (current session)
|
||||||
|
const memoryAgent = getSessionAgent(sessionID)
|
||||||
|
if (memoryAgent) return memoryAgent
|
||||||
|
|
||||||
|
// Check boulder state (persisted across restarts) - fixes #927
|
||||||
|
const boulderState = readBoulderState(directory)
|
||||||
|
if (boulderState?.session_ids.includes(sessionID) && boulderState.agent) {
|
||||||
|
return boulderState.agent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to message files
|
||||||
|
return getAgentFromMessageFiles(sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPrometheusMdOnlyHook(ctx: PluginInput) {
|
export function createPrometheusMdOnlyHook(ctx: PluginInput) {
|
||||||
@@ -80,7 +104,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
|
|||||||
input: { tool: string; sessionID: string; callID: string },
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
output: { args: Record<string, unknown>; message?: string }
|
output: { args: Record<string, unknown>; message?: string }
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const agentName = getAgentFromSession(input.sessionID)
|
const agentName = getAgentFromSession(input.sessionID, ctx.directory)
|
||||||
|
|
||||||
if (agentName !== PROMETHEUS_AGENT) {
|
if (agentName !== PROMETHEUS_AGENT) {
|
||||||
return
|
return
|
||||||
@@ -106,6 +130,20 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block bash commands completely - Prometheus is read-only
|
||||||
|
if (toolName === "bash") {
|
||||||
|
log(`[${HOOK_NAME}] Blocked: Prometheus cannot execute bash commands`, {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
tool: toolName,
|
||||||
|
agent: agentName,
|
||||||
|
})
|
||||||
|
throw new Error(
|
||||||
|
`[${HOOK_NAME}] ${getAgentDisplayName("prometheus")} cannot execute bash commands. ` +
|
||||||
|
`${getAgentDisplayName("prometheus")} is a READ-ONLY planner. Use /start-work to execute the plan. ` +
|
||||||
|
`APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const filePath = (output.args.filePath ?? output.args.path ?? output.args.file) as string | undefined
|
const filePath = (output.args.filePath ?? output.args.path ?? output.args.file) as string | undefined
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ All ${progress.total} tasks are done. Create a new plan with: /plan "your task"`
|
|||||||
if (existingState) {
|
if (existingState) {
|
||||||
clearBoulderState(ctx.directory)
|
clearBoulderState(ctx.directory)
|
||||||
}
|
}
|
||||||
const newState = createBoulderState(matchedPlan, sessionId)
|
const newState = createBoulderState(matchedPlan, sessionId, "atlas")
|
||||||
writeBoulderState(ctx.directory, newState)
|
writeBoulderState(ctx.directory, newState)
|
||||||
|
|
||||||
contextInfo = `
|
contextInfo = `
|
||||||
@@ -187,7 +187,7 @@ All ${plans.length} plan(s) are complete. Create a new plan with: /plan "your ta
|
|||||||
} else if (incompletePlans.length === 1) {
|
} else if (incompletePlans.length === 1) {
|
||||||
const planPath = incompletePlans[0]
|
const planPath = incompletePlans[0]
|
||||||
const progress = getPlanProgress(planPath)
|
const progress = getPlanProgress(planPath)
|
||||||
const newState = createBoulderState(planPath, sessionId)
|
const newState = createBoulderState(planPath, sessionId, "atlas")
|
||||||
writeBoulderState(ctx.directory, newState)
|
writeBoulderState(ctx.directory, newState)
|
||||||
|
|
||||||
contextInfo += `
|
contextInfo += `
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("createThinkModeHook integration", () => {
|
|||||||
const hook = createThinkModeHook()
|
const hook = createThinkModeHook()
|
||||||
const input = createMockInput(
|
const input = createMockInput(
|
||||||
"github-copilot",
|
"github-copilot",
|
||||||
"claude-opus-4-5",
|
"claude-opus-4-6",
|
||||||
"Please think deeply about this problem"
|
"Please think deeply about this problem"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ describe("createThinkModeHook integration", () => {
|
|||||||
|
|
||||||
// then should upgrade to high variant and inject thinking config
|
// then should upgrade to high variant and inject thinking config
|
||||||
const message = input.message as MessageWithInjectedProps
|
const message = input.message as MessageWithInjectedProps
|
||||||
expect(input.message.model?.modelID).toBe("claude-opus-4-5-high")
|
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||||
expect(message.thinking).toBeDefined()
|
expect(message.thinking).toBeDefined()
|
||||||
expect((message.thinking as Record<string, unknown>)?.type).toBe(
|
expect((message.thinking as Record<string, unknown>)?.type).toBe(
|
||||||
"enabled"
|
"enabled"
|
||||||
@@ -61,11 +61,11 @@ describe("createThinkModeHook integration", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle github-copilot Claude with dots in version", async () => {
|
it("should handle github-copilot Claude with dots in version", async () => {
|
||||||
// given a github-copilot Claude model with dot format (claude-opus-4.5)
|
// given a github-copilot Claude model with dot format (claude-opus-4.6)
|
||||||
const hook = createThinkModeHook()
|
const hook = createThinkModeHook()
|
||||||
const input = createMockInput(
|
const input = createMockInput(
|
||||||
"github-copilot",
|
"github-copilot",
|
||||||
"claude-opus-4.5",
|
"claude-opus-4.6",
|
||||||
"ultrathink mode"
|
"ultrathink mode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ describe("createThinkModeHook integration", () => {
|
|||||||
|
|
||||||
// then should upgrade to high variant (hyphen format)
|
// then should upgrade to high variant (hyphen format)
|
||||||
const message = input.message as MessageWithInjectedProps
|
const message = input.message as MessageWithInjectedProps
|
||||||
expect(input.message.model?.modelID).toBe("claude-opus-4-5-high")
|
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||||
expect(message.thinking).toBeDefined()
|
expect(message.thinking).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ describe("createThinkModeHook integration", () => {
|
|||||||
const hook = createThinkModeHook()
|
const hook = createThinkModeHook()
|
||||||
const input = createMockInput(
|
const input = createMockInput(
|
||||||
"github-copilot",
|
"github-copilot",
|
||||||
"claude-opus-4-5",
|
"claude-opus-4-6",
|
||||||
"Just do this task"
|
"Just do this task"
|
||||||
)
|
)
|
||||||
const originalModelID = input.message.model?.modelID
|
const originalModelID = input.message.model?.modelID
|
||||||
@@ -271,7 +271,7 @@ describe("createThinkModeHook integration", () => {
|
|||||||
const hook = createThinkModeHook()
|
const hook = createThinkModeHook()
|
||||||
const input = createMockInput(
|
const input = createMockInput(
|
||||||
"github-copilot",
|
"github-copilot",
|
||||||
"claude-opus-4-5-high",
|
"claude-opus-4-6-high",
|
||||||
"think deeply"
|
"think deeply"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ describe("createThinkModeHook integration", () => {
|
|||||||
|
|
||||||
// then should NOT modify the model (already high)
|
// then should NOT modify the model (already high)
|
||||||
const message = input.message as MessageWithInjectedProps
|
const message = input.message as MessageWithInjectedProps
|
||||||
expect(input.message.model?.modelID).toBe("claude-opus-4-5-high")
|
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||||
// No additional thinking config should be injected
|
// No additional thinking config should be injected
|
||||||
expect(message.thinking).toBeUndefined()
|
expect(message.thinking).toBeUndefined()
|
||||||
})
|
})
|
||||||
@@ -341,13 +341,13 @@ describe("createThinkModeHook integration", () => {
|
|||||||
it("should handle empty prompt gracefully", async () => {
|
it("should handle empty prompt gracefully", async () => {
|
||||||
// given empty prompt
|
// given empty prompt
|
||||||
const hook = createThinkModeHook()
|
const hook = createThinkModeHook()
|
||||||
const input = createMockInput("github-copilot", "claude-opus-4-5", "")
|
const input = createMockInput("github-copilot", "claude-opus-4-6", "")
|
||||||
|
|
||||||
// when the chat.params hook is called
|
// when the chat.params hook is called
|
||||||
await hook["chat.params"](input, sessionID)
|
await hook["chat.params"](input, sessionID)
|
||||||
|
|
||||||
// then should not upgrade (no think keyword)
|
// then should not upgrade (no think keyword)
|
||||||
expect(input.message.model?.modelID).toBe("claude-opus-4-5")
|
expect(input.message.model?.modelID).toBe("claude-opus-4-6")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("think-mode switcher", () => {
|
|||||||
it("should resolve github-copilot Claude Opus to anthropic config", () => {
|
it("should resolve github-copilot Claude Opus to anthropic config", () => {
|
||||||
// given a github-copilot provider with Claude Opus model
|
// given a github-copilot provider with Claude Opus model
|
||||||
const providerID = "github-copilot"
|
const providerID = "github-copilot"
|
||||||
const modelID = "claude-opus-4-5"
|
const modelID = "claude-opus-4-6"
|
||||||
|
|
||||||
// when getting thinking config
|
// when getting thinking config
|
||||||
const config = getThinkingConfig(providerID, modelID)
|
const config = getThinkingConfig(providerID, modelID)
|
||||||
@@ -38,8 +38,8 @@ describe("think-mode switcher", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should handle Claude with dots in version number", () => {
|
it("should handle Claude with dots in version number", () => {
|
||||||
// given a model ID with dots (claude-opus-4.5)
|
// given a model ID with dots (claude-opus-4.6)
|
||||||
const config = getThinkingConfig("github-copilot", "claude-opus-4.5")
|
const config = getThinkingConfig("github-copilot", "claude-opus-4.6")
|
||||||
|
|
||||||
// then should still return anthropic thinking config
|
// then should still return anthropic thinking config
|
||||||
expect(config).not.toBeNull()
|
expect(config).not.toBeNull()
|
||||||
@@ -127,18 +127,26 @@ describe("think-mode switcher", () => {
|
|||||||
describe("getHighVariant with dots vs hyphens", () => {
|
describe("getHighVariant with dots vs hyphens", () => {
|
||||||
it("should handle dots in Claude version numbers", () => {
|
it("should handle dots in Claude version numbers", () => {
|
||||||
// given a Claude model ID with dot format
|
// given a Claude model ID with dot format
|
||||||
const variant = getHighVariant("claude-opus-4.5")
|
const variant = getHighVariant("claude-opus-4.6")
|
||||||
|
|
||||||
// then should return high variant with hyphen format
|
// then should return high variant with hyphen format
|
||||||
expect(variant).toBe("claude-opus-4-5-high")
|
expect(variant).toBe("claude-opus-4-6-high")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle hyphens in Claude version numbers", () => {
|
it("should handle hyphens in Claude version numbers", () => {
|
||||||
// given a Claude model ID with hyphen format
|
// given a Claude model ID with hyphen format
|
||||||
const variant = getHighVariant("claude-opus-4-5")
|
const variant = getHighVariant("claude-opus-4-6")
|
||||||
|
|
||||||
// then should return high variant
|
// then should return high variant
|
||||||
expect(variant).toBe("claude-opus-4-5-high")
|
expect(variant).toBe("claude-opus-4-6-high")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle claude-opus-4-6 high variant", () => {
|
||||||
|
// given a Claude Opus 4.6 model ID
|
||||||
|
const variant = getHighVariant("claude-opus-4-6")
|
||||||
|
|
||||||
|
// then should return high variant
|
||||||
|
expect(variant).toBe("claude-opus-4-6-high")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle dots in GPT version numbers", () => {
|
it("should handle dots in GPT version numbers", () => {
|
||||||
@@ -169,7 +177,7 @@ describe("think-mode switcher", () => {
|
|||||||
|
|
||||||
it("should return null for already-high variants", () => {
|
it("should return null for already-high variants", () => {
|
||||||
// given model IDs that are already high variants
|
// given model IDs that are already high variants
|
||||||
expect(getHighVariant("claude-opus-4-5-high")).toBeNull()
|
expect(getHighVariant("claude-opus-4-6-high")).toBeNull()
|
||||||
expect(getHighVariant("gpt-5-2-high")).toBeNull()
|
expect(getHighVariant("gpt-5-2-high")).toBeNull()
|
||||||
expect(getHighVariant("gemini-3-pro-high")).toBeNull()
|
expect(getHighVariant("gemini-3-pro-high")).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -185,7 +193,7 @@ describe("think-mode switcher", () => {
|
|||||||
describe("isAlreadyHighVariant", () => {
|
describe("isAlreadyHighVariant", () => {
|
||||||
it("should detect -high suffix", () => {
|
it("should detect -high suffix", () => {
|
||||||
// given model IDs with -high suffix
|
// given model IDs with -high suffix
|
||||||
expect(isAlreadyHighVariant("claude-opus-4-5-high")).toBe(true)
|
expect(isAlreadyHighVariant("claude-opus-4-6-high")).toBe(true)
|
||||||
expect(isAlreadyHighVariant("gpt-5-2-high")).toBe(true)
|
expect(isAlreadyHighVariant("gpt-5-2-high")).toBe(true)
|
||||||
expect(isAlreadyHighVariant("gemini-3-pro-high")).toBe(true)
|
expect(isAlreadyHighVariant("gemini-3-pro-high")).toBe(true)
|
||||||
})
|
})
|
||||||
@@ -197,8 +205,8 @@ describe("think-mode switcher", () => {
|
|||||||
|
|
||||||
it("should return false for base models", () => {
|
it("should return false for base models", () => {
|
||||||
// given base model IDs without -high suffix
|
// given base model IDs without -high suffix
|
||||||
expect(isAlreadyHighVariant("claude-opus-4-5")).toBe(false)
|
expect(isAlreadyHighVariant("claude-opus-4-6")).toBe(false)
|
||||||
expect(isAlreadyHighVariant("claude-opus-4.5")).toBe(false)
|
expect(isAlreadyHighVariant("claude-opus-4.6")).toBe(false)
|
||||||
expect(isAlreadyHighVariant("gpt-5.2")).toBe(false)
|
expect(isAlreadyHighVariant("gpt-5.2")).toBe(false)
|
||||||
expect(isAlreadyHighVariant("gemini-3-pro")).toBe(false)
|
expect(isAlreadyHighVariant("gemini-3-pro")).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -214,7 +222,7 @@ describe("think-mode switcher", () => {
|
|||||||
it("should return null for already-high variants", () => {
|
it("should return null for already-high variants", () => {
|
||||||
// given already-high model variants
|
// given already-high model variants
|
||||||
expect(
|
expect(
|
||||||
getThinkingConfig("anthropic", "claude-opus-4-5-high")
|
getThinkingConfig("anthropic", "claude-opus-4-6-high")
|
||||||
).toBeNull()
|
).toBeNull()
|
||||||
expect(getThinkingConfig("openai", "gpt-5-2-high")).toBeNull()
|
expect(getThinkingConfig("openai", "gpt-5-2-high")).toBeNull()
|
||||||
expect(getThinkingConfig("google", "gemini-3-pro-high")).toBeNull()
|
expect(getThinkingConfig("google", "gemini-3-pro-high")).toBeNull()
|
||||||
@@ -223,7 +231,7 @@ describe("think-mode switcher", () => {
|
|||||||
it("should return null for already-high variants via github-copilot", () => {
|
it("should return null for already-high variants via github-copilot", () => {
|
||||||
// given already-high model variants via github-copilot
|
// given already-high model variants via github-copilot
|
||||||
expect(
|
expect(
|
||||||
getThinkingConfig("github-copilot", "claude-opus-4-5-high")
|
getThinkingConfig("github-copilot", "claude-opus-4-6-high")
|
||||||
).toBeNull()
|
).toBeNull()
|
||||||
expect(getThinkingConfig("github-copilot", "gpt-5.2-high")).toBeNull()
|
expect(getThinkingConfig("github-copilot", "gpt-5.2-high")).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -250,7 +258,7 @@ describe("think-mode switcher", () => {
|
|||||||
describe("Direct provider configs (backwards compatibility)", () => {
|
describe("Direct provider configs (backwards compatibility)", () => {
|
||||||
it("should still work for direct anthropic provider", () => {
|
it("should still work for direct anthropic provider", () => {
|
||||||
// given direct anthropic provider
|
// given direct anthropic provider
|
||||||
const config = getThinkingConfig("anthropic", "claude-opus-4-5")
|
const config = getThinkingConfig("anthropic", "claude-opus-4-6")
|
||||||
|
|
||||||
// then should return anthropic thinking config
|
// then should return anthropic thinking config
|
||||||
expect(config).not.toBeNull()
|
expect(config).not.toBeNull()
|
||||||
@@ -343,10 +351,10 @@ describe("think-mode switcher", () => {
|
|||||||
|
|
||||||
it("should handle prefixes with dots in version numbers", () => {
|
it("should handle prefixes with dots in version numbers", () => {
|
||||||
// given a model ID with prefix and dots
|
// given a model ID with prefix and dots
|
||||||
const variant = getHighVariant("vertex_ai/claude-opus-4.5")
|
const variant = getHighVariant("vertex_ai/claude-opus-4.6")
|
||||||
|
|
||||||
// then should normalize dots and preserve prefix
|
// then should normalize dots and preserve prefix
|
||||||
expect(variant).toBe("vertex_ai/claude-opus-4-5-high")
|
expect(variant).toBe("vertex_ai/claude-opus-4-6-high")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle multiple different prefixes", () => {
|
it("should handle multiple different prefixes", () => {
|
||||||
@@ -364,7 +372,7 @@ describe("think-mode switcher", () => {
|
|||||||
|
|
||||||
it("should return null for already-high prefixed models", () => {
|
it("should return null for already-high prefixed models", () => {
|
||||||
// given prefixed model IDs that are already high
|
// given prefixed model IDs that are already high
|
||||||
expect(getHighVariant("vertex_ai/claude-opus-4-5-high")).toBeNull()
|
expect(getHighVariant("vertex_ai/claude-opus-4-6-high")).toBeNull()
|
||||||
expect(getHighVariant("openai/gpt-5-2-high")).toBeNull()
|
expect(getHighVariant("openai/gpt-5-2-high")).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -372,14 +380,14 @@ describe("think-mode switcher", () => {
|
|||||||
describe("isAlreadyHighVariant with prefixes", () => {
|
describe("isAlreadyHighVariant with prefixes", () => {
|
||||||
it("should detect -high suffix in prefixed models", () => {
|
it("should detect -high suffix in prefixed models", () => {
|
||||||
// given prefixed model IDs with -high suffix
|
// given prefixed model IDs with -high suffix
|
||||||
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-5-high")).toBe(true)
|
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-6-high")).toBe(true)
|
||||||
expect(isAlreadyHighVariant("openai/gpt-5-2-high")).toBe(true)
|
expect(isAlreadyHighVariant("openai/gpt-5-2-high")).toBe(true)
|
||||||
expect(isAlreadyHighVariant("custom/gemini-3-pro-high")).toBe(true)
|
expect(isAlreadyHighVariant("custom/gemini-3-pro-high")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return false for prefixed base models", () => {
|
it("should return false for prefixed base models", () => {
|
||||||
// given prefixed base model IDs without -high suffix
|
// given prefixed base model IDs without -high suffix
|
||||||
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-5")).toBe(false)
|
expect(isAlreadyHighVariant("vertex_ai/claude-opus-4-6")).toBe(false)
|
||||||
expect(isAlreadyHighVariant("openai/gpt-5-2")).toBe(false)
|
expect(isAlreadyHighVariant("openai/gpt-5-2")).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -402,7 +410,7 @@ describe("think-mode switcher", () => {
|
|||||||
it("should work with prefixed models on known providers", () => {
|
it("should work with prefixed models on known providers", () => {
|
||||||
// given known provider (anthropic) with prefixed model
|
// given known provider (anthropic) with prefixed model
|
||||||
// This tests that the base model name is correctly extracted for capability check
|
// This tests that the base model name is correctly extracted for capability check
|
||||||
const config = getThinkingConfig("anthropic", "custom-prefix/claude-opus-4-5")
|
const config = getThinkingConfig("anthropic", "custom-prefix/claude-opus-4-6")
|
||||||
|
|
||||||
// then should return thinking config (base model is capable)
|
// then should return thinking config (base model is capable)
|
||||||
expect(config).not.toBeNull()
|
expect(config).not.toBeNull()
|
||||||
@@ -411,7 +419,7 @@ describe("think-mode switcher", () => {
|
|||||||
|
|
||||||
it("should return null for prefixed models that are already high", () => {
|
it("should return null for prefixed models that are already high", () => {
|
||||||
// given prefixed already-high model
|
// given prefixed already-high model
|
||||||
const config = getThinkingConfig("anthropic", "vertex_ai/claude-opus-4-5-high")
|
const config = getThinkingConfig("anthropic", "vertex_ai/claude-opus-4-6-high")
|
||||||
|
|
||||||
// then should return null
|
// then should return null
|
||||||
expect(config).toBeNull()
|
expect(config).toBeNull()
|
||||||
@@ -444,11 +452,11 @@ describe("think-mode switcher", () => {
|
|||||||
|
|
||||||
it("should not break when switching to high variant in think mode", () => {
|
it("should not break when switching to high variant in think mode", () => {
|
||||||
// given think mode switching vertex_ai/claude model to high variant
|
// given think mode switching vertex_ai/claude model to high variant
|
||||||
const original = "vertex_ai/claude-opus-4-5"
|
const original = "vertex_ai/claude-opus-4-6"
|
||||||
const high = getHighVariant(original)
|
const high = getHighVariant(original)
|
||||||
|
|
||||||
// then the high variant should be valid
|
// then the high variant should be valid
|
||||||
expect(high).toBe("vertex_ai/claude-opus-4-5-high")
|
expect(high).toBe("vertex_ai/claude-opus-4-6-high")
|
||||||
|
|
||||||
// #and should be recognized as already high
|
// #and should be recognized as already high
|
||||||
expect(isAlreadyHighVariant(high!)).toBe(true)
|
expect(isAlreadyHighVariant(high!)).toBe(true)
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ function extractModelPrefix(modelID: string): { prefix: string; base: string } {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes model IDs to use consistent hyphen formatting.
|
* Normalizes model IDs to use consistent hyphen formatting.
|
||||||
* GitHub Copilot may use dots (claude-opus-4.5) but our maps use hyphens (claude-opus-4-5).
|
* GitHub Copilot may use dots (claude-opus-4.6) but our maps use hyphens (claude-opus-4-6).
|
||||||
* This ensures lookups work regardless of format.
|
* This ensures lookups work regardless of format.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* normalizeModelID("claude-opus-4.5") // "claude-opus-4-5"
|
* normalizeModelID("claude-opus-4.6") // "claude-opus-4-6"
|
||||||
* normalizeModelID("gemini-3.5-pro") // "gemini-3-5-pro"
|
* normalizeModelID("gemini-3.5-pro") // "gemini-3-5-pro"
|
||||||
* normalizeModelID("gpt-5.2") // "gpt-5-2"
|
* normalizeModelID("gpt-5.2") // "gpt-5-2"
|
||||||
* normalizeModelID("vertex_ai/claude-opus-4.5") // "vertex_ai/claude-opus-4-5"
|
* normalizeModelID("vertex_ai/claude-opus-4.6") // "vertex_ai/claude-opus-4-6"
|
||||||
*/
|
*/
|
||||||
function normalizeModelID(modelID: string): string {
|
function normalizeModelID(modelID: string): string {
|
||||||
// Replace dots with hyphens when followed by a digit
|
// Replace dots with hyphens when followed by a digit
|
||||||
@@ -59,10 +59,10 @@ function normalizeModelID(modelID: string): string {
|
|||||||
* model provider (Anthropic, Google, OpenAI).
|
* model provider (Anthropic, Google, OpenAI).
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* resolveProvider("github-copilot", "claude-opus-4-5") // "anthropic"
|
* resolveProvider("github-copilot", "claude-opus-4-6") // "anthropic"
|
||||||
* resolveProvider("github-copilot", "gemini-3-pro") // "google"
|
* resolveProvider("github-copilot", "gemini-3-pro") // "google"
|
||||||
* resolveProvider("github-copilot", "gpt-5.2") // "openai"
|
* resolveProvider("github-copilot", "gpt-5.2") // "openai"
|
||||||
* resolveProvider("anthropic", "claude-opus-4-5") // "anthropic" (unchanged)
|
* resolveProvider("anthropic", "claude-opus-4-6") // "anthropic" (unchanged)
|
||||||
*/
|
*/
|
||||||
function resolveProvider(providerID: string, modelID: string): string {
|
function resolveProvider(providerID: string, modelID: string): string {
|
||||||
// GitHub Copilot is a proxy - infer actual provider from model name
|
// GitHub Copilot is a proxy - infer actual provider from model name
|
||||||
@@ -88,7 +88,7 @@ function resolveProvider(providerID: string, modelID: string): string {
|
|||||||
const HIGH_VARIANT_MAP: Record<string, string> = {
|
const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||||
// Claude
|
// Claude
|
||||||
"claude-sonnet-4-5": "claude-sonnet-4-5-high",
|
"claude-sonnet-4-5": "claude-sonnet-4-5-high",
|
||||||
"claude-opus-4-5": "claude-opus-4-5-high",
|
"claude-opus-4-6": "claude-opus-4-6-high",
|
||||||
// Gemini
|
// Gemini
|
||||||
"gemini-3-pro": "gemini-3-pro-high",
|
"gemini-3-pro": "gemini-3-pro-high",
|
||||||
"gemini-3-pro-low": "gemini-3-pro-high",
|
"gemini-3-pro-low": "gemini-3-pro-high",
|
||||||
|
|||||||
206
src/hooks/write-existing-file-guard/index.test.ts
Normal file
206
src/hooks/write-existing-file-guard/index.test.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
|
import { createWriteExistingFileGuardHook } from "./index"
|
||||||
|
import * as fs from "fs"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as os from "os"
|
||||||
|
|
||||||
|
describe("createWriteExistingFileGuardHook", () => {
|
||||||
|
let tempDir: string
|
||||||
|
let ctx: { directory: string }
|
||||||
|
let hook: ReturnType<typeof createWriteExistingFileGuardHook>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "write-guard-test-"))
|
||||||
|
ctx = { directory: tempDir }
|
||||||
|
hook = createWriteExistingFileGuardHook(ctx as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("tool.execute.before", () => {
|
||||||
|
test("allows write to non-existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const nonExistingFile = path.join(tempDir, "new-file.txt")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: nonExistingFile, content: "hello" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("blocks write to existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "existing-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: existingFile, content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("blocks write tool (lowercase) to existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "existing-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: existingFile, content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores non-write tools", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "existing-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "Edit", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: existingFile, content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores tools without any file path arg", async () => {
|
||||||
|
//#given
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { command: "ls" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("alternative arg names", () => {
|
||||||
|
test("blocks write using 'path' arg to existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "existing-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { path: existingFile, content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("blocks write using 'file_path' arg to existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "existing-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { file_path: existingFile, content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows write using 'path' arg to non-existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const nonExistingFile = path.join(tempDir, "new-file.txt")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { path: nonExistingFile, content: "hello" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows write using 'file_path' arg to non-existing file", async () => {
|
||||||
|
//#given
|
||||||
|
const nonExistingFile = path.join(tempDir, "new-file.txt")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { file_path: nonExistingFile, content: "hello" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("relative path resolution using ctx.directory", () => {
|
||||||
|
test("blocks write to existing file using relative path", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "existing-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: "existing-file.txt", content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("allows write to non-existing file using relative path", async () => {
|
||||||
|
//#given
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: "new-file.txt", content: "hello" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("blocks write to nested relative path when file exists", async () => {
|
||||||
|
//#given
|
||||||
|
const subDir = path.join(tempDir, "subdir")
|
||||||
|
fs.mkdirSync(subDir)
|
||||||
|
const existingFile = path.join(subDir, "existing.txt")
|
||||||
|
fs.writeFileSync(existingFile, "existing content")
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: "subdir/existing.txt", content: "new content" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = hook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses ctx.directory not process.cwd for relative path resolution", async () => {
|
||||||
|
//#given
|
||||||
|
const existingFile = path.join(tempDir, "test-file.txt")
|
||||||
|
fs.writeFileSync(existingFile, "content")
|
||||||
|
const differentCtx = { directory: tempDir }
|
||||||
|
const differentHook = createWriteExistingFileGuardHook(differentCtx as any)
|
||||||
|
const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" }
|
||||||
|
const output = { args: { filePath: "test-file.txt", content: "new" } }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = differentHook["tool.execute.before"]?.(input as any, output as any)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
await expect(result).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
33
src/hooks/write-existing-file-guard/index.ts
Normal file
33
src/hooks/write-existing-file-guard/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import { existsSync } from "fs"
|
||||||
|
import { resolve, isAbsolute } from "path"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
|
||||||
|
export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
|
||||||
|
return {
|
||||||
|
"tool.execute.before": async (input, output) => {
|
||||||
|
const toolName = input.tool?.toLowerCase()
|
||||||
|
if (toolName !== "write") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = output.args as { filePath?: string; path?: string; file_path?: string } | undefined
|
||||||
|
const filePath = args?.filePath ?? args?.path ?? args?.file_path
|
||||||
|
if (!filePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = isAbsolute(filePath) ? filePath : resolve(ctx.directory, filePath)
|
||||||
|
|
||||||
|
if (existsSync(resolvedPath)) {
|
||||||
|
log("[write-existing-file-guard] Blocking write to existing file", {
|
||||||
|
sessionID: input.sessionID,
|
||||||
|
filePath,
|
||||||
|
resolvedPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
throw new Error("File already exists. Use edit tool instead.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/index.compaction-model-agnostic.static.test.ts
Normal file
21
src/index.compaction-model-agnostic.static.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { readFileSync } from "node:fs"
|
||||||
|
|
||||||
|
describe("experimental.session.compacting", () => {
|
||||||
|
test("does not hardcode a model and uses output.context", () => {
|
||||||
|
//#given
|
||||||
|
const indexUrl = new URL("./index.ts", import.meta.url)
|
||||||
|
const content = readFileSync(indexUrl, "utf-8")
|
||||||
|
const hookIndex = content.indexOf('"experimental.session.compacting"')
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const hookSlice = hookIndex >= 0 ? content.slice(hookIndex, hookIndex + 1200) : ""
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(hookIndex).toBeGreaterThanOrEqual(0)
|
||||||
|
expect(content.includes('modelID: "claude-opus-4-6"')).toBe(false)
|
||||||
|
expect(hookSlice.includes("output.context.push")).toBe(true)
|
||||||
|
expect(hookSlice.includes("providerID:")).toBe(false)
|
||||||
|
expect(hookSlice.includes("modelID:")).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
158
src/index.ts
158
src/index.ts
@@ -1,4 +1,5 @@
|
|||||||
import type { Plugin, ToolDefinition } from "@opencode-ai/plugin";
|
import type { Plugin, ToolDefinition } from "@opencode-ai/plugin";
|
||||||
|
import type { AvailableSkill } from "./agents/dynamic-agent-prompt-builder";
|
||||||
import {
|
import {
|
||||||
createTodoContinuationEnforcer,
|
createTodoContinuationEnforcer,
|
||||||
createContextWindowMonitorHook,
|
createContextWindowMonitorHook,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
createUnstableAgentBabysitterHook,
|
createUnstableAgentBabysitterHook,
|
||||||
createPreemptiveCompactionHook,
|
createPreemptiveCompactionHook,
|
||||||
createTasksTodowriteDisablerHook,
|
createTasksTodowriteDisablerHook,
|
||||||
|
createWriteExistingFileGuardHook,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import {
|
import {
|
||||||
contextCollector,
|
contextCollector,
|
||||||
@@ -55,6 +57,7 @@ import {
|
|||||||
discoverOpencodeProjectSkills,
|
discoverOpencodeProjectSkills,
|
||||||
mergeSkills,
|
mergeSkills,
|
||||||
} from "./features/opencode-skill-loader";
|
} from "./features/opencode-skill-loader";
|
||||||
|
import type { SkillScope } from "./features/opencode-skill-loader/types";
|
||||||
import { createBuiltinSkills } from "./features/builtin-skills";
|
import { createBuiltinSkills } from "./features/builtin-skills";
|
||||||
import { getSystemMcpServerNames } from "./features/claude-code-mcp-loader";
|
import { getSystemMcpServerNames } from "./features/claude-code-mcp-loader";
|
||||||
import {
|
import {
|
||||||
@@ -83,6 +86,10 @@ import {
|
|||||||
createTaskList,
|
createTaskList,
|
||||||
createTaskUpdateTool,
|
createTaskUpdateTool,
|
||||||
} from "./tools";
|
} from "./tools";
|
||||||
|
import {
|
||||||
|
CATEGORY_DESCRIPTIONS,
|
||||||
|
DEFAULT_CATEGORIES,
|
||||||
|
} from "./tools/delegate-task/constants";
|
||||||
import { BackgroundManager } from "./features/background-agent";
|
import { BackgroundManager } from "./features/background-agent";
|
||||||
import { SkillMcpManager } from "./features/skill-mcp-manager";
|
import { SkillMcpManager } from "./features/skill-mcp-manager";
|
||||||
import { initTaskToastManager } from "./features/task-toast-manager";
|
import { initTaskToastManager } from "./features/task-toast-manager";
|
||||||
@@ -98,7 +105,9 @@ import {
|
|||||||
getOpenCodeVersion,
|
getOpenCodeVersion,
|
||||||
isOpenCodeVersionAtLeast,
|
isOpenCodeVersionAtLeast,
|
||||||
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
|
OPENCODE_NATIVE_AGENTS_INJECTION_VERSION,
|
||||||
|
injectServerAuthIntoClient,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
import { filterDisabledTools } from "./shared/disabled-tools";
|
||||||
import { loadPluginConfig } from "./plugin-config";
|
import { loadPluginConfig } from "./plugin-config";
|
||||||
import { createModelCacheState } from "./plugin-state";
|
import { createModelCacheState } from "./plugin-state";
|
||||||
import { createConfigHandler } from "./plugin-handlers";
|
import { createConfigHandler } from "./plugin-handlers";
|
||||||
@@ -107,11 +116,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", {
|
log("[OhMyOpenCodePlugin] ENTRY - plugin loading", {
|
||||||
directory: ctx.directory,
|
directory: ctx.directory,
|
||||||
});
|
});
|
||||||
|
injectServerAuthIntoClient(ctx.client);
|
||||||
// Start background tmux check immediately
|
// Start background tmux check immediately
|
||||||
startTmuxCheck();
|
startTmuxCheck();
|
||||||
|
|
||||||
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
const pluginConfig = loadPluginConfig(ctx.directory, ctx);
|
||||||
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||||
|
|
||||||
const firstMessageVariantGate = createFirstMessageVariantGate();
|
const firstMessageVariantGate = createFirstMessageVariantGate();
|
||||||
|
|
||||||
const tmuxConfig = {
|
const tmuxConfig = {
|
||||||
@@ -239,9 +250,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
? createThinkingBlockValidatorHook()
|
? createThinkingBlockValidatorHook()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const categorySkillReminder = isHookEnabled("category-skill-reminder")
|
let categorySkillReminder: ReturnType<typeof createCategorySkillReminderHook> | null = null;
|
||||||
? createCategorySkillReminderHook(ctx)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const ralphLoop = isHookEnabled("ralph-loop")
|
const ralphLoop = isHookEnabled("ralph-loop")
|
||||||
? createRalphLoopHook(ctx, {
|
? createRalphLoopHook(ctx, {
|
||||||
@@ -278,6 +287,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
const questionLabelTruncator = createQuestionLabelTruncatorHook();
|
const questionLabelTruncator = createQuestionLabelTruncatorHook();
|
||||||
const subagentQuestionBlocker = createSubagentQuestionBlockerHook();
|
const subagentQuestionBlocker = createSubagentQuestionBlockerHook();
|
||||||
|
const writeExistingFileGuard = isHookEnabled("write-existing-file-guard")
|
||||||
|
? createWriteExistingFileGuardHook(ctx)
|
||||||
|
: null;
|
||||||
|
|
||||||
const taskResumeInfo = createTaskResumeInfoHook();
|
const taskResumeInfo = createTaskResumeInfoHook();
|
||||||
|
|
||||||
@@ -386,37 +398,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const lookAt = isMultimodalLookerEnabled ? createLookAt(ctx) : null;
|
const lookAt = isMultimodalLookerEnabled ? createLookAt(ctx) : null;
|
||||||
const browserProvider =
|
const browserProvider =
|
||||||
pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
pluginConfig.browser_automation_engine?.provider ?? "playwright";
|
||||||
const delegateTask = createDelegateTask({
|
const disabledSkills = new Set<string>(pluginConfig.disabled_skills ?? []);
|
||||||
manager: backgroundManager,
|
|
||||||
client: ctx.client,
|
|
||||||
directory: ctx.directory,
|
|
||||||
userCategories: pluginConfig.categories,
|
|
||||||
gitMasterConfig: pluginConfig.git_master,
|
|
||||||
sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model,
|
|
||||||
browserProvider,
|
|
||||||
onSyncSessionCreated: async (event) => {
|
|
||||||
log("[index] onSyncSessionCreated callback", {
|
|
||||||
sessionID: event.sessionID,
|
|
||||||
parentID: event.parentID,
|
|
||||||
title: event.title,
|
|
||||||
});
|
|
||||||
await tmuxSessionManager.onSessionCreated({
|
|
||||||
type: "session.created",
|
|
||||||
properties: {
|
|
||||||
info: {
|
|
||||||
id: event.sessionID,
|
|
||||||
parentID: event.parentID,
|
|
||||||
title: event.title,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const disabledSkills = new Set(pluginConfig.disabled_skills ?? []);
|
|
||||||
const systemMcpNames = getSystemMcpServerNames();
|
const systemMcpNames = getSystemMcpServerNames();
|
||||||
const builtinSkills = createBuiltinSkills({ browserProvider }).filter(
|
const builtinSkills = createBuiltinSkills({ browserProvider, disabledSkills }).filter((skill) => {
|
||||||
(skill) => {
|
|
||||||
if (disabledSkills.has(skill.name as never)) return false;
|
|
||||||
if (skill.mcpConfig) {
|
if (skill.mcpConfig) {
|
||||||
for (const mcpName of Object.keys(skill.mcpConfig)) {
|
for (const mcpName of Object.keys(skill.mcpConfig)) {
|
||||||
if (systemMcpNames.has(mcpName)) return false;
|
if (systemMcpNames.has(mcpName)) return false;
|
||||||
@@ -441,6 +425,68 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
projectSkills,
|
projectSkills,
|
||||||
opencodeProjectSkills,
|
opencodeProjectSkills,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function mapScopeToLocation(scope: SkillScope): AvailableSkill["location"] {
|
||||||
|
if (scope === "user" || scope === "opencode") return "user";
|
||||||
|
if (scope === "project" || scope === "opencode-project") return "project";
|
||||||
|
return "plugin";
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableSkills: AvailableSkill[] = mergedSkills.map((skill) => ({
|
||||||
|
name: skill.name,
|
||||||
|
description: skill.definition.description ?? "",
|
||||||
|
location: mapScopeToLocation(skill.scope),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mergedCategories = pluginConfig.categories
|
||||||
|
? { ...DEFAULT_CATEGORIES, ...pluginConfig.categories }
|
||||||
|
: DEFAULT_CATEGORIES;
|
||||||
|
|
||||||
|
const availableCategories = Object.entries(mergedCategories).map(
|
||||||
|
([name, categoryConfig]) => ({
|
||||||
|
name,
|
||||||
|
description:
|
||||||
|
pluginConfig.categories?.[name]?.description
|
||||||
|
?? CATEGORY_DESCRIPTIONS[name]
|
||||||
|
?? "General tasks",
|
||||||
|
model: categoryConfig.model,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const delegateTask = createDelegateTask({
|
||||||
|
manager: backgroundManager,
|
||||||
|
client: ctx.client,
|
||||||
|
directory: ctx.directory,
|
||||||
|
userCategories: pluginConfig.categories,
|
||||||
|
gitMasterConfig: pluginConfig.git_master,
|
||||||
|
sisyphusJuniorModel: pluginConfig.agents?.["sisyphus-junior"]?.model,
|
||||||
|
browserProvider,
|
||||||
|
disabledSkills,
|
||||||
|
availableCategories,
|
||||||
|
availableSkills,
|
||||||
|
onSyncSessionCreated: async (event) => {
|
||||||
|
log("[index] onSyncSessionCreated callback", {
|
||||||
|
sessionID: event.sessionID,
|
||||||
|
parentID: event.parentID,
|
||||||
|
title: event.title,
|
||||||
|
});
|
||||||
|
await tmuxSessionManager.onSessionCreated({
|
||||||
|
type: "session.created",
|
||||||
|
properties: {
|
||||||
|
info: {
|
||||||
|
id: event.sessionID,
|
||||||
|
parentID: event.parentID,
|
||||||
|
title: event.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
categorySkillReminder = isHookEnabled("category-skill-reminder")
|
||||||
|
? createCategorySkillReminderHook(ctx, availableSkills)
|
||||||
|
: null;
|
||||||
|
|
||||||
const skillMcpManager = new SkillMcpManager();
|
const skillMcpManager = new SkillMcpManager();
|
||||||
const getSessionIDForMcp = () => getMainSessionID() || "";
|
const getSessionIDForMcp = () => getMainSessionID() || "";
|
||||||
const skillTool = createSkillTool({
|
const skillTool = createSkillTool({
|
||||||
@@ -448,6 +494,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
mcpManager: skillMcpManager,
|
mcpManager: skillMcpManager,
|
||||||
getSessionID: getSessionIDForMcp,
|
getSessionID: getSessionIDForMcp,
|
||||||
gitMasterConfig: pluginConfig.git_master,
|
gitMasterConfig: pluginConfig.git_master,
|
||||||
|
disabledSkills
|
||||||
});
|
});
|
||||||
const skillMcpTool = createSkillMcpTool({
|
const skillMcpTool = createSkillMcpTool({
|
||||||
manager: skillMcpManager,
|
manager: skillMcpManager,
|
||||||
@@ -481,19 +528,26 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
const allTools: Record<string, ToolDefinition> = {
|
||||||
|
...builtinTools,
|
||||||
|
...backgroundTools,
|
||||||
|
call_omo_agent: callOmoAgent,
|
||||||
|
...(lookAt ? { look_at: lookAt } : {}),
|
||||||
|
delegate_task: delegateTask,
|
||||||
|
skill: skillTool,
|
||||||
|
skill_mcp: skillMcpTool,
|
||||||
|
slashcommand: slashcommandTool,
|
||||||
|
interactive_bash,
|
||||||
|
...taskToolsRecord,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredTools: Record<string, ToolDefinition> = filterDisabledTools(
|
||||||
|
allTools,
|
||||||
|
pluginConfig.disabled_tools,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tool: {
|
tool: filteredTools,
|
||||||
...builtinTools,
|
|
||||||
...backgroundTools,
|
|
||||||
call_omo_agent: callOmoAgent,
|
|
||||||
...(lookAt ? { look_at: lookAt } : {}),
|
|
||||||
delegate_task: delegateTask,
|
|
||||||
skill: skillTool,
|
|
||||||
skill_mcp: skillMcpTool,
|
|
||||||
slashcommand: slashcommandTool,
|
|
||||||
interactive_bash,
|
|
||||||
...taskToolsRecord,
|
|
||||||
},
|
|
||||||
|
|
||||||
"chat.message": async (input, output) => {
|
"chat.message": async (input, output) => {
|
||||||
if (input.agent) {
|
if (input.agent) {
|
||||||
@@ -718,6 +772,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
await subagentQuestionBlocker["tool.execute.before"]?.(input, output);
|
await subagentQuestionBlocker["tool.execute.before"]?.(input, output);
|
||||||
|
await writeExistingFileGuard?.["tool.execute.before"]?.(input, output);
|
||||||
await questionLabelTruncator["tool.execute.before"]?.(input, output);
|
await questionLabelTruncator["tool.execute.before"]?.(input, output);
|
||||||
await claudeCodeHooks["tool.execute.before"](input, output);
|
await claudeCodeHooks["tool.execute.before"](input, output);
|
||||||
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
||||||
@@ -835,17 +890,14 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
await taskResumeInfo["tool.execute.after"](input, output);
|
await taskResumeInfo["tool.execute.after"](input, output);
|
||||||
},
|
},
|
||||||
|
|
||||||
"experimental.session.compacting": async (input: { sessionID: string }) => {
|
"experimental.session.compacting": async (
|
||||||
|
_input: { sessionID: string },
|
||||||
|
output: { context: string[] },
|
||||||
|
): Promise<void> => {
|
||||||
if (!compactionContextInjector) {
|
if (!compactionContextInjector) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await compactionContextInjector({
|
output.context.push(compactionContextInjector());
|
||||||
sessionID: input.sessionID,
|
|
||||||
providerID: "anthropic",
|
|
||||||
modelID: "claude-opus-4-5",
|
|
||||||
usageRatio: 0.8,
|
|
||||||
directory: ctx.directory,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Tier 1 of three-tier MCP system: 3 built-in remote HTTP MCPs.
|
|||||||
```
|
```
|
||||||
mcp/
|
mcp/
|
||||||
├── index.ts # createBuiltinMcps() factory
|
├── index.ts # createBuiltinMcps() factory
|
||||||
├── websearch.ts # Exa AI web search
|
├── websearch.ts # Exa AI / Tavily web search
|
||||||
├── context7.ts # Library documentation
|
├── context7.ts # Library documentation
|
||||||
├── grep-app.ts # GitHub code search
|
├── grep-app.ts # GitHub code search
|
||||||
├── types.ts # McpNameSchema
|
├── types.ts # McpNameSchema
|
||||||
@@ -25,15 +25,24 @@ mcp/
|
|||||||
|
|
||||||
| Name | URL | Purpose | Auth |
|
| Name | URL | Purpose | Auth |
|
||||||
|------|-----|---------|------|
|
|------|-----|---------|------|
|
||||||
| websearch | mcp.exa.ai/mcp?tools=web_search_exa | Real-time web search | EXA_API_KEY |
|
| websearch | mcp.exa.ai / mcp.tavily.com | Real-time web search | EXA_API_KEY / TAVILY_API_KEY |
|
||||||
| context7 | mcp.context7.com/mcp | Library docs | CONTEXT7_API_KEY |
|
| context7 | mcp.context7.com/mcp | Library docs | CONTEXT7_API_KEY (optional) |
|
||||||
| grep_app | mcp.grep.app | GitHub code search | None |
|
| grep_app | mcp.grep.app | GitHub code search | None |
|
||||||
|
|
||||||
## THREE-TIER MCP SYSTEM
|
## Websearch Provider Configuration
|
||||||
|
|
||||||
1. **Built-in** (this directory): websearch, context7, grep_app
|
| Provider | URL | Auth | API Key Required |
|
||||||
2. **Claude Code compat**: `.mcp.json` with `${VAR}` expansion
|
|----------|-----|------|------------------|
|
||||||
3. **Skill-embedded**: YAML frontmatter in skills (handled by skill-mcp-manager)
|
| exa (default) | mcp.exa.ai | x-api-key header | No (optional) |
|
||||||
|
| tavily | mcp.tavily.com | Authorization Bearer | Yes |
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"websearch": {
|
||||||
|
"provider": "tavily" // or "exa" (default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## CONFIG PATTERN
|
## CONFIG PATTERN
|
||||||
|
|
||||||
@@ -47,15 +56,6 @@ export const mcp_name = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## USAGE
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createBuiltinMcps } from "./mcp"
|
|
||||||
|
|
||||||
const mcps = createBuiltinMcps() // Enable all
|
|
||||||
const mcps = createBuiltinMcps(["websearch"]) // Disable specific
|
|
||||||
```
|
|
||||||
|
|
||||||
## HOW TO ADD
|
## HOW TO ADD
|
||||||
|
|
||||||
1. Create `src/mcp/my-mcp.ts`
|
1. Create `src/mcp/my-mcp.ts`
|
||||||
@@ -66,5 +66,5 @@ const mcps = createBuiltinMcps(["websearch"]) // Disable specific
|
|||||||
|
|
||||||
- **Remote only**: HTTP/SSE, no stdio
|
- **Remote only**: HTTP/SSE, no stdio
|
||||||
- **Disable**: User can set `disabled_mcps: ["name"]` in config
|
- **Disable**: User can set `disabled_mcps: ["name"]` in config
|
||||||
- **Context7**: Optional auth using `CONTEXT7_API_KEY` env var
|
- **Exa**: Default provider, works without API key
|
||||||
- **Exa**: Optional auth using `EXA_API_KEY` env var
|
- **Tavily**: Requires `TAVILY_API_KEY` env var
|
||||||
|
|||||||
@@ -83,4 +83,24 @@ describe("createBuiltinMcps", () => {
|
|||||||
expect(result).toHaveProperty("grep_app")
|
expect(result).toHaveProperty("grep_app")
|
||||||
expect(Object.keys(result)).toHaveLength(3)
|
expect(Object.keys(result)).toHaveLength(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("should not throw when websearch disabled even if tavily configured without API key", () => {
|
||||||
|
// given
|
||||||
|
const originalTavilyKey = process.env.TAVILY_API_KEY
|
||||||
|
delete process.env.TAVILY_API_KEY
|
||||||
|
const disabledMcps = ["websearch"]
|
||||||
|
const config = { websearch: { provider: "tavily" as const } }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// when
|
||||||
|
const createMcps = () => createBuiltinMcps(disabledMcps, config)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(createMcps).not.toThrow()
|
||||||
|
const result = createMcps()
|
||||||
|
expect(result).not.toHaveProperty("websearch")
|
||||||
|
} finally {
|
||||||
|
if (originalTavilyKey) process.env.TAVILY_API_KEY = originalTavilyKey
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { websearch } from "./websearch"
|
import { createWebsearchConfig } from "./websearch"
|
||||||
import { context7 } from "./context7"
|
import { context7 } from "./context7"
|
||||||
import { grep_app } from "./grep-app"
|
import { grep_app } from "./grep-app"
|
||||||
import type { McpName } from "./types"
|
import type { McpName } from "./types"
|
||||||
|
import type { OhMyOpenCodeConfig } from "../config/schema"
|
||||||
|
|
||||||
export { McpNameSchema, type McpName } from "./types"
|
export { McpNameSchema, type McpName } from "./types"
|
||||||
|
|
||||||
@@ -13,19 +14,19 @@ type RemoteMcpConfig = {
|
|||||||
oauth?: false
|
oauth?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const allBuiltinMcps: Record<McpName, RemoteMcpConfig> = {
|
export function createBuiltinMcps(disabledMcps: string[] = [], config?: OhMyOpenCodeConfig) {
|
||||||
websearch,
|
|
||||||
context7,
|
|
||||||
grep_app,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBuiltinMcps(disabledMcps: string[] = []) {
|
|
||||||
const mcps: Record<string, RemoteMcpConfig> = {}
|
const mcps: Record<string, RemoteMcpConfig> = {}
|
||||||
|
|
||||||
for (const [name, config] of Object.entries(allBuiltinMcps)) {
|
if (!disabledMcps.includes("websearch")) {
|
||||||
if (!disabledMcps.includes(name)) {
|
mcps.websearch = createWebsearchConfig(config?.websearch)
|
||||||
mcps[name] = config
|
}
|
||||||
}
|
|
||||||
|
if (!disabledMcps.includes("context7")) {
|
||||||
|
mcps.context7 = context7
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!disabledMcps.includes("grep_app")) {
|
||||||
|
mcps.grep_app = grep_app
|
||||||
}
|
}
|
||||||
|
|
||||||
return mcps
|
return mcps
|
||||||
|
|||||||
116
src/mcp/websearch.test.ts
Normal file
116
src/mcp/websearch.test.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach } from "bun:test"
|
||||||
|
import { createWebsearchConfig } from "./websearch"
|
||||||
|
|
||||||
|
describe("websearch MCP provider configuration", () => {
|
||||||
|
const originalEnv = { ...process.env }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env.EXA_API_KEY
|
||||||
|
delete process.env.TAVILY_API_KEY
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...originalEnv }
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns Exa config when no config provided", () => {
|
||||||
|
//#given - no config
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.url).toContain("mcp.exa.ai")
|
||||||
|
expect(result.type).toBe("remote")
|
||||||
|
expect(result.enabled).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns Exa config when provider is 'exa'", () => {
|
||||||
|
//#given
|
||||||
|
const config = { provider: "exa" as const }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.url).toContain("mcp.exa.ai")
|
||||||
|
expect(result.type).toBe("remote")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("includes x-api-key header when EXA_API_KEY is set", () => {
|
||||||
|
//#given
|
||||||
|
const apiKey = "test-exa-key-12345"
|
||||||
|
process.env.EXA_API_KEY = apiKey
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.headers).toEqual({ "x-api-key": apiKey })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns Tavily config when provider is 'tavily' and TAVILY_API_KEY set", () => {
|
||||||
|
//#given
|
||||||
|
const tavilyKey = "test-tavily-key-67890"
|
||||||
|
process.env.TAVILY_API_KEY = tavilyKey
|
||||||
|
const config = { provider: "tavily" as const }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.url).toContain("mcp.tavily.com")
|
||||||
|
expect(result.headers).toEqual({ Authorization: `Bearer ${tavilyKey}` })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws error when provider is 'tavily' but TAVILY_API_KEY missing", () => {
|
||||||
|
//#given
|
||||||
|
delete process.env.TAVILY_API_KEY
|
||||||
|
const config = { provider: "tavily" as const }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const createTavilyConfig = () => createWebsearchConfig(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(createTavilyConfig).toThrow("TAVILY_API_KEY environment variable is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns Exa when both keys present but no explicit provider", () => {
|
||||||
|
//#given
|
||||||
|
process.env.EXA_API_KEY = "test-exa-key"
|
||||||
|
process.env.TAVILY_API_KEY = "test-tavily-key"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.url).toContain("mcp.exa.ai")
|
||||||
|
expect(result.headers).toEqual({ "x-api-key": "test-exa-key" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Tavily config uses Authorization Bearer header format", () => {
|
||||||
|
//#given
|
||||||
|
const tavilyKey = "tavily-secret-key-xyz"
|
||||||
|
process.env.TAVILY_API_KEY = tavilyKey
|
||||||
|
const config = { provider: "tavily" as const }
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig(config)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.headers?.Authorization).toMatch(/^Bearer /)
|
||||||
|
expect(result.headers?.Authorization).toBe(`Bearer ${tavilyKey}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Exa config has no headers when EXA_API_KEY not set", () => {
|
||||||
|
//#given
|
||||||
|
delete process.env.EXA_API_KEY
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = createWebsearchConfig()
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.url).toContain("mcp.exa.ai")
|
||||||
|
expect(result.headers).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,10 +1,44 @@
|
|||||||
export const websearch = {
|
import type { WebsearchConfig } from "../config/schema"
|
||||||
type: "remote" as const,
|
|
||||||
url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
|
type RemoteMcpConfig = {
|
||||||
enabled: true,
|
type: "remote"
|
||||||
headers: process.env.EXA_API_KEY
|
url: string
|
||||||
? { "x-api-key": process.env.EXA_API_KEY }
|
enabled: boolean
|
||||||
: undefined,
|
headers?: Record<string, string>
|
||||||
// Disable OAuth auto-detection - Exa uses API key header, not OAuth
|
oauth?: false
|
||||||
oauth: false as const,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createWebsearchConfig(config?: WebsearchConfig): RemoteMcpConfig {
|
||||||
|
const provider = config?.provider || "exa"
|
||||||
|
|
||||||
|
if (provider === "tavily") {
|
||||||
|
const tavilyKey = process.env.TAVILY_API_KEY
|
||||||
|
if (!tavilyKey) {
|
||||||
|
throw new Error("TAVILY_API_KEY environment variable is required for Tavily provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "remote" as const,
|
||||||
|
url: "https://mcp.tavily.com/mcp/",
|
||||||
|
enabled: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tavilyKey}`,
|
||||||
|
},
|
||||||
|
oauth: false as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to Exa
|
||||||
|
return {
|
||||||
|
type: "remote" as const,
|
||||||
|
url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
|
||||||
|
enabled: true,
|
||||||
|
headers: process.env.EXA_API_KEY
|
||||||
|
? { "x-api-key": process.env.EXA_API_KEY }
|
||||||
|
: undefined,
|
||||||
|
oauth: false as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility: export static instance using default config
|
||||||
|
export const websearch = createWebsearchConfig()
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ beforeEach(() => {
|
|||||||
oracle: { name: "oracle", prompt: "test", mode: "subagent" },
|
oracle: { name: "oracle", prompt: "test", mode: "subagent" },
|
||||||
})
|
})
|
||||||
|
|
||||||
spyOn(sisyphusJunior, "createSisyphusJuniorAgentWithOverrides" as any).mockReturnValue({
|
|
||||||
name: "sisyphus-junior",
|
|
||||||
prompt: "test",
|
|
||||||
mode: "subagent",
|
|
||||||
})
|
|
||||||
|
|
||||||
spyOn(commandLoader, "loadUserCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, "loadUserCommands" as any).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({})
|
||||||
@@ -63,7 +57,7 @@ beforeEach(() => {
|
|||||||
spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({})
|
spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({})
|
||||||
|
|
||||||
spyOn(shared, "log" as any).mockImplementation(() => {})
|
spyOn(shared, "log" as any).mockImplementation(() => {})
|
||||||
spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set(["anthropic/claude-opus-4-5"]))
|
spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set(["anthropic/claude-opus-4-6"]))
|
||||||
spyOn(shared, "readConnectedProvidersCache" as any).mockReturnValue(null)
|
spyOn(shared, "readConnectedProvidersCache" as any).mockReturnValue(null)
|
||||||
|
|
||||||
spyOn(configDir, "getOpenCodeConfigPaths" as any).mockReturnValue({
|
spyOn(configDir, "getOpenCodeConfigPaths" as any).mockReturnValue({
|
||||||
@@ -73,7 +67,7 @@ beforeEach(() => {
|
|||||||
|
|
||||||
spyOn(permissionCompat, "migrateAgentConfig" as any).mockImplementation((config: Record<string, unknown>) => config)
|
spyOn(permissionCompat, "migrateAgentConfig" as any).mockImplementation((config: Record<string, unknown>) => config)
|
||||||
|
|
||||||
spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-5" })
|
spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6" })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -105,6 +99,66 @@ afterEach(() => {
|
|||||||
;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.()
|
;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Sisyphus-Junior model inheritance", () => {
|
||||||
|
test("does not inherit UI-selected model as system default", async () => {
|
||||||
|
// #given
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "opencode/kimi-k2.5-free",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
const agentConfig = config.agent as Record<string, { model?: string }>
|
||||||
|
expect(agentConfig["sisyphus-junior"]?.model).toBe(
|
||||||
|
sisyphusJunior.SISYPHUS_JUNIOR_DEFAULTS.model
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses explicitly configured sisyphus-junior model", async () => {
|
||||||
|
// #given
|
||||||
|
const pluginConfig: OhMyOpenCodeConfig = {
|
||||||
|
agents: {
|
||||||
|
"sisyphus-junior": {
|
||||||
|
model: "openai/gpt-5.3-codex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
model: "opencode/kimi-k2.5-free",
|
||||||
|
agent: {},
|
||||||
|
}
|
||||||
|
const handler = createConfigHandler({
|
||||||
|
ctx: { directory: "/tmp" },
|
||||||
|
pluginConfig,
|
||||||
|
modelCacheState: {
|
||||||
|
anthropicContext1MEnabled: false,
|
||||||
|
modelContextLimitsCache: new Map(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await handler(config)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
const agentConfig = config.agent as Record<string, { model?: string }>
|
||||||
|
expect(agentConfig["sisyphus-junior"]?.model).toBe(
|
||||||
|
"openai/gpt-5.3-codex"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Plan agent demote behavior", () => {
|
describe("Plan agent demote behavior", () => {
|
||||||
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
||||||
// #given
|
// #given
|
||||||
@@ -123,7 +177,7 @@ describe("Plan agent demote behavior", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -154,7 +208,7 @@ describe("Plan agent demote behavior", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {
|
agent: {
|
||||||
plan: {
|
plan: {
|
||||||
name: "plan",
|
name: "plan",
|
||||||
@@ -191,7 +245,7 @@ describe("Plan agent demote behavior", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {
|
agent: {
|
||||||
plan: {
|
plan: {
|
||||||
name: "plan",
|
name: "plan",
|
||||||
@@ -228,7 +282,7 @@ describe("Plan agent demote behavior", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -263,7 +317,7 @@ describe("Agent permission defaults", () => {
|
|||||||
})
|
})
|
||||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -295,7 +349,7 @@ describe("Prometheus category config resolution", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(config).toBeDefined()
|
expect(config).toBeDefined()
|
||||||
expect(config?.model).toBe("openai/gpt-5.2-codex")
|
expect(config?.model).toBe("openai/gpt-5.3-codex")
|
||||||
expect(config?.variant).toBe("xhigh")
|
expect(config?.variant).toBe("xhigh")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -355,7 +409,7 @@ describe("Prometheus category config resolution", () => {
|
|||||||
|
|
||||||
// then - falls back to DEFAULT_CATEGORIES
|
// then - falls back to DEFAULT_CATEGORIES
|
||||||
expect(config).toBeDefined()
|
expect(config).toBeDefined()
|
||||||
expect(config?.model).toBe("openai/gpt-5.2-codex")
|
expect(config?.model).toBe("openai/gpt-5.3-codex")
|
||||||
expect(config?.variant).toBe("xhigh")
|
expect(config?.variant).toBe("xhigh")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -406,7 +460,7 @@ describe("Prometheus direct override priority over category", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -446,7 +500,7 @@ describe("Prometheus direct override priority over category", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -487,7 +541,7 @@ describe("Prometheus direct override priority over category", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -522,7 +576,7 @@ describe("Prometheus direct override priority over category", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const handler = createConfigHandler({
|
const handler = createConfigHandler({
|
||||||
@@ -560,7 +614,7 @@ describe("Deadlock prevention - fetchAvailableModels must not receive client", (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
const config: Record<string, unknown> = {
|
const config: Record<string, unknown> = {
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-6",
|
||||||
agent: {},
|
agent: {},
|
||||||
}
|
}
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
// config.model represents the currently active model in OpenCode (including UI selection)
|
// config.model represents the currently active model in OpenCode (including UI selection)
|
||||||
// Pass it as uiSelectedModel so it takes highest priority in model resolution
|
// Pass it as uiSelectedModel so it takes highest priority in model resolution
|
||||||
const currentModel = config.model as string | undefined;
|
const currentModel = config.model as string | undefined;
|
||||||
|
const disabledSkills = new Set<string>(pluginConfig.disabled_skills ?? []);
|
||||||
const builtinAgents = await createBuiltinAgents(
|
const builtinAgents = await createBuiltinAgents(
|
||||||
migratedDisabledAgents,
|
migratedDisabledAgents,
|
||||||
pluginConfig.agents,
|
pluginConfig.agents,
|
||||||
@@ -167,7 +168,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
allDiscoveredSkills,
|
allDiscoveredSkills,
|
||||||
ctx.client,
|
ctx.client,
|
||||||
browserProvider,
|
browserProvider,
|
||||||
currentModel // uiSelectedModel - takes highest priority
|
currentModel, // uiSelectedModel - takes highest priority
|
||||||
|
disabledSkills
|
||||||
);
|
);
|
||||||
|
|
||||||
// Claude Code agents: Do NOT apply permission migration
|
// Claude Code agents: Do NOT apply permission migration
|
||||||
@@ -220,7 +222,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
|
|
||||||
agentConfig["sisyphus-junior"] = createSisyphusJuniorAgentWithOverrides(
|
agentConfig["sisyphus-junior"] = createSisyphusJuniorAgentWithOverrides(
|
||||||
pluginConfig.agents?.["sisyphus-junior"],
|
pluginConfig.agents?.["sisyphus-junior"],
|
||||||
config.model as string | undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
if (builderEnabled) {
|
if (builderEnabled) {
|
||||||
@@ -305,7 +307,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
prompt: PROMETHEUS_SYSTEM_PROMPT,
|
prompt: PROMETHEUS_SYSTEM_PROMPT,
|
||||||
permission: PROMETHEUS_PERMISSION,
|
permission: PROMETHEUS_PERMISSION,
|
||||||
description: `${configAgent?.plan?.description ?? "Plan agent"} (Prometheus - OhMyOpenCode)`,
|
description: `${configAgent?.plan?.description ?? "Plan agent"} (Prometheus - OhMyOpenCode)`,
|
||||||
color: (configAgent?.plan?.color as string) ?? "#9D4EDD", // Amethyst Purple - wisdom/foresight
|
color: (configAgent?.plan?.color as string) ?? "#FF5722", // Deep Orange - Fire/Flame theme
|
||||||
...(temperatureToUse !== undefined ? { temperature: temperatureToUse } : {}),
|
...(temperatureToUse !== undefined ? { temperature: temperatureToUse } : {}),
|
||||||
...(topPToUse !== undefined ? { top_p: topPToUse } : {}),
|
...(topPToUse !== undefined ? { top_p: topPToUse } : {}),
|
||||||
...(maxTokensToUse !== undefined ? { maxTokens: maxTokensToUse } : {}),
|
...(maxTokensToUse !== undefined ? { maxTokens: maxTokensToUse } : {}),
|
||||||
@@ -358,7 +360,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
: {};
|
: {};
|
||||||
|
|
||||||
const planDemoteConfig = shouldDemotePlan
|
const planDemoteConfig = shouldDemotePlan
|
||||||
? { mode: "subagent" as const }
|
? { mode: "subagent" as const
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
config.agent = {
|
config.agent = {
|
||||||
@@ -447,7 +450,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
: { servers: {} };
|
: { servers: {} };
|
||||||
|
|
||||||
config.mcp = {
|
config.mcp = {
|
||||||
...createBuiltinMcps(pluginConfig.disabled_mcps),
|
...createBuiltinMcps(pluginConfig.disabled_mcps, pluginConfig),
|
||||||
...(config.mcp as Record<string, unknown>),
|
...(config.mcp as Record<string, unknown>),
|
||||||
...mcpResult.servers,
|
...mcpResult.servers,
|
||||||
...pluginComponents.mcpServers,
|
...pluginComponents.mcpServers,
|
||||||
|
|||||||
@@ -9,22 +9,3 @@ export function createModelCacheState(): ModelCacheState {
|
|||||||
anthropicContext1MEnabled: false,
|
anthropicContext1MEnabled: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModelLimit(
|
|
||||||
state: ModelCacheState,
|
|
||||||
providerID: string,
|
|
||||||
modelID: string
|
|
||||||
): number | undefined {
|
|
||||||
const key = `${providerID}/${modelID}`;
|
|
||||||
const cached = state.modelContextLimitsCache.get(key);
|
|
||||||
if (cached) return cached;
|
|
||||||
|
|
||||||
if (
|
|
||||||
providerID === "anthropic" &&
|
|
||||||
state.anthropicContext1MEnabled &&
|
|
||||||
modelID.includes("sonnet")
|
|
||||||
) {
|
|
||||||
return 1_000_000;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
```
|
```
|
||||||
shared/
|
shared/
|
||||||
├── tmux/ # Tmux TUI integration (types, utils, constants)
|
├── tmux/ # Tmux TUI integration (types, utils 312 lines, constants)
|
||||||
├── logger.ts # File-based logging (/tmp/oh-my-opencode.log) - 53 imports
|
├── logger.ts # File-based logging (/tmp/oh-my-opencode.log) - 53 imports
|
||||||
├── dynamic-truncator.ts # Token-aware context window management (194 lines)
|
├── dynamic-truncator.ts # Token-aware context window management (194 lines)
|
||||||
├── model-resolver.ts # 3-step resolution (Override → Fallback → Default)
|
├── model-resolver.ts # 3-step resolution (Override → Fallback → Default)
|
||||||
├── model-requirements.ts # Agent/category model fallback chains (162 lines)
|
├── model-requirements.ts # Agent/category model fallback chains (162 lines)
|
||||||
├── model-availability.ts # Provider model fetching & fuzzy matching (154 lines)
|
├── model-availability.ts # Provider model fetching & fuzzy matching (357 lines)
|
||||||
|
├── model-sanitizer.ts # Model name sanitization
|
||||||
|
├── model-suggestion-retry.ts # Model suggestion on failure
|
||||||
├── jsonc-parser.ts # JSONC parsing with comment support
|
├── jsonc-parser.ts # JSONC parsing with comment support
|
||||||
├── frontmatter.ts # YAML frontmatter extraction (JSON_SCHEMA only) - 9 imports
|
├── frontmatter.ts # YAML frontmatter extraction (JSON_SCHEMA only) - 9 imports
|
||||||
├── data-path.ts # XDG-compliant storage resolution
|
├── data-path.ts # XDG-compliant storage resolution
|
||||||
@@ -22,16 +24,26 @@ shared/
|
|||||||
├── claude-config-dir.ts # ~/.claude resolution - 9 imports
|
├── claude-config-dir.ts # ~/.claude resolution - 9 imports
|
||||||
├── migration.ts # Legacy config migration logic (231 lines)
|
├── migration.ts # Legacy config migration logic (231 lines)
|
||||||
├── opencode-version.ts # Semantic version comparison
|
├── opencode-version.ts # Semantic version comparison
|
||||||
├── permission-compat.ts # Agent tool restriction enforcement
|
├── permission-compat.ts # Agent tool restriction enforcement - 6 imports
|
||||||
├── system-directive.ts # Unified system message prefix & types
|
├── system-directive.ts # Unified system message prefix & types - 8 imports
|
||||||
├── session-utils.ts # Session cursor, orchestrator detection
|
├── session-utils.ts # Session cursor, orchestrator detection
|
||||||
|
├── session-cursor.ts # Session message cursor tracking
|
||||||
├── shell-env.ts # Cross-platform shell environment
|
├── shell-env.ts # Cross-platform shell environment
|
||||||
├── agent-variant.ts # Agent variant from config
|
├── agent-variant.ts # Agent variant from config
|
||||||
├── zip-extractor.ts # Binary/Resource ZIP extraction
|
├── zip-extractor.ts # Binary/Resource ZIP extraction
|
||||||
├── deep-merge.ts # Recursive object merging (proto-pollution safe, MAX_DEPTH=50)
|
├── deep-merge.ts # Recursive object merging (proto-pollution safe, MAX_DEPTH=50)
|
||||||
├── case-insensitive.ts # Case-insensitive object lookups
|
├── case-insensitive.ts # Case-insensitive object lookups
|
||||||
├── session-cursor.ts # Session message cursor tracking
|
|
||||||
├── command-executor.ts # Shell command execution (225 lines)
|
├── command-executor.ts # Shell command execution (225 lines)
|
||||||
|
├── snake-case.ts # Case conversion utilities
|
||||||
|
├── tool-name.ts # Tool naming conventions
|
||||||
|
├── pattern-matcher.ts # Pattern matching utilities
|
||||||
|
├── port-utils.ts # Port management
|
||||||
|
├── file-utils.ts # File operation utilities
|
||||||
|
├── file-reference-resolver.ts # File reference resolution
|
||||||
|
├── connected-providers-cache.ts # Provider caching
|
||||||
|
├── external-plugin-detector.ts # Plugin detection
|
||||||
|
├── first-message-variant.ts # Message variant types
|
||||||
|
├── opencode-server-auth.ts # Authentication utilities
|
||||||
└── index.ts # Barrel export for all utilities
|
└── index.ts # Barrel export for all utilities
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ describe("Agent Config Integration", () => {
|
|||||||
test("migrates old format agent keys to lowercase", () => {
|
test("migrates old format agent keys to lowercase", () => {
|
||||||
// given - config with old format keys
|
// given - config with old format keys
|
||||||
const oldConfig = {
|
const oldConfig = {
|
||||||
Sisyphus: { model: "anthropic/claude-opus-4-5" },
|
Sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||||
Atlas: { model: "anthropic/claude-opus-4-5" },
|
Atlas: { model: "anthropic/claude-opus-4-6" },
|
||||||
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-5" },
|
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-6" },
|
||||||
"Metis (Plan Consultant)": { model: "anthropic/claude-sonnet-4-5" },
|
"Metis (Plan Consultant)": { model: "anthropic/claude-sonnet-4-5" },
|
||||||
"Momus (Plan Reviewer)": { model: "anthropic/claude-sonnet-4-5" },
|
"Momus (Plan Reviewer)": { model: "anthropic/claude-sonnet-4-5" },
|
||||||
}
|
}
|
||||||
@@ -33,9 +33,9 @@ describe("Agent Config Integration", () => {
|
|||||||
expect(result.migrated).not.toHaveProperty("Momus (Plan Reviewer)")
|
expect(result.migrated).not.toHaveProperty("Momus (Plan Reviewer)")
|
||||||
|
|
||||||
// then - values are preserved
|
// then - values are preserved
|
||||||
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-5" })
|
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-6" })
|
||||||
expect(result.migrated.atlas).toEqual({ model: "anthropic/claude-opus-4-5" })
|
expect(result.migrated.atlas).toEqual({ model: "anthropic/claude-opus-4-6" })
|
||||||
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-5" })
|
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-6" })
|
||||||
|
|
||||||
// then - changed flag is true
|
// then - changed flag is true
|
||||||
expect(result.changed).toBe(true)
|
expect(result.changed).toBe(true)
|
||||||
@@ -44,7 +44,7 @@ describe("Agent Config Integration", () => {
|
|||||||
test("preserves already lowercase keys", () => {
|
test("preserves already lowercase keys", () => {
|
||||||
// given - config with lowercase keys
|
// given - config with lowercase keys
|
||||||
const config = {
|
const config = {
|
||||||
sisyphus: { model: "anthropic/claude-opus-4-5" },
|
sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||||
oracle: { model: "openai/gpt-5.2" },
|
oracle: { model: "openai/gpt-5.2" },
|
||||||
librarian: { model: "opencode/glm-4.7-free" },
|
librarian: { model: "opencode/glm-4.7-free" },
|
||||||
}
|
}
|
||||||
@@ -62,9 +62,9 @@ describe("Agent Config Integration", () => {
|
|||||||
test("handles mixed case config", () => {
|
test("handles mixed case config", () => {
|
||||||
// given - config with mixed old and new format
|
// given - config with mixed old and new format
|
||||||
const mixedConfig = {
|
const mixedConfig = {
|
||||||
Sisyphus: { model: "anthropic/claude-opus-4-5" },
|
Sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||||
oracle: { model: "openai/gpt-5.2" },
|
oracle: { model: "openai/gpt-5.2" },
|
||||||
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-5" },
|
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-6" },
|
||||||
librarian: { model: "opencode/glm-4.7-free" },
|
librarian: { model: "opencode/glm-4.7-free" },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ describe("Agent Config Integration", () => {
|
|||||||
test("old config migrates and displays correctly", () => {
|
test("old config migrates and displays correctly", () => {
|
||||||
// given - old format config
|
// given - old format config
|
||||||
const oldConfig = {
|
const oldConfig = {
|
||||||
Sisyphus: { model: "anthropic/claude-opus-4-5", temperature: 0.1 },
|
Sisyphus: { model: "anthropic/claude-opus-4-6", temperature: 0.1 },
|
||||||
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-5" },
|
"Prometheus (Planner)": { model: "anthropic/claude-opus-4-6" },
|
||||||
}
|
}
|
||||||
|
|
||||||
// when - config is migrated
|
// when - config is migrated
|
||||||
@@ -192,15 +192,15 @@ describe("Agent Config Integration", () => {
|
|||||||
expect(prometheusDisplay).toBe("Prometheus (Plan Builder)")
|
expect(prometheusDisplay).toBe("Prometheus (Plan Builder)")
|
||||||
|
|
||||||
// then - config values are preserved
|
// then - config values are preserved
|
||||||
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-5", temperature: 0.1 })
|
expect(result.migrated.sisyphus).toEqual({ model: "anthropic/claude-opus-4-6", temperature: 0.1 })
|
||||||
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-5" })
|
expect(result.migrated.prometheus).toEqual({ model: "anthropic/claude-opus-4-6" })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("new config works without migration", () => {
|
test("new config works without migration", () => {
|
||||||
// given - new format config (already lowercase)
|
// given - new format config (already lowercase)
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
sisyphus: { model: "anthropic/claude-opus-4-5" },
|
sisyphus: { model: "anthropic/claude-opus-4-6" },
|
||||||
atlas: { model: "anthropic/claude-opus-4-5" },
|
atlas: { model: "anthropic/claude-opus-4-6" },
|
||||||
}
|
}
|
||||||
|
|
||||||
// when - migration is applied (should be no-op)
|
// when - migration is applied (should be no-op)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user