Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cab836a3b | ||
|
|
4efa58616f | ||
|
|
fbae3aeb6b | ||
|
|
74da07d584 | ||
|
|
7cd04a246c | ||
|
|
1de7df4933 | ||
|
|
ea6121ee1c | ||
|
|
4939f81625 | ||
|
|
820b339fae | ||
|
|
5412578600 | ||
|
|
502e9f504f | ||
|
|
8c3d413c8a | ||
|
|
b51d0bdf65 | ||
|
|
b2adda6e90 | ||
|
|
0da20f21b0 | ||
|
|
2f1ede072f | ||
|
|
ffeb92eb13 | ||
|
|
d49c221cb1 | ||
|
|
dea17dc3ba | ||
|
|
c6efe70f09 | ||
|
|
8cbdfbaf78 | ||
|
|
7cb3f23c2b | ||
|
|
471cf868ff |
BIN
.github/assets/orchestrator-sisyphus.png
vendored
Normal file
BIN
.github/assets/orchestrator-sisyphus.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 984 KiB |
28
AGENTS.md
28
AGENTS.md
@@ -1,7 +1,7 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2025-12-28T19:26:00+09:00
|
||||
**Commit:** 122e918
|
||||
**Generated:** 2025-12-31T14:05:00+09:00
|
||||
**Commit:** 502e9f5
|
||||
**Branch:** dev
|
||||
|
||||
## OVERVIEW
|
||||
@@ -20,7 +20,8 @@ oh-my-opencode/
|
||||
│ ├── features/ # Claude Code compatibility - see src/features/AGENTS.md
|
||||
│ ├── config/ # Zod schema, TypeScript types
|
||||
│ ├── auth/ # Google Antigravity OAuth (antigravity/)
|
||||
│ ├── shared/ # Utilities: deep-merge, pattern-matcher, logger, etc.
|
||||
│ ├── shared/ # Utilities: deep-merge, pattern-matcher, logger, etc. - see src/shared/AGENTS.md
|
||||
│ ├── cli/ # CLI installer, doctor, run - see src/cli/AGENTS.md
|
||||
│ └── index.ts # Main plugin entry (OhMyOpenCodePlugin)
|
||||
├── script/ # build-schema.ts, publish.ts, generate-changelog.ts
|
||||
├── assets/ # JSON schema
|
||||
@@ -34,7 +35,7 @@ oh-my-opencode/
|
||||
| Add agent | `src/agents/` | Create .ts, add to builtinAgents in index.ts, update types.ts |
|
||||
| Add hook | `src/hooks/` | Create dir with createXXXHook(), export from index.ts |
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts, add to builtinTools |
|
||||
| Add MCP | `src/mcp/` | Create config, add to index.ts |
|
||||
| Add MCP | `src/mcp/` | Create config, add to index.ts and types.ts |
|
||||
| LSP behavior | `src/tools/lsp/` | client.ts (connection), tools.ts (handlers) |
|
||||
| AST-Grep | `src/tools/ast-grep/` | napi.ts for @ast-grep/napi binding |
|
||||
| Google OAuth | `src/auth/antigravity/` | OAuth plugin for Google models |
|
||||
@@ -42,6 +43,9 @@ oh-my-opencode/
|
||||
| Claude Code compat | `src/features/claude-code-*-loader/` | Command, skill, agent, mcp loaders |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts for task management |
|
||||
| Interactive terminal | `src/tools/interactive-bash/` | tmux session management |
|
||||
| CLI installer | `src/cli/install.ts` | Interactive TUI installation |
|
||||
| Doctor checks | `src/cli/doctor/checks/` | Health checks for environment |
|
||||
| Shared utilities | `src/shared/` | Cross-cutting utilities |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
@@ -64,6 +68,8 @@ oh-my-opencode/
|
||||
- **Year 2024**: NEVER use 2024 in code/prompts (use current year)
|
||||
- **Rush completion**: Never mark tasks complete without verification
|
||||
- **Over-exploration**: Stop searching when sufficient context found
|
||||
- **High temperature**: Don't use >0.3 for code-related agents
|
||||
- **Broad tool access**: Prefer explicit `include` over unrestricted access
|
||||
|
||||
## UNIQUE STYLES
|
||||
|
||||
@@ -109,8 +115,19 @@ bun test # Run tests
|
||||
|
||||
## CI PIPELINE
|
||||
|
||||
- **ci.yml**: Parallel test/typecheck, build verification, auto-commit schema on master
|
||||
- **ci.yml**: Parallel test/typecheck, build verification, auto-commit schema on master, rolling `next` draft release
|
||||
- **publish.yml**: Manual workflow_dispatch, version bump, changelog, OIDC npm publish
|
||||
- **sisyphus-agent.yml**: Agent-in-CI for automated issue handling via `@sisyphus-dev-ai` mentions
|
||||
|
||||
## COMPLEXITY HOTSPOTS
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/index.ts` | 690 | Main plugin orchestration, all hook/tool initialization |
|
||||
| `src/hooks/anthropic-context-window-limit-recovery/executor.ts` | 670 | Session compaction, multi-stage recovery pipeline |
|
||||
| `src/cli/config-manager.ts` | 669 | JSONC parsing, environment detection, installation |
|
||||
| `src/auth/antigravity/fetch.ts` | 621 | Token refresh, URL rewriting, endpoint fallbacks |
|
||||
| `src/tools/lsp/client.ts` | 611 | LSP protocol, stdin/stdout buffering, JSON-RPC |
|
||||
|
||||
## NOTES
|
||||
|
||||
@@ -119,3 +136,4 @@ bun test # Run tests
|
||||
- **Multi-lang docs**: README.md (EN), README.ko.md (KO), README.ja.md (JA), README.zh-cn.md (ZH-CN)
|
||||
- **Config**: `~/.config/opencode/oh-my-opencode.json` (user) or `.opencode/oh-my-opencode.json` (project)
|
||||
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
||||
- **JSONC support**: Config files support comments (`// comment`, `/* block */`) and trailing commas
|
||||
|
||||
25
README.ja.md
25
README.ja.md
@@ -660,6 +660,10 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
- **Empty Message Sanitizer**: 空のチャットメッセージによるAPIエラーを防止します。送信前にメッセージ内容を自動的にサニタイズします。
|
||||
- **Grep Output Truncator**: grep は山のようなテキストを返すことがあります。残りのコンテキストウィンドウに応じて動的に出力を切り詰めます—50% の余裕を維持し、最大 50k トークンに制限します。
|
||||
- **Tool Output Truncator**: 同じ考え方をより広範囲に適用します。Grep、Glob、LSP ツール、AST-grep の出力を切り詰めます。一度の冗長な検索がコンテキスト全体を食いつぶすのを防ぎます。
|
||||
- **Preemptive Compaction**: トークン制限に達する前にセッションを事前にコンパクションします。コンテキストウィンドウ使用率85%で実行されます。**デフォルトで有効。** `disabled_hooks: ["preemptive-compaction"]`で無効化できます。
|
||||
- **Compaction Context Injector**: セッションコンパクション中に重要なコンテキスト(AGENTS.md、現在のディレクトリ情報)を保持し、重要な状態を失わないようにします。
|
||||
- **Thinking Block Validator**: thinking ブロックを検証し、適切なフォーマットを確保し、不正な thinking コンテンツによる API エラーを防ぎます。
|
||||
- **Claude Code Hooks**: Claude Code の settings.json からフックを実行します - これは PreToolUse/PostToolUse/UserPromptSubmit/Stop フックを実行する互換性レイヤーです。
|
||||
|
||||
## 設定
|
||||
|
||||
@@ -755,7 +759,19 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
}
|
||||
```
|
||||
|
||||
各エージェントでサポートされるオプション:`model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`。
|
||||
各エージェントでサポートされるオプション:`model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`。
|
||||
|
||||
`prompt_append` を使用すると、デフォルトのシステムプロンプトを置き換えずに追加の指示を付け加えられます:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"librarian": {
|
||||
"prompt_append": "Emacs Lisp のドキュメント検索には常に elisp-dev-mcp を使用してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Sisyphus` (メインオーケストレーター) と `build` (デフォルトエージェント) も同じオプションで設定をオーバーライドできます。
|
||||
|
||||
@@ -874,7 +890,7 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
}
|
||||
```
|
||||
|
||||
利用可能なフック:`todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`
|
||||
利用可能なフック:`todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
|
||||
**`auto-update-checker`と`startup-toast`について**: `startup-toast` フックは `auto-update-checker` のサブ機能です。アップデートチェックは有効なまま起動トースト通知のみを無効化するには、`disabled_hooks` に `"startup-toast"` を追加してください。すべてのアップデートチェック機能(トーストを含む)を無効化するには、`"auto-update-checker"` を追加してください。
|
||||
|
||||
@@ -926,7 +942,7 @@ OpenCode でサポートされるすべての LSP 構成およびカスタム設
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"preemptive_compaction": true,
|
||||
"preemptive_compaction_threshold": 0.85,
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
@@ -936,8 +952,7 @@ OpenCode でサポートされるすべての LSP 構成およびカスタム設
|
||||
|
||||
| オプション | デフォルト | 説明 |
|
||||
| --------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `preemptive_compaction` | `false` | トークン制限に達する前にセッションを事前にコンパクションします。デフォルトでコンテキストウィンドウ使用率80%で実行されます。 |
|
||||
| `preemptive_compaction_threshold` | `0.80` | プリエンプティブコンパクションをトリガーする閾値(0.5-0.95)。`preemptive_compaction`が有効な場合のみ適用されます。 |
|
||||
| `preemptive_compaction_threshold` | `0.85` | プリエンプティブコンパクションをトリガーする閾値(0.5-0.95)。`preemptive-compaction` フックはデフォルトで有効です。このオプションで閾値をカスタマイズできます。 |
|
||||
| `truncate_all_tool_outputs` | `false` | ホワイトリストのツール(Grep、Glob、LSP、AST-grep)だけでなく、すべてのツール出力を切り詰めます。Tool output truncator はデフォルトで有効です - `disabled_hooks`で無効化できます。 |
|
||||
| `aggressive_truncation` | `false` | トークン制限を超えた場合、ツール出力を積極的に切り詰めて制限内に収めます。デフォルトの切り詰めより積極的です。不十分な場合は要約/復元にフォールバックします。 |
|
||||
| `auto_resume` | `false` | thinking block エラーや thinking disabled violation からの回復成功後、自動的にセッションを再開します。最後のユーザーメッセージを抽出して続行します。 |
|
||||
|
||||
23
README.ko.md
23
README.ko.md
@@ -653,7 +653,7 @@ Oh My OpenCode는 다음 위치의 훅을 읽고 실행합니다:
|
||||
- **Empty Message Sanitizer**: 빈 채팅 메시지로 인한 API 오류를 방지합니다. 전송 전 메시지 내용을 자동으로 정리합니다.
|
||||
- **Grep Output Truncator**: grep은 산더미 같은 텍스트를 반환할 수 있습니다. 남은 컨텍스트 윈도우에 따라 동적으로 출력을 축소합니다—50% 여유 공간 유지, 최대 50k 토큰.
|
||||
- **Tool Output Truncator**: 같은 아이디어, 더 넓은 범위. Grep, Glob, LSP 도구, AST-grep의 출력을 축소합니다. 한 번의 장황한 검색이 전체 컨텍스트를 잡아먹는 것을 방지합니다.
|
||||
- **선제적 압축 (Preemptive Compaction)**: 세션 토큰 한계에 도달하기 전에 선제적으로 세션을 압축합니다. 문제가 발생하기 전에 미리 실행됩니다.
|
||||
- **선제적 압축 (Preemptive Compaction)**: 세션 토큰 한계에 도달하기 전에 선제적으로 세션을 압축합니다. 컨텍스트 윈도우 사용량 85%에서 실행됩니다. **기본적으로 활성화됨.** `disabled_hooks: ["preemptive-compaction"]`으로 비활성화 가능.
|
||||
- **압축 컨텍스트 주입기 (Compaction Context Injector)**: 세션 압축 중에 중요한 컨텍스트(AGENTS.md, 현재 디렉토리 정보 등)를 유지하여 중요한 상태를 잃지 않도록 합니다.
|
||||
- **사고 블록 검증기 (Thinking Block Validator)**: 사고(thinking) 블록의 형식이 올바른지 검증하여 잘못된 형식으로 인한 API 오류를 방지합니다.
|
||||
- **Claude Code 훅 (Claude Code Hooks)**: Claude Code의 settings.json에 설정된 훅을 실행합니다. PreToolUse/PostToolUse/UserPromptSubmit/Stop 이벤트를 지원하는 호환성 레이어입니다.
|
||||
@@ -752,7 +752,19 @@ Schema 자동 완성이 지원됩니다:
|
||||
}
|
||||
```
|
||||
|
||||
각 에이전트에서 지원하는 옵션: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
각 에이전트에서 지원하는 옵션: `model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
`prompt_append`를 사용하면 기본 시스템 프롬프트를 대체하지 않고 추가 지시사항을 덧붙일 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"librarian": {
|
||||
"prompt_append": "Emacs Lisp 문서 조회 시 항상 elisp-dev-mcp를 사용하세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Sisyphus` (메인 오케스트레이터)와 `build` (기본 에이전트)도 동일한 옵션으로 설정을 오버라이드할 수 있습니다.
|
||||
|
||||
@@ -871,7 +883,7 @@ Schema 자동 완성이 지원됩니다:
|
||||
}
|
||||
```
|
||||
|
||||
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`
|
||||
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
|
||||
**`auto-update-checker`와 `startup-toast`에 대한 참고사항**: `startup-toast` 훅은 `auto-update-checker`의 하위 기능입니다. 업데이트 확인은 유지하면서 시작 토스트 알림만 비활성화하려면 `disabled_hooks`에 `"startup-toast"`를 추가하세요. 모든 업데이트 확인 기능(토스트 포함)을 비활성화하려면 `"auto-update-checker"`를 추가하세요.
|
||||
|
||||
@@ -923,7 +935,7 @@ OpenCode 에서 지원하는 모든 LSP 구성 및 커스텀 설정 (opencode.js
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"preemptive_compaction": true,
|
||||
"preemptive_compaction_threshold": 0.85,
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
@@ -933,8 +945,7 @@ OpenCode 에서 지원하는 모든 LSP 구성 및 커스텀 설정 (opencode.js
|
||||
|
||||
| 옵션 | 기본값 | 설명 |
|
||||
| --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `preemptive_compaction` | `false` | 토큰 제한에 도달하기 전에 세션을 미리 컴팩션합니다. 기본적으로 컨텍스트 윈도우 사용량이 80%일 때 실행됩니다. |
|
||||
| `preemptive_compaction_threshold` | `0.80` | 선제적 컴팩션을 트리거할 임계값 비율(0.5-0.95). `preemptive_compaction`이 활성화된 경우에만 적용됩니다. |
|
||||
| `preemptive_compaction_threshold` | `0.85` | 선제적 컴팩션을 트리거할 임계값 비율(0.5-0.95). `preemptive-compaction` 훅은 기본적으로 활성화되어 있으며, 이 옵션으로 임계값을 커스터마이즈할 수 있습니다. |
|
||||
| `truncate_all_tool_outputs` | `false` | 화이트리스트 도구(Grep, Glob, LSP, AST-grep)만이 아닌 모든 도구 출력을 잘라냅니다. Tool output truncator는 기본적으로 활성화됩니다 - `disabled_hooks`로 비활성화 가능합니다. |
|
||||
| `aggressive_truncation` | `false` | 토큰 제한을 초과하면 도구 출력을 공격적으로 잘라내어 제한 내에 맞춥니다. 기본 truncation보다 더 공격적입니다. 부족하면 요약/복구로 fallback합니다. |
|
||||
| `auto_resume` | `false` | thinking block 에러나 thinking disabled violation으로부터 성공적으로 복구한 후 자동으로 세션을 재개합니다. 마지막 사용자 메시지를 추출하여 계속합니다. |
|
||||
|
||||
26
README.md
26
README.md
@@ -2,6 +2,9 @@
|
||||
>
|
||||
> *"I aim to spark a software revolution by creating a world where agent-generated code is indistinguishable from human code, yet capable of achieving vastly more. I have poured my personal time, passion, and funds into this journey, and I will continue to do so."*
|
||||
>
|
||||
> [](https://x.com/justsisyphus/status/2006250634354548963)
|
||||
> > **The Orchestrator is coming. This Week. [Get notified on X](https://x.com/justsisyphus/status/2006250634354548963)**
|
||||
>
|
||||
> Be with us!
|
||||
>
|
||||
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | Join our [Discord community](https://discord.gg/PWpXmbhF) to connect with contributors and fellow `oh-my-opencode` users. |
|
||||
@@ -692,7 +695,7 @@ When agents thrive, you thrive. But I want to help you directly too.
|
||||
- **Empty Message Sanitizer**: Prevents API errors from empty chat messages by automatically sanitizing message content before sending.
|
||||
- **Grep Output Truncator**: Grep can return mountains of text. This dynamically truncates output based on your remaining context window—keeps 50% headroom, caps at 50k tokens.
|
||||
- **Tool Output Truncator**: Same idea, broader scope. Truncates output from Grep, Glob, LSP tools, and AST-grep. Prevents one verbose search from eating your entire context.
|
||||
- **Preemptive Compaction**: Compacts session proactively before hitting hard token limits. Runs before you get into trouble.
|
||||
- **Preemptive Compaction**: Compacts session proactively before hitting hard token limits. Runs at 85% context window usage. **Enabled by default.** Disable via `disabled_hooks: ["preemptive-compaction"]`.
|
||||
- **Compaction Context Injector**: Preserves critical context (AGENTS.md, current directory info) during session compaction so you don't lose important state.
|
||||
- **Thinking Block Validator**: Validates thinking blocks to ensure proper formatting and prevent API errors from malformed thinking content.
|
||||
- **Claude Code Hooks**: Executes hooks from Claude Code's settings.json - this is the compatibility layer that runs PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks.
|
||||
@@ -791,7 +794,19 @@ Override built-in agent settings:
|
||||
}
|
||||
```
|
||||
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
Use `prompt_append` to add extra instructions without replacing the default system prompt:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"librarian": {
|
||||
"prompt_append": "Always use the elisp-dev-mcp for Emacs Lisp documentation lookups."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also override settings for `Sisyphus` (the main orchestrator) and `build` (the default agent) using the same options.
|
||||
|
||||
@@ -910,7 +925,7 @@ Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-m
|
||||
}
|
||||
```
|
||||
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
|
||||
**Note on `auto-update-checker` and `startup-toast`**: The `startup-toast` hook is a sub-feature of `auto-update-checker`. To disable only the startup toast notification while keeping update checking enabled, add `"startup-toast"` to `disabled_hooks`. To disable all update checking features (including the toast), add `"auto-update-checker"` to `disabled_hooks`.
|
||||
|
||||
@@ -962,7 +977,7 @@ Opt-in experimental features that may change or be removed in future versions. U
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"preemptive_compaction": true,
|
||||
"preemptive_compaction_threshold": 0.85,
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
@@ -972,8 +987,7 @@ Opt-in experimental features that may change or be removed in future versions. U
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `preemptive_compaction` | `false` | Compacts session proactively before hitting hard token limits. Runs at 80% context window usage by default. |
|
||||
| `preemptive_compaction_threshold` | `0.80` | Threshold percentage (0.5-0.95) to trigger preemptive compaction. Only applies when `preemptive_compaction` is enabled. |
|
||||
| `preemptive_compaction_threshold` | `0.85` | Threshold percentage (0.5-0.95) to trigger preemptive compaction. The `preemptive-compaction` hook is enabled by default; this option customizes the threshold. |
|
||||
| `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. |
|
||||
| `aggressive_truncation` | `false` | When token limit is exceeded, aggressively truncates tool outputs to fit within limits. More aggressive than the default truncation behavior. Falls back to summarize/revert if insufficient. |
|
||||
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts the last user message and continues. |
|
||||
|
||||
@@ -664,6 +664,10 @@ Agent 爽了,你自然也爽。但我还想直接让你爽。
|
||||
- **空消息清理器**:防止发空消息导致 API 报错。发出去之前自动打扫干净。
|
||||
- **Grep 输出截断器**:grep 结果太多?根据剩余窗口动态截断——留 50% 空间,顶天 50k token。
|
||||
- **工具输出截断器**:Grep、Glob、LSP、AST-grep 统统管上。防止一次无脑搜索把上下文撑爆。
|
||||
- **预防性压缩 (Preemptive Compaction)**:在达到 token 限制之前主动压缩会话。在上下文窗口使用率 85% 时运行。**默认启用。** 通过 `disabled_hooks: ["preemptive-compaction"]` 禁用。
|
||||
- **压缩上下文注入器**:会话压缩时保留关键上下文(AGENTS.md、当前目录信息),防止丢失重要状态。
|
||||
- **思考块验证器**:验证 thinking block 以确保格式正确,防止因格式错误的 thinking 内容而导致 API 错误。
|
||||
- **Claude Code Hooks**:执行 Claude Code settings.json 中的 hooks - 这是运行 PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks 的兼容层。
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -759,7 +763,19 @@ Agent 爽了,你自然也爽。但我还想直接让你爽。
|
||||
}
|
||||
```
|
||||
|
||||
每个 Agent 能改这些:`model`、`temperature`、`top_p`、`prompt`、`tools`、`disable`、`description`、`mode`、`color`、`permission`。
|
||||
每个 Agent 能改这些:`model`、`temperature`、`top_p`、`prompt`、`prompt_append`、`tools`、`disable`、`description`、`mode`、`color`、`permission`。
|
||||
|
||||
用 `prompt_append` 可以在默认系统提示后面追加额外指令,不用替换整个提示:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"librarian": {
|
||||
"prompt_append": "查 Emacs Lisp 文档时用 elisp-dev-mcp。"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Sisyphus`(主编排器)和 `build`(默认 Agent)也能改。
|
||||
|
||||
@@ -878,7 +894,7 @@ Sisyphus Agent 也能自定义:
|
||||
}
|
||||
```
|
||||
|
||||
可关的 hook:`todo-continuation-enforcer`、`context-window-monitor`、`session-recovery`、`session-notification`、`comment-checker`、`grep-output-truncator`、`tool-output-truncator`、`directory-agents-injector`、`directory-readme-injector`、`empty-task-response-detector`、`think-mode`、`anthropic-context-window-limit-recovery`、`rules-injector`、`background-notification`、`auto-update-checker`、`startup-toast`、`keyword-detector`、`agent-usage-reminder`、`non-interactive-env`、`interactive-bash-session`、`empty-message-sanitizer`、`compaction-context-injector`、`thinking-block-validator`、`claude-code-hooks`、`ralph-loop`
|
||||
可关的 hook:`todo-continuation-enforcer`、`context-window-monitor`、`session-recovery`、`session-notification`、`comment-checker`、`grep-output-truncator`、`tool-output-truncator`、`directory-agents-injector`、`directory-readme-injector`、`empty-task-response-detector`、`think-mode`、`anthropic-context-window-limit-recovery`、`rules-injector`、`background-notification`、`auto-update-checker`、`startup-toast`、`keyword-detector`、`agent-usage-reminder`、`non-interactive-env`、`interactive-bash-session`、`empty-message-sanitizer`、`compaction-context-injector`、`thinking-block-validator`、`claude-code-hooks`、`ralph-loop`、`preemptive-compaction`
|
||||
|
||||
**关于 `auto-update-checker` 和 `startup-toast`**: `startup-toast` hook 是 `auto-update-checker` 的子功能。若想保持更新检查但只禁用启动提示通知,在 `disabled_hooks` 中添加 `"startup-toast"`。若要禁用所有更新检查功能(包括提示),添加 `"auto-update-checker"`。
|
||||
|
||||
@@ -930,7 +946,7 @@ Oh My OpenCode 送你重构工具(重命名、代码操作)。
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"preemptive_compaction": true,
|
||||
"preemptive_compaction_threshold": 0.85,
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
@@ -940,8 +956,7 @@ Oh My OpenCode 送你重构工具(重命名、代码操作)。
|
||||
|
||||
| 选项 | 默认值 | 说明 |
|
||||
| --------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `preemptive_compaction` | `false` | 在达到 token 限制之前主动压缩会话。默认在上下文窗口使用率达到 80% 时运行。 |
|
||||
| `preemptive_compaction_threshold` | `0.80` | 触发预先压缩的阈值比例(0.5-0.95)。仅在 `preemptive_compaction` 启用时生效。 |
|
||||
| `preemptive_compaction_threshold` | `0.85` | 触发预防性压缩的阈值比例(0.5-0.95)。`preemptive-compaction` 钩子默认启用;此选项用于自定义阈值。 |
|
||||
| `truncate_all_tool_outputs` | `false` | 截断所有工具输出,而不仅仅是白名单工具(Grep、Glob、LSP、AST-grep)。Tool output truncator 默认启用 - 使用 `disabled_hooks` 禁用。 |
|
||||
| `aggressive_truncation` | `false` | 超出 token 限制时,激进地截断工具输出以适应限制。比默认截断更激进。不够的话会回退到摘要/恢复。 |
|
||||
| `auto_resume` | `false` | 从 thinking block 错误或 thinking disabled violation 成功恢复后,自动恢复会话。提取最后一条用户消息继续执行。 |
|
||||
|
||||
@@ -61,7 +61,10 @@
|
||||
"interactive-bash-session",
|
||||
"empty-message-sanitizer",
|
||||
"thinking-block-validator",
|
||||
"ralph-loop"
|
||||
"ralph-loop",
|
||||
"preemptive-compaction",
|
||||
"compaction-context-injector",
|
||||
"claude-code-hooks"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "2.8.3",
|
||||
"version": "2.9.1",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -103,6 +103,46 @@
|
||||
"created_at": "2025-12-30T12:04:59Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 349
|
||||
},
|
||||
{
|
||||
"name": "junhoyeo",
|
||||
"id": 32605822,
|
||||
"comment_id": 3701585491,
|
||||
"created_at": "2025-12-31T07:00:36Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 375
|
||||
},
|
||||
{
|
||||
"name": "gtg7784",
|
||||
"id": 32065632,
|
||||
"comment_id": 3701688739,
|
||||
"created_at": "2025-12-31T08:05:25Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 377
|
||||
},
|
||||
{
|
||||
"name": "ul8",
|
||||
"id": 589744,
|
||||
"comment_id": 3701705644,
|
||||
"created_at": "2025-12-31T08:16:46Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 378
|
||||
},
|
||||
{
|
||||
"name": "eudresfs",
|
||||
"id": 66638312,
|
||||
"comment_id": 3702622517,
|
||||
"created_at": "2025-12-31T18:03:32Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 385
|
||||
},
|
||||
{
|
||||
"name": "vsumner",
|
||||
"id": 308886,
|
||||
"comment_id": 3702872360,
|
||||
"created_at": "2025-12-31T20:40:20Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 388
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-flash-preview"
|
||||
|
||||
export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "specialist",
|
||||
cost: "CHEAP",
|
||||
promptAlias: "Document Writer",
|
||||
triggers: [
|
||||
{ domain: "Documentation", trigger: "README, API docs, guides" },
|
||||
],
|
||||
}
|
||||
|
||||
export function createDocumentWriterAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
|
||||
const DEFAULT_MODEL = "opencode/grok-code"
|
||||
|
||||
export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "exploration",
|
||||
cost: "FREE",
|
||||
promptAlias: "Explore",
|
||||
keyTrigger: "2+ modules involved → fire `explore` background",
|
||||
triggers: [
|
||||
{ domain: "Explore", trigger: "Find existing codebase structure, patterns and styles" },
|
||||
],
|
||||
useWhen: [
|
||||
"Multiple search angles needed",
|
||||
"Unfamiliar module structure",
|
||||
"Cross-layer pattern discovery",
|
||||
],
|
||||
avoidWhen: [
|
||||
"You know exactly what to search",
|
||||
"Single keyword/pattern suffices",
|
||||
"Known file location",
|
||||
],
|
||||
}
|
||||
|
||||
export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
return {
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-pro-preview"
|
||||
|
||||
export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "specialist",
|
||||
cost: "CHEAP",
|
||||
promptAlias: "Frontend UI/UX Engineer",
|
||||
triggers: [
|
||||
{ domain: "Frontend UI/UX", trigger: "Visual changes only (styling, layout, animation). Pure logic changes in frontend files → handle directly" },
|
||||
],
|
||||
useWhen: [
|
||||
"Visual/UI/UX changes: Color, spacing, layout, typography, animation, responsive breakpoints, hover states, shadows, borders, icons, images",
|
||||
],
|
||||
avoidWhen: [
|
||||
"Pure logic: API calls, data fetching, state management, event handlers (non-visual), type definitions, utility functions, business logic",
|
||||
],
|
||||
}
|
||||
|
||||
export function createFrontendUiUxEngineerAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
|
||||
@@ -19,3 +19,4 @@ export const builtinAgents: Record<string, AgentConfig> = {
|
||||
|
||||
export * from "./types"
|
||||
export { createBuiltinAgents } from "./utils"
|
||||
export type { AvailableAgent } from "./sisyphus-prompt-builder"
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
|
||||
|
||||
export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "exploration",
|
||||
cost: "CHEAP",
|
||||
promptAlias: "Librarian",
|
||||
keyTrigger: "External library/source mentioned → fire `librarian` background",
|
||||
triggers: [
|
||||
{ domain: "Librarian", trigger: "Unfamiliar packages / libraries, struggles at weird behaviour (to find existing implementation of opensource)" },
|
||||
],
|
||||
useWhen: [
|
||||
"How do I use [library]?",
|
||||
"What's the best practice for [framework feature]?",
|
||||
"Why does [external dependency] behave this way?",
|
||||
"Find examples of [library] usage",
|
||||
"Working with unfamiliar npm/pip/cargo packages",
|
||||
],
|
||||
}
|
||||
|
||||
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
return {
|
||||
description:
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-flash"
|
||||
|
||||
export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "utility",
|
||||
cost: "CHEAP",
|
||||
promptAlias: "Multimodal Looker",
|
||||
triggers: [],
|
||||
}
|
||||
|
||||
export function createMultimodalLookerAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
|
||||
@@ -1,8 +1,35 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
|
||||
const DEFAULT_MODEL = "openai/gpt-5.2"
|
||||
|
||||
export const ORACLE_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
cost: "EXPENSIVE",
|
||||
promptAlias: "Oracle",
|
||||
triggers: [
|
||||
{ domain: "Architecture decisions", trigger: "Multi-system tradeoffs, unfamiliar patterns" },
|
||||
{ domain: "Self-review", trigger: "After completing significant implementation" },
|
||||
{ domain: "Hard debugging", trigger: "After 2+ failed fix attempts" },
|
||||
],
|
||||
useWhen: [
|
||||
"Complex architecture design",
|
||||
"After completing significant work",
|
||||
"2+ failed fix attempts",
|
||||
"Unfamiliar code patterns",
|
||||
"Security/performance concerns",
|
||||
"Multi-system tradeoffs",
|
||||
],
|
||||
avoidWhen: [
|
||||
"Simple file operations (use direct tools)",
|
||||
"First attempt at any fix (try yourself first)",
|
||||
"Questions answerable from code you've read",
|
||||
"Trivial decisions (variable names, formatting)",
|
||||
"Things you can infer from existing code patterns",
|
||||
],
|
||||
}
|
||||
|
||||
const ORACLE_SYSTEM_PROMPT = `You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment.
|
||||
|
||||
## Context
|
||||
|
||||
309
src/agents/sisyphus-prompt-builder.ts
Normal file
309
src/agents/sisyphus-prompt-builder.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import type { AgentPromptMetadata, BuiltinAgentName } from "./types"
|
||||
|
||||
export interface AvailableAgent {
|
||||
name: BuiltinAgentName
|
||||
description: string
|
||||
metadata: AgentPromptMetadata
|
||||
}
|
||||
|
||||
export interface AvailableTool {
|
||||
name: string
|
||||
category: "lsp" | "ast" | "search" | "session" | "command" | "other"
|
||||
}
|
||||
|
||||
export interface AvailableSkill {
|
||||
name: string
|
||||
description: string
|
||||
location: "user" | "project" | "plugin"
|
||||
}
|
||||
|
||||
export function categorizeTools(toolNames: string[]): AvailableTool[] {
|
||||
return toolNames.map((name) => {
|
||||
let category: AvailableTool["category"] = "other"
|
||||
if (name.startsWith("lsp_")) {
|
||||
category = "lsp"
|
||||
} else if (name.startsWith("ast_grep")) {
|
||||
category = "ast"
|
||||
} else if (name === "grep" || name === "glob") {
|
||||
category = "search"
|
||||
} else if (name.startsWith("session_")) {
|
||||
category = "session"
|
||||
} else if (name === "slashcommand") {
|
||||
category = "command"
|
||||
}
|
||||
return { name, category }
|
||||
})
|
||||
}
|
||||
|
||||
function formatToolsForPrompt(tools: AvailableTool[]): string {
|
||||
const lspTools = tools.filter((t) => t.category === "lsp")
|
||||
const astTools = tools.filter((t) => t.category === "ast")
|
||||
const searchTools = tools.filter((t) => t.category === "search")
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
if (searchTools.length > 0) {
|
||||
parts.push(...searchTools.map((t) => `\`${t.name}\``))
|
||||
}
|
||||
|
||||
if (lspTools.length > 0) {
|
||||
parts.push("`lsp_*`")
|
||||
}
|
||||
|
||||
if (astTools.length > 0) {
|
||||
parts.push("`ast_grep`")
|
||||
}
|
||||
|
||||
return parts.join(", ")
|
||||
}
|
||||
|
||||
export function buildKeyTriggersSection(agents: AvailableAgent[], skills: AvailableSkill[] = []): string {
|
||||
const keyTriggers = agents
|
||||
.filter((a) => a.metadata.keyTrigger)
|
||||
.map((a) => `- ${a.metadata.keyTrigger}`)
|
||||
|
||||
const skillTriggers = skills
|
||||
.filter((s) => s.description)
|
||||
.map((s) => `- **Skill \`${s.name}\`**: ${extractTriggerFromDescription(s.description)}`)
|
||||
|
||||
const allTriggers = [...keyTriggers, ...skillTriggers]
|
||||
|
||||
if (allTriggers.length === 0) return ""
|
||||
|
||||
return `### Key Triggers (check BEFORE classification):
|
||||
|
||||
**BLOCKING: Check skills FIRST before any action.**
|
||||
If a skill matches, invoke it IMMEDIATELY via \`skill\` tool.
|
||||
|
||||
${allTriggers.join("\n")}
|
||||
- **GitHub mention (@mention in issue/PR)** → This is a WORK REQUEST. Plan full cycle: investigate → implement → create PR
|
||||
- **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.`
|
||||
}
|
||||
|
||||
function extractTriggerFromDescription(description: string): string {
|
||||
const triggerMatch = description.match(/Trigger[s]?[:\s]+([^.]+)/i)
|
||||
if (triggerMatch) return triggerMatch[1].trim()
|
||||
|
||||
const activateMatch = description.match(/Activate when[:\s]+([^.]+)/i)
|
||||
if (activateMatch) return activateMatch[1].trim()
|
||||
|
||||
const useWhenMatch = description.match(/Use (?:this )?when[:\s]+([^.]+)/i)
|
||||
if (useWhenMatch) return useWhenMatch[1].trim()
|
||||
|
||||
return description.split(".")[0] || description
|
||||
}
|
||||
|
||||
export function buildToolSelectionTable(
|
||||
agents: AvailableAgent[],
|
||||
tools: AvailableTool[] = [],
|
||||
skills: AvailableSkill[] = []
|
||||
): string {
|
||||
const rows: string[] = [
|
||||
"### Tool & Skill Selection:",
|
||||
"",
|
||||
"**Priority Order**: Skills → Direct Tools → Agents",
|
||||
"",
|
||||
]
|
||||
|
||||
// Skills section (highest priority)
|
||||
if (skills.length > 0) {
|
||||
rows.push("#### Skills (INVOKE FIRST if matching)")
|
||||
rows.push("")
|
||||
rows.push("| Skill | When to Use |")
|
||||
rows.push("|-------|-------------|")
|
||||
for (const skill of skills) {
|
||||
const shortDesc = extractTriggerFromDescription(skill.description)
|
||||
rows.push(`| \`${skill.name}\` | ${shortDesc} |`)
|
||||
}
|
||||
rows.push("")
|
||||
}
|
||||
|
||||
// Tools and Agents table
|
||||
rows.push("#### Tools & Agents")
|
||||
rows.push("")
|
||||
rows.push("| Resource | Cost | When to Use |")
|
||||
rows.push("|----------|------|-------------|")
|
||||
|
||||
if (tools.length > 0) {
|
||||
const toolsDisplay = formatToolsForPrompt(tools)
|
||||
rows.push(`| ${toolsDisplay} | FREE | Not Complex, Scope Clear, No Implicit Assumptions |`)
|
||||
}
|
||||
|
||||
const costOrder = { FREE: 0, CHEAP: 1, EXPENSIVE: 2 }
|
||||
const sortedAgents = [...agents]
|
||||
.filter((a) => a.metadata.category !== "utility")
|
||||
.sort((a, b) => costOrder[a.metadata.cost] - costOrder[b.metadata.cost])
|
||||
|
||||
for (const agent of sortedAgents) {
|
||||
const shortDesc = agent.description.split(".")[0] || agent.description
|
||||
rows.push(`| \`${agent.name}\` agent | ${agent.metadata.cost} | ${shortDesc} |`)
|
||||
}
|
||||
|
||||
rows.push("")
|
||||
rows.push("**Default flow**: skill (if match) → explore/librarian (background) + tools → oracle (if required)")
|
||||
|
||||
return rows.join("\n")
|
||||
}
|
||||
|
||||
export function buildExploreSection(agents: AvailableAgent[]): string {
|
||||
const exploreAgent = agents.find((a) => a.name === "explore")
|
||||
if (!exploreAgent) return ""
|
||||
|
||||
const useWhen = exploreAgent.metadata.useWhen || []
|
||||
const avoidWhen = exploreAgent.metadata.avoidWhen || []
|
||||
|
||||
return `### Explore Agent = Contextual Grep
|
||||
|
||||
Use it as a **peer tool**, not a fallback. Fire liberally.
|
||||
|
||||
| Use Direct Tools | Use Explore Agent |
|
||||
|------------------|-------------------|
|
||||
${avoidWhen.map((w) => `| ${w} | |`).join("\n")}
|
||||
${useWhen.map((w) => `| | ${w} |`).join("\n")}`
|
||||
}
|
||||
|
||||
export function buildLibrarianSection(agents: AvailableAgent[]): string {
|
||||
const librarianAgent = agents.find((a) => a.name === "librarian")
|
||||
if (!librarianAgent) return ""
|
||||
|
||||
const useWhen = librarianAgent.metadata.useWhen || []
|
||||
|
||||
return `### Librarian Agent = Reference Grep
|
||||
|
||||
Search **external references** (docs, OSS, web). Fire proactively when unfamiliar libraries are involved.
|
||||
|
||||
| Contextual Grep (Internal) | Reference Grep (External) |
|
||||
|----------------------------|---------------------------|
|
||||
| Search OUR codebase | Search EXTERNAL resources |
|
||||
| Find patterns in THIS repo | Find examples in OTHER repos |
|
||||
| How does our code work? | How does this library work? |
|
||||
| Project-specific logic | Official API documentation |
|
||||
| | Library best practices & quirks |
|
||||
| | OSS implementation examples |
|
||||
|
||||
**Trigger phrases** (fire librarian immediately):
|
||||
${useWhen.map((w) => `- "${w}"`).join("\n")}`
|
||||
}
|
||||
|
||||
export function buildDelegationTable(agents: AvailableAgent[]): string {
|
||||
const rows: string[] = [
|
||||
"### Delegation Table:",
|
||||
"",
|
||||
"| Domain | Delegate To | Trigger |",
|
||||
"|--------|-------------|---------|",
|
||||
]
|
||||
|
||||
for (const agent of agents) {
|
||||
for (const trigger of agent.metadata.triggers) {
|
||||
rows.push(`| ${trigger.domain} | \`${agent.name}\` | ${trigger.trigger} |`)
|
||||
}
|
||||
}
|
||||
|
||||
return rows.join("\n")
|
||||
}
|
||||
|
||||
export function buildFrontendSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
if (!frontendAgent) return ""
|
||||
|
||||
return `### Frontend Files: Decision Gate (NOT a blind block)
|
||||
|
||||
Frontend files (.tsx, .jsx, .vue, .svelte, .css, etc.) require **classification before action**.
|
||||
|
||||
#### Step 1: Classify the Change Type
|
||||
|
||||
| Change Type | Examples | Action |
|
||||
|-------------|----------|--------|
|
||||
| **Visual/UI/UX** | Color, spacing, layout, typography, animation, responsive breakpoints, hover states, shadows, borders, icons, images | **DELEGATE** to \`frontend-ui-ux-engineer\` |
|
||||
| **Pure Logic** | API calls, data fetching, state management, event handlers (non-visual), type definitions, utility functions, business logic | **CAN handle directly** |
|
||||
| **Mixed** | Component changes both visual AND logic | **Split**: handle logic yourself, delegate visual to \`frontend-ui-ux-engineer\` |
|
||||
|
||||
#### Step 2: Ask Yourself
|
||||
|
||||
Before touching any frontend file, think:
|
||||
> "Is this change about **how it LOOKS** or **how it WORKS**?"
|
||||
|
||||
- **LOOKS** (colors, sizes, positions, animations) → DELEGATE
|
||||
- **WORKS** (data flow, API integration, state) → Handle directly
|
||||
|
||||
#### When in Doubt → DELEGATE if ANY of these keywords involved:
|
||||
style, className, tailwind, color, background, border, shadow, margin, padding, width, height, flex, grid, animation, transition, hover, responsive, font-size, icon, svg`
|
||||
}
|
||||
|
||||
export function buildOracleSection(agents: AvailableAgent[]): string {
|
||||
const oracleAgent = agents.find((a) => a.name === "oracle")
|
||||
if (!oracleAgent) return ""
|
||||
|
||||
const useWhen = oracleAgent.metadata.useWhen || []
|
||||
const avoidWhen = oracleAgent.metadata.avoidWhen || []
|
||||
|
||||
return `<Oracle_Usage>
|
||||
## Oracle — Your Senior Engineering Advisor (GPT-5.2)
|
||||
|
||||
Oracle is an expensive, high-quality reasoning model. Use it wisely.
|
||||
|
||||
### WHEN to Consult:
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
${useWhen.map((w) => `| ${w} | Oracle FIRST, then implement |`).join("\n")}
|
||||
|
||||
### WHEN NOT to Consult:
|
||||
|
||||
${avoidWhen.map((w) => `- ${w}`).join("\n")}
|
||||
|
||||
### Usage Pattern:
|
||||
Briefly announce "Consulting Oracle for [reason]" before invocation.
|
||||
|
||||
**Exception**: This is the ONLY case where you announce before acting. For all other work, start immediately without status updates.
|
||||
</Oracle_Usage>`
|
||||
}
|
||||
|
||||
export function buildHardBlocksSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
|
||||
const blocks = [
|
||||
"| Type error suppression (`as any`, `@ts-ignore`) | Never |",
|
||||
"| Commit without explicit request | Never |",
|
||||
"| Speculate about unread code | Never |",
|
||||
"| Leave code in broken state after failures | Never |",
|
||||
]
|
||||
|
||||
if (frontendAgent) {
|
||||
blocks.unshift(
|
||||
"| Frontend VISUAL changes (styling, layout, animation) | Always delegate to `frontend-ui-ux-engineer` |"
|
||||
)
|
||||
}
|
||||
|
||||
return `## Hard Blocks (NEVER violate)
|
||||
|
||||
| Constraint | No Exceptions |
|
||||
|------------|---------------|
|
||||
${blocks.join("\n")}`
|
||||
}
|
||||
|
||||
export function buildAntiPatternsSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
|
||||
const patterns = [
|
||||
"| **Type Safety** | `as any`, `@ts-ignore`, `@ts-expect-error` |",
|
||||
"| **Error Handling** | Empty catch blocks `catch(e) {}` |",
|
||||
"| **Testing** | Deleting failing tests to \"pass\" |",
|
||||
"| **Search** | Firing agents for single-line typos or obvious syntax errors |",
|
||||
"| **Debugging** | Shotgun debugging, random changes |",
|
||||
]
|
||||
|
||||
if (frontendAgent) {
|
||||
patterns.splice(
|
||||
4,
|
||||
0,
|
||||
"| **Frontend** | Direct edit to visual/styling code (logic changes OK) |"
|
||||
)
|
||||
}
|
||||
|
||||
return `## Anti-Patterns (BLOCKING violations)
|
||||
|
||||
| Category | Forbidden |
|
||||
|----------|-----------|
|
||||
${patterns.join("\n")}`
|
||||
}
|
||||
@@ -1,9 +1,22 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { isGptModel } from "./types"
|
||||
import type { AvailableAgent, AvailableTool, AvailableSkill } from "./sisyphus-prompt-builder"
|
||||
import {
|
||||
buildKeyTriggersSection,
|
||||
buildToolSelectionTable,
|
||||
buildExploreSection,
|
||||
buildLibrarianSection,
|
||||
buildDelegationTable,
|
||||
buildFrontendSection,
|
||||
buildOracleSection,
|
||||
buildHardBlocksSection,
|
||||
buildAntiPatternsSection,
|
||||
categorizeTools,
|
||||
} from "./sisyphus-prompt-builder"
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
const SISYPHUS_SYSTEM_PROMPT = `<Role>
|
||||
const SISYPHUS_ROLE_SECTION = `<Role>
|
||||
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
|
||||
Named by [YeonGyu Kim](https://github.com/code-yeongyu).
|
||||
|
||||
@@ -21,22 +34,27 @@ Named by [YeonGyu Kim](https://github.com/code-yeongyu).
|
||||
|
||||
**Operating Mode**: You NEVER work alone when specialists are available. Frontend work → delegate. Deep research → parallel background agents (async subagents). Complex architecture → consult Oracle.
|
||||
|
||||
</Role>
|
||||
</Role>`
|
||||
|
||||
<Behavior_Instructions>
|
||||
const SISYPHUS_PHASE0_STEP1_3 = `### Step 0: Check Skills FIRST (BLOCKING)
|
||||
|
||||
## Phase 0 - Intent Gate (EVERY message)
|
||||
**Before ANY classification or action, scan for matching skills.**
|
||||
|
||||
### Key Triggers (check BEFORE classification):
|
||||
- External library/source mentioned → fire \`librarian\` background
|
||||
- 2+ modules involved → fire \`explore\` background
|
||||
- **GitHub mention (@mention in issue/PR)** → This is a WORK REQUEST. Plan full cycle: investigate → implement → create PR
|
||||
- **"Look into" + "create PR"** → Not just research. Full implementation cycle expected.
|
||||
\`\`\`
|
||||
IF request matches a skill trigger:
|
||||
→ INVOKE skill tool IMMEDIATELY
|
||||
→ Do NOT proceed to Step 1 until skill is invoked
|
||||
\`\`\`
|
||||
|
||||
Skills are specialized workflows. When relevant, they handle the task better than manual orchestration.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Classify Request Type
|
||||
|
||||
| Type | Signal | Action |
|
||||
|------|--------|--------|
|
||||
| **Skill Match** | Matches skill trigger phrase | **INVOKE skill FIRST** via \`skill\` tool |
|
||||
| **Trivial** | Single file, known location, direct answer | Direct tools only (UNLESS Key Trigger applies) |
|
||||
| **Explicit** | Specific file/line, clear command | Execute directly |
|
||||
| **Exploratory** | "How does X work?", "Find Y" | Fire explore (1-3) + tools in parallel |
|
||||
@@ -78,11 +96,9 @@ Then: Raise your concern concisely. Propose an alternative. Ask if they want to
|
||||
I notice [observation]. This might cause [problem] because [reason].
|
||||
Alternative: [your suggestion].
|
||||
Should I proceed with your original request, or try the alternative?
|
||||
\`\`\`
|
||||
\`\`\``
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Codebase Assessment (for Open-ended tasks)
|
||||
const SISYPHUS_PHASE1 = `## Phase 1 - Codebase Assessment (for Open-ended tasks)
|
||||
|
||||
Before following existing patterns, assess whether they're worth following.
|
||||
|
||||
@@ -103,54 +119,9 @@ Before following existing patterns, assess whether they're worth following.
|
||||
IMPORTANT: If codebase appears undisciplined, verify before assuming:
|
||||
- Different patterns may serve different purposes (intentional)
|
||||
- Migration might be in progress
|
||||
- You might be looking at the wrong reference files
|
||||
- You might be looking at the wrong reference files`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2A - Exploration & Research
|
||||
|
||||
### Tool Selection:
|
||||
|
||||
| Tool | Cost | When to Use |
|
||||
|------|------|-------------|
|
||||
| \`grep\`, \`glob\`, \`lsp_*\`, \`ast_grep\` | FREE | Not Complex, Scope Clear, No Implicit Assumptions |
|
||||
| \`explore\` agent | FREE | Multiple search angles, unfamiliar modules, cross-layer patterns |
|
||||
| \`librarian\` agent | CHEAP | External docs, GitHub examples, OpenSource Implementations, OSS reference |
|
||||
| \`oracle\` agent | EXPENSIVE | Architecture, review, debugging after 2+ failures |
|
||||
|
||||
**Default flow**: explore/librarian (background) + tools → oracle (if required)
|
||||
|
||||
### Explore Agent = Contextual Grep
|
||||
|
||||
Use it as a **peer tool**, not a fallback. Fire liberally.
|
||||
|
||||
| Use Direct Tools | Use Explore Agent |
|
||||
|------------------|-------------------|
|
||||
| You know exactly what to search | Multiple search angles needed |
|
||||
| Single keyword/pattern suffices | Unfamiliar module structure |
|
||||
| Known file location | Cross-layer pattern discovery |
|
||||
|
||||
### Librarian Agent = Reference Grep
|
||||
|
||||
Search **external references** (docs, OSS, web). Fire proactively when unfamiliar libraries are involved.
|
||||
|
||||
| Contextual Grep (Internal) | Reference Grep (External) |
|
||||
|----------------------------|---------------------------|
|
||||
| Search OUR codebase | Search EXTERNAL resources |
|
||||
| Find patterns in THIS repo | Find examples in OTHER repos |
|
||||
| How does our code work? | How does this library work? |
|
||||
| Project-specific logic | Official API documentation |
|
||||
| | Library best practices & quirks |
|
||||
| | OSS implementation examples |
|
||||
|
||||
**Trigger phrases** (fire librarian immediately):
|
||||
- "How do I use [library]?"
|
||||
- "What's the best practice for [framework feature]?"
|
||||
- "Why does [external dependency] behave this way?"
|
||||
- "Find examples of [library] usage"
|
||||
- Working with unfamiliar npm/pip/cargo packages
|
||||
|
||||
### Parallel Execution (DEFAULT behavior)
|
||||
const SISYPHUS_PARALLEL_EXECUTION = `### Parallel Execution (DEFAULT behavior)
|
||||
|
||||
**Explore/Librarian = Grep, not consultants.
|
||||
|
||||
@@ -182,64 +153,16 @@ STOP searching when:
|
||||
- 2 search iterations yielded no new useful data
|
||||
- Direct answer found
|
||||
|
||||
**DO NOT over-explore. Time is precious.**
|
||||
**DO NOT over-explore. Time is precious.**`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2B - Implementation
|
||||
const SISYPHUS_PHASE2B_PRE_IMPLEMENTATION = `## Phase 2B - Implementation
|
||||
|
||||
### Pre-Implementation:
|
||||
1. If task has 2+ steps → Create todo list IMMEDIATELY, IN SUPER DETAIL. No announcements—just create it.
|
||||
2. Mark current task \`in_progress\` before starting
|
||||
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS
|
||||
3. Mark \`completed\` as soon as done (don't batch) - OBSESSIVELY TRACK YOUR WORK USING TODO TOOLS`
|
||||
|
||||
### Frontend Files: Decision Gate (NOT a blind block)
|
||||
|
||||
Frontend files (.tsx, .jsx, .vue, .svelte, .css, etc.) require **classification before action**.
|
||||
|
||||
#### Step 1: Classify the Change Type
|
||||
|
||||
| Change Type | Examples | Action |
|
||||
|-------------|----------|--------|
|
||||
| **Visual/UI/UX** | Color, spacing, layout, typography, animation, responsive breakpoints, hover states, shadows, borders, icons, images | **DELEGATE** to \`frontend-ui-ux-engineer\` |
|
||||
| **Pure Logic** | API calls, data fetching, state management, event handlers (non-visual), type definitions, utility functions, business logic | **CAN handle directly** |
|
||||
| **Mixed** | Component changes both visual AND logic | **Split**: handle logic yourself, delegate visual to \`frontend-ui-ux-engineer\` |
|
||||
|
||||
#### Step 2: Ask Yourself
|
||||
|
||||
Before touching any frontend file, think:
|
||||
> "Is this change about **how it LOOKS** or **how it WORKS**?"
|
||||
|
||||
- **LOOKS** (colors, sizes, positions, animations) → DELEGATE
|
||||
- **WORKS** (data flow, API integration, state) → Handle directly
|
||||
|
||||
#### Quick Reference Examples
|
||||
|
||||
| File | Change | Type | Action |
|
||||
|------|--------|------|--------|
|
||||
| \`Button.tsx\` | Change color blue→green | Visual | DELEGATE |
|
||||
| \`Button.tsx\` | Add onClick API call | Logic | Direct |
|
||||
| \`UserList.tsx\` | Add loading spinner animation | Visual | DELEGATE |
|
||||
| \`UserList.tsx\` | Fix pagination logic bug | Logic | Direct |
|
||||
| \`Modal.tsx\` | Make responsive for mobile | Visual | DELEGATE |
|
||||
| \`Modal.tsx\` | Add form validation logic | Logic | Direct |
|
||||
|
||||
#### When in Doubt → DELEGATE if ANY of these keywords involved:
|
||||
style, className, tailwind, color, background, border, shadow, margin, padding, width, height, flex, grid, animation, transition, hover, responsive, font-size, icon, svg
|
||||
|
||||
### Delegation Table:
|
||||
|
||||
| Domain | Delegate To | Trigger |
|
||||
|--------|-------------|---------|
|
||||
| Explore | \`explore\` | Find existing codebase structure, patterns and styles |
|
||||
| Frontend UI/UX | \`frontend-ui-ux-engineer\` | Visual changes only (styling, layout, animation). Pure logic changes in frontend files → handle directly |
|
||||
| Librarian | \`librarian\` | Unfamiliar packages / libraries, struggles at weird behaviour (to find existing implementation of opensource) |
|
||||
| Documentation | \`document-writer\` | README, API docs, guides |
|
||||
| Architecture decisions | \`oracle\` | Multi-system tradeoffs, unfamiliar patterns |
|
||||
| Self-review | \`oracle\` | After completing significant implementation |
|
||||
| Hard debugging | \`oracle\` | After 2+ failed fix attempts |
|
||||
|
||||
### Delegation Prompt Structure (MANDATORY - ALL 7 sections):
|
||||
const SISYPHUS_DELEGATION_PROMPT_STRUCTURE = `### Delegation Prompt Structure (MANDATORY - ALL 7 sections):
|
||||
|
||||
When delegating, your prompt MUST include:
|
||||
|
||||
@@ -259,9 +182,9 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
- EXPECTED RESULT CAME OUT?
|
||||
- DID THE AGENT FOLLOWED "MUST DO" AND "MUST NOT DO" REQUIREMENTS?
|
||||
|
||||
**Vague prompts = rejected. Be exhaustive.**
|
||||
**Vague prompts = rejected. Be exhaustive.**`
|
||||
|
||||
### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
|
||||
const SISYPHUS_GITHUB_WORKFLOW = `### GitHub Workflow (CRITICAL - When mentioned in issues/PRs):
|
||||
|
||||
When you're mentioned in GitHub issues or asked to "look into" something and "create PR":
|
||||
|
||||
@@ -294,9 +217,9 @@ When you're mentioned in GitHub issues or asked to "look into" something and "cr
|
||||
**EMPHASIS**: "Look into" does NOT mean "just investigate and report back."
|
||||
It means "investigate, understand, implement a solution, and create a PR."
|
||||
|
||||
**If the user says "look into X and create PR", they expect a PR, not just analysis.**
|
||||
**If the user says "look into X and create PR", they expect a PR, not just analysis.**`
|
||||
|
||||
### Code Changes:
|
||||
const SISYPHUS_CODE_CHANGES = `### Code Changes:
|
||||
- Match existing patterns (if codebase is disciplined)
|
||||
- Propose approach first (if codebase is chaotic)
|
||||
- Never suppress type errors with \`as any\`, \`@ts-ignore\`, \`@ts-expect-error\`
|
||||
@@ -322,11 +245,9 @@ If project has build/test commands, run them at task completion.
|
||||
| Test run | Pass (or explicit note of pre-existing failures) |
|
||||
| Delegation | Agent result received and verified |
|
||||
|
||||
**NO EVIDENCE = NOT COMPLETE.**
|
||||
**NO EVIDENCE = NOT COMPLETE.**`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2C - Failure Recovery
|
||||
const SISYPHUS_PHASE2C = `## Phase 2C - Failure Recovery
|
||||
|
||||
### When Fixes Fail:
|
||||
|
||||
@@ -342,11 +263,9 @@ If project has build/test commands, run them at task completion.
|
||||
4. **CONSULT** Oracle with full failure context
|
||||
5. If Oracle cannot resolve → **ASK USER** before proceeding
|
||||
|
||||
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"
|
||||
**Never**: Leave code in broken state, continue hoping it'll work, delete failing tests to "pass"`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 - Completion
|
||||
const SISYPHUS_PHASE3 = `## Phase 3 - Completion
|
||||
|
||||
A task is complete when:
|
||||
- [ ] All planned todo items marked done
|
||||
@@ -361,41 +280,9 @@ If verification fails:
|
||||
|
||||
### Before Delivering Final Answer:
|
||||
- Cancel ALL running background tasks: \`background_cancel(all=true)\`
|
||||
- This conserves resources and ensures clean workflow completion
|
||||
- This conserves resources and ensures clean workflow completion`
|
||||
|
||||
</Behavior_Instructions>
|
||||
|
||||
<Oracle_Usage>
|
||||
## Oracle — Your Senior Engineering Advisor (GPT-5.2)
|
||||
|
||||
Oracle is an expensive, high-quality reasoning model. Use it wisely.
|
||||
|
||||
### WHEN to Consult:
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Complex architecture design | Oracle FIRST, then implement |
|
||||
| After completing significant work | Oracle review before marking complete |
|
||||
| 2+ failed fix attempts | Oracle for debugging guidance |
|
||||
| Unfamiliar code patterns | Oracle to explain behavior |
|
||||
| Security/performance concerns | Oracle for analysis |
|
||||
| Multi-system tradeoffs | Oracle for architectural decision |
|
||||
|
||||
### WHEN NOT to Consult:
|
||||
|
||||
- Simple file operations (use direct tools)
|
||||
- First attempt at any fix (try yourself first)
|
||||
- Questions answerable from code you've read
|
||||
- Trivial decisions (variable names, formatting)
|
||||
- Things you can infer from existing code patterns
|
||||
|
||||
### Usage Pattern:
|
||||
Briefly announce "Consulting Oracle for [reason]" before invocation.
|
||||
|
||||
**Exception**: This is the ONLY case where you announce before acting. For all other work, start immediately without status updates.
|
||||
</Oracle_Usage>
|
||||
|
||||
<Task_Management>
|
||||
const SISYPHUS_TASK_MANAGEMENT = `<Task_Management>
|
||||
## Todo Management (CRITICAL)
|
||||
|
||||
**DEFAULT BEHAVIOR**: Create todos BEFORE starting any non-trivial task. This is your PRIMARY coordination mechanism.
|
||||
@@ -450,9 +337,9 @@ I want to make sure I understand correctly.
|
||||
|
||||
Should I proceed with [recommendation], or would you prefer differently?
|
||||
\`\`\`
|
||||
</Task_Management>
|
||||
</Task_Management>`
|
||||
|
||||
<Tone_and_Style>
|
||||
const SISYPHUS_TONE_AND_STYLE = `<Tone_and_Style>
|
||||
## Communication Style
|
||||
|
||||
### Be Concise
|
||||
@@ -492,31 +379,9 @@ If the user's approach seems problematic:
|
||||
- If user is terse, be terse
|
||||
- If user wants detail, provide detail
|
||||
- Adapt to their communication preference
|
||||
</Tone_and_Style>
|
||||
</Tone_and_Style>`
|
||||
|
||||
<Constraints>
|
||||
## Hard Blocks (NEVER violate)
|
||||
|
||||
| Constraint | No Exceptions |
|
||||
|------------|---------------|
|
||||
| Frontend VISUAL changes (styling, layout, animation) | Always delegate to \`frontend-ui-ux-engineer\` |
|
||||
| Type error suppression (\`as any\`, \`@ts-ignore\`) | Never |
|
||||
| Commit without explicit request | Never |
|
||||
| Speculate about unread code | Never |
|
||||
| Leave code in broken state after failures | Never |
|
||||
|
||||
## Anti-Patterns (BLOCKING violations)
|
||||
|
||||
| Category | Forbidden |
|
||||
|----------|-----------|
|
||||
| **Type Safety** | \`as any\`, \`@ts-ignore\`, \`@ts-expect-error\` |
|
||||
| **Error Handling** | Empty catch blocks \`catch(e) {}\` |
|
||||
| **Testing** | Deleting failing tests to "pass" |
|
||||
| **Search** | Firing agents for single-line typos or obvious syntax errors |
|
||||
| **Frontend** | Direct edit to visual/styling code (logic changes OK) |
|
||||
| **Debugging** | Shotgun debugging, random changes |
|
||||
|
||||
## Soft Guidelines
|
||||
const SISYPHUS_SOFT_GUIDELINES = `## Soft Guidelines
|
||||
|
||||
- Prefer existing libraries over new dependencies
|
||||
- Prefer small, focused changes over large refactors
|
||||
@@ -525,14 +390,107 @@ If the user's approach seems problematic:
|
||||
|
||||
`
|
||||
|
||||
export function createSisyphusAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
function buildDynamicSisyphusPrompt(
|
||||
availableAgents: AvailableAgent[],
|
||||
availableTools: AvailableTool[] = [],
|
||||
availableSkills: AvailableSkill[] = []
|
||||
): string {
|
||||
const keyTriggers = buildKeyTriggersSection(availableAgents, availableSkills)
|
||||
const toolSelection = buildToolSelectionTable(availableAgents, availableTools, availableSkills)
|
||||
const exploreSection = buildExploreSection(availableAgents)
|
||||
const librarianSection = buildLibrarianSection(availableAgents)
|
||||
const frontendSection = buildFrontendSection(availableAgents)
|
||||
const delegationTable = buildDelegationTable(availableAgents)
|
||||
const oracleSection = buildOracleSection(availableAgents)
|
||||
const hardBlocks = buildHardBlocksSection(availableAgents)
|
||||
const antiPatterns = buildAntiPatternsSection(availableAgents)
|
||||
|
||||
const sections = [
|
||||
SISYPHUS_ROLE_SECTION,
|
||||
"<Behavior_Instructions>",
|
||||
"",
|
||||
"## Phase 0 - Intent Gate (EVERY message)",
|
||||
"",
|
||||
keyTriggers,
|
||||
"",
|
||||
SISYPHUS_PHASE0_STEP1_3,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE1,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## Phase 2A - Exploration & Research",
|
||||
"",
|
||||
toolSelection,
|
||||
"",
|
||||
exploreSection,
|
||||
"",
|
||||
librarianSection,
|
||||
"",
|
||||
SISYPHUS_PARALLEL_EXECUTION,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE2B_PRE_IMPLEMENTATION,
|
||||
"",
|
||||
frontendSection,
|
||||
"",
|
||||
delegationTable,
|
||||
"",
|
||||
SISYPHUS_DELEGATION_PROMPT_STRUCTURE,
|
||||
"",
|
||||
SISYPHUS_GITHUB_WORKFLOW,
|
||||
"",
|
||||
SISYPHUS_CODE_CHANGES,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE2C,
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
SISYPHUS_PHASE3,
|
||||
"",
|
||||
"</Behavior_Instructions>",
|
||||
"",
|
||||
oracleSection,
|
||||
"",
|
||||
SISYPHUS_TASK_MANAGEMENT,
|
||||
"",
|
||||
SISYPHUS_TONE_AND_STYLE,
|
||||
"",
|
||||
"<Constraints>",
|
||||
hardBlocks,
|
||||
"",
|
||||
antiPatterns,
|
||||
"",
|
||||
SISYPHUS_SOFT_GUIDELINES,
|
||||
]
|
||||
|
||||
return sections.filter((s) => s !== "").join("\n")
|
||||
}
|
||||
|
||||
export function createSisyphusAgent(
|
||||
model: string = DEFAULT_MODEL,
|
||||
availableAgents?: AvailableAgent[],
|
||||
availableToolNames?: string[],
|
||||
availableSkills?: AvailableSkill[]
|
||||
): AgentConfig {
|
||||
const tools = availableToolNames ? categorizeTools(availableToolNames) : []
|
||||
const skills = availableSkills ?? []
|
||||
const prompt = availableAgents
|
||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills)
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.",
|
||||
mode: "primary" as const,
|
||||
model,
|
||||
maxTokens: 64000,
|
||||
prompt: SISYPHUS_SYSTEM_PROMPT,
|
||||
prompt,
|
||||
color: "#00CED1",
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,56 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export type AgentFactory = (model?: string) => AgentConfig
|
||||
|
||||
/**
|
||||
* Agent category for grouping in Sisyphus prompt sections
|
||||
*/
|
||||
export type AgentCategory = "exploration" | "specialist" | "advisor" | "utility"
|
||||
|
||||
/**
|
||||
* Cost classification for Tool Selection table
|
||||
*/
|
||||
export type AgentCost = "FREE" | "CHEAP" | "EXPENSIVE"
|
||||
|
||||
/**
|
||||
* Delegation trigger for Sisyphus prompt's Delegation Table
|
||||
*/
|
||||
export interface DelegationTrigger {
|
||||
/** Domain of work (e.g., "Frontend UI/UX") */
|
||||
domain: string
|
||||
/** When to delegate (e.g., "Visual changes only...") */
|
||||
trigger: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for generating Sisyphus prompt sections dynamically
|
||||
* This allows adding/removing agents without manually updating the Sisyphus prompt
|
||||
*/
|
||||
export interface AgentPromptMetadata {
|
||||
/** Category for grouping in prompt sections */
|
||||
category: AgentCategory
|
||||
|
||||
/** Cost classification for Tool Selection table */
|
||||
cost: AgentCost
|
||||
|
||||
/** Domain triggers for Delegation Table */
|
||||
triggers: DelegationTrigger[]
|
||||
|
||||
/** When to use this agent (for detailed sections) */
|
||||
useWhen?: string[]
|
||||
|
||||
/** When NOT to use this agent */
|
||||
avoidWhen?: string[]
|
||||
|
||||
/** Optional dedicated prompt section (markdown) - for agents like Oracle that have special sections */
|
||||
dedicatedSection?: string
|
||||
|
||||
/** Nickname/alias used in prompt (e.g., "Oracle" instead of "oracle") */
|
||||
promptAlias?: string
|
||||
|
||||
/** Key triggers that should appear in Phase 0 (e.g., "External library mentioned → fire librarian") */
|
||||
keyTrigger?: string
|
||||
}
|
||||
|
||||
export function isGptModel(model: string): boolean {
|
||||
return model.startsWith("openai/") || model.startsWith("github-copilot/gpt-")
|
||||
}
|
||||
|
||||
93
src/cli/AGENTS.md
Normal file
93
src/cli/AGENTS.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# CLI KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Command-line interface for oh-my-opencode. Interactive installer, health diagnostics (doctor), and runtime commands. Entry point: `bunx oh-my-opencode`.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry point, subcommand routing
|
||||
├── install.ts # Interactive TUI installer
|
||||
├── config-manager.ts # Config detection, parsing, merging (669 lines)
|
||||
├── types.ts # CLI-specific types
|
||||
├── doctor/ # Health check system
|
||||
│ ├── index.ts # Doctor command entry
|
||||
│ ├── constants.ts # Check categories, descriptions
|
||||
│ ├── types.ts # Check result interfaces
|
||||
│ └── checks/ # 17 individual health checks
|
||||
├── get-local-version/ # Version detection utility
|
||||
│ ├── index.ts
|
||||
│ └── formatter.ts
|
||||
└── run/ # OpenCode session launcher
|
||||
├── index.ts
|
||||
└── completion.test.ts
|
||||
```
|
||||
|
||||
## CLI COMMANDS
|
||||
|
||||
| Command | Purpose | Key File |
|
||||
|---------|---------|----------|
|
||||
| `install` | Interactive setup wizard | `install.ts` |
|
||||
| `doctor` | Environment health checks | `doctor/index.ts` |
|
||||
| `run` | Launch OpenCode session | `run/index.ts` |
|
||||
|
||||
## DOCTOR CHECKS
|
||||
|
||||
17 checks in `doctor/checks/`:
|
||||
|
||||
| Check | Validates |
|
||||
|-------|-----------|
|
||||
| `version.ts` | OpenCode version >= 1.0.150 |
|
||||
| `config.ts` | Plugin registered in opencode.json |
|
||||
| `bun.ts` | Bun runtime available |
|
||||
| `node.ts` | Node.js version compatibility |
|
||||
| `git.ts` | Git installed |
|
||||
| `anthropic-auth.ts` | Claude authentication |
|
||||
| `openai-auth.ts` | OpenAI authentication |
|
||||
| `google-auth.ts` | Google/Gemini authentication |
|
||||
| `lsp-*.ts` | Language server availability |
|
||||
| `mcp-*.ts` | MCP server connectivity |
|
||||
|
||||
## INSTALLATION FLOW
|
||||
|
||||
1. **Detection**: Find existing `opencode.json` / `opencode.jsonc`
|
||||
2. **TUI Prompts**: Claude subscription? ChatGPT? Gemini?
|
||||
3. **Config Generation**: Build `oh-my-opencode.json` based on answers
|
||||
4. **Plugin Registration**: Add to `plugin` array in opencode.json
|
||||
5. **Auth Guidance**: Instructions for `opencode auth login`
|
||||
|
||||
## CONFIG-MANAGER
|
||||
|
||||
The largest file (669 lines) handles:
|
||||
|
||||
- **JSONC support**: Parses comments and trailing commas
|
||||
- **Multi-source detection**: User (~/.config/opencode/) + Project (.opencode/)
|
||||
- **Schema validation**: Zod-based config validation
|
||||
- **Migration**: Handles legacy config formats
|
||||
- **Error collection**: Aggregates parsing errors for doctor
|
||||
|
||||
## HOW TO ADD A DOCTOR CHECK
|
||||
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`:
|
||||
```typescript
|
||||
import type { DoctorCheck } from "../types"
|
||||
|
||||
export const myCheck: DoctorCheck = {
|
||||
name: "my-check",
|
||||
category: "environment",
|
||||
check: async () => {
|
||||
// Return { status: "pass" | "warn" | "fail", message: string }
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Add to `src/cli/doctor/checks/index.ts`
|
||||
3. Update `constants.ts` if new category
|
||||
|
||||
## ANTI-PATTERNS (CLI)
|
||||
|
||||
- **Blocking prompts in non-TTY**: Check `process.stdout.isTTY` before TUI
|
||||
- **Hardcoded paths**: Use shared utilities for config paths
|
||||
- **Ignoring JSONC**: User configs may have comments
|
||||
- **Silent failures**: Doctor checks must return clear status/message
|
||||
@@ -1,76 +1,11 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { CheckResult, CheckDefinition, VersionCheckInfo } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
|
||||
import { parseJsonc } from "../../../shared"
|
||||
|
||||
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const OPENCODE_PACKAGE_JSON = join(OPENCODE_CONFIG_DIR, "package.json")
|
||||
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
|
||||
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
|
||||
|
||||
async function fetchLatestVersion(): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (!res.ok) return null
|
||||
const data = (await res.json()) as { version: string }
|
||||
return data.version
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentVersion(): {
|
||||
version: string | null
|
||||
isLocalDev: boolean
|
||||
isPinned: boolean
|
||||
pinnedVersion: string | null
|
||||
} {
|
||||
const configPath = existsSync(OPENCODE_JSONC) ? OPENCODE_JSONC : OPENCODE_JSON
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return { version: null, isLocalDev: false, isPinned: false, pinnedVersion: null }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(configPath, "utf-8")
|
||||
const config = parseJsonc<{ plugin?: string[] }>(content)
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.startsWith("file:") && plugin.includes(PACKAGE_NAME)) {
|
||||
return { version: "local-dev", isLocalDev: true, isPinned: false, pinnedVersion: null }
|
||||
}
|
||||
if (plugin.startsWith(`${PACKAGE_NAME}@`)) {
|
||||
const pinnedVersion = plugin.split("@")[1]
|
||||
return { version: pinnedVersion, isLocalDev: false, isPinned: true, pinnedVersion }
|
||||
}
|
||||
if (plugin === PACKAGE_NAME) {
|
||||
if (existsSync(OPENCODE_PACKAGE_JSON)) {
|
||||
try {
|
||||
const pkgContent = readFileSync(OPENCODE_PACKAGE_JSON, "utf-8")
|
||||
const pkg = JSON.parse(pkgContent) as { dependencies?: Record<string, string> }
|
||||
const depVersion = pkg.dependencies?.[PACKAGE_NAME]
|
||||
if (depVersion) {
|
||||
const cleanVersion = depVersion.replace(/^[\^~]/, "")
|
||||
return { version: cleanVersion, isLocalDev: false, isPinned: false, pinnedVersion: null }
|
||||
}
|
||||
} catch {
|
||||
// intentionally empty - parse errors ignored
|
||||
}
|
||||
}
|
||||
return { version: null, isLocalDev: false, isPinned: false, pinnedVersion: null }
|
||||
}
|
||||
}
|
||||
|
||||
return { version: null, isLocalDev: false, isPinned: false, pinnedVersion: null }
|
||||
} catch {
|
||||
return { version: null, isLocalDev: false, isPinned: false, pinnedVersion: null }
|
||||
}
|
||||
}
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import {
|
||||
getCachedVersion,
|
||||
getLatestVersion,
|
||||
isLocalDevMode,
|
||||
findPluginEntry,
|
||||
} from "../../../hooks/auto-update-checker/checker"
|
||||
|
||||
function compareVersions(current: string, latest: string): boolean {
|
||||
const parseVersion = (v: string): number[] => {
|
||||
@@ -91,22 +26,43 @@ function compareVersions(current: string, latest: string): boolean {
|
||||
}
|
||||
|
||||
export async function getVersionInfo(): Promise<VersionCheckInfo> {
|
||||
const current = getCurrentVersion()
|
||||
const latestVersion = await fetchLatestVersion()
|
||||
const cwd = process.cwd()
|
||||
|
||||
if (isLocalDevMode(cwd)) {
|
||||
return {
|
||||
currentVersion: "local-dev",
|
||||
latestVersion: null,
|
||||
isUpToDate: true,
|
||||
isLocalDev: true,
|
||||
isPinned: false,
|
||||
}
|
||||
}
|
||||
|
||||
const pluginInfo = findPluginEntry(cwd)
|
||||
if (pluginInfo?.isPinned) {
|
||||
return {
|
||||
currentVersion: pluginInfo.pinnedVersion,
|
||||
latestVersion: null,
|
||||
isUpToDate: true,
|
||||
isLocalDev: false,
|
||||
isPinned: true,
|
||||
}
|
||||
}
|
||||
|
||||
const currentVersion = getCachedVersion()
|
||||
const latestVersion = await getLatestVersion()
|
||||
|
||||
const isUpToDate =
|
||||
current.isLocalDev ||
|
||||
current.isPinned ||
|
||||
!current.version ||
|
||||
!currentVersion ||
|
||||
!latestVersion ||
|
||||
compareVersions(current.version, latestVersion)
|
||||
compareVersions(currentVersion, latestVersion)
|
||||
|
||||
return {
|
||||
currentVersion: current.version,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
isUpToDate,
|
||||
isLocalDev: current.isLocalDev,
|
||||
isPinned: current.isPinned,
|
||||
isLocalDev: false,
|
||||
isPinned: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ export const HookNameSchema = z.enum([
|
||||
"empty-message-sanitizer",
|
||||
"thinking-block-validator",
|
||||
"ralph-loop",
|
||||
"preemptive-compaction",
|
||||
"compaction-context-injector",
|
||||
"claude-code-hooks",
|
||||
])
|
||||
|
||||
export const BuiltinCommandNameSchema = z.enum([
|
||||
|
||||
@@ -42,10 +42,8 @@ export function loadBuiltinCommands(
|
||||
|
||||
for (const [name, definition] of Object.entries(BUILTIN_COMMAND_DEFINITIONS)) {
|
||||
if (!disabled.has(name as BuiltinCommandName)) {
|
||||
commands[name] = {
|
||||
name,
|
||||
...definition,
|
||||
}
|
||||
const { argumentHint: _argumentHint, ...openCodeCompatible } = definition
|
||||
commands[name] = openCodeCompatible as CommandDefinition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,205 +1,191 @@
|
||||
export const INIT_DEEP_TEMPLATE = `# Initialize Deep Knowledge Base
|
||||
export const INIT_DEEP_TEMPLATE = `# /init-deep
|
||||
|
||||
Generate comprehensive AGENTS.md files across project hierarchy. Combines root-level project knowledge (gen-knowledge) with complexity-based subdirectory documentation (gen-knowledge-deep).
|
||||
Generate hierarchical AGENTS.md files. Root + complexity-scored subdirectories.
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`
|
||||
/init-deep # Analyze and generate hierarchical AGENTS.md
|
||||
/init-deep --create-new # Force create from scratch (ignore existing)
|
||||
/init-deep --max-depth=2 # Limit to N directory levels (default: 3)
|
||||
/init-deep # Update mode: modify existing + create new where warranted
|
||||
/init-deep --create-new # Read existing → remove all → regenerate from scratch
|
||||
/init-deep --max-depth=2 # Limit directory depth (default: 3)
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
## Workflow (High-Level)
|
||||
|
||||
- **Telegraphic Style**: Sacrifice grammar for concision ("Project uses React" → "React 18")
|
||||
- **Predict-then-Compare**: Predict standard → find actual → document ONLY deviations
|
||||
- **Hierarchy Aware**: Parent covers general, children cover specific
|
||||
- **No Redundancy**: Child AGENTS.md NEVER repeats parent content
|
||||
- **LSP-First**: Use LSP tools for accurate code intelligence when available (semantic > text search)
|
||||
|
||||
---
|
||||
|
||||
## Process
|
||||
1. **Discovery + Analysis** (concurrent)
|
||||
- Fire background explore agents immediately
|
||||
- Main session: bash structure + LSP codemap + read existing AGENTS.md
|
||||
2. **Score & Decide** - Determine AGENTS.md locations from merged findings
|
||||
3. **Generate** - Root first, then subdirs in parallel
|
||||
4. **Review** - Deduplicate, trim, validate
|
||||
|
||||
<critical>
|
||||
**MANDATORY: TodoWrite for ALL phases. Mark in_progress → completed in real-time.**
|
||||
</critical>
|
||||
|
||||
### Phase 0: Initialize
|
||||
|
||||
**TodoWrite ALL phases. Mark in_progress → completed in real-time.**
|
||||
\`\`\`
|
||||
TodoWrite([
|
||||
{ id: "p1-analysis", content: "Parallel project structure & complexity analysis", status: "pending", priority: "high" },
|
||||
{ id: "p2-scoring", content: "Score directories, determine AGENTS.md locations", status: "pending", priority: "high" },
|
||||
{ id: "p3-root", content: "Generate root AGENTS.md with Predict-then-Compare", status: "pending", priority: "high" },
|
||||
{ id: "p4-subdirs", content: "Generate subdirectory AGENTS.md files in parallel", status: "pending", priority: "high" },
|
||||
{ id: "p5-review", content: "Review, deduplicate, validate all files", status: "pending", priority: "medium" }
|
||||
{ id: "discovery", content: "Fire explore agents + LSP codemap + read existing", status: "pending", priority: "high" },
|
||||
{ id: "scoring", content: "Score directories, determine locations", status: "pending", priority: "high" },
|
||||
{ id: "generate", content: "Generate AGENTS.md files (root + subdirs)", status: "pending", priority: "high" },
|
||||
{ id: "review", content: "Deduplicate, validate, trim", status: "pending", priority: "medium" }
|
||||
])
|
||||
\`\`\`
|
||||
</critical>
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Parallel Project Analysis
|
||||
## Phase 1: Discovery + Analysis (Concurrent)
|
||||
|
||||
**Mark "p1-analysis" as in_progress.**
|
||||
**Mark "discovery" as in_progress.**
|
||||
|
||||
Launch **ALL tasks simultaneously**:
|
||||
### Fire Background Explore Agents IMMEDIATELY
|
||||
|
||||
<parallel-tasks>
|
||||
Don't wait—these run async while main session works.
|
||||
|
||||
\`\`\`
|
||||
// Fire all at once, collect results later
|
||||
background_task(agent="explore", prompt="Project structure: PREDICT standard patterns for detected language → REPORT deviations only")
|
||||
background_task(agent="explore", prompt="Entry points: FIND main files → REPORT non-standard organization")
|
||||
background_task(agent="explore", prompt="Conventions: FIND config files (.eslintrc, pyproject.toml, .editorconfig) → REPORT project-specific rules")
|
||||
background_task(agent="explore", prompt="Anti-patterns: FIND 'DO NOT', 'NEVER', 'ALWAYS', 'DEPRECATED' comments → LIST forbidden patterns")
|
||||
background_task(agent="explore", prompt="Build/CI: FIND .github/workflows, Makefile → REPORT non-standard patterns")
|
||||
background_task(agent="explore", prompt="Test patterns: FIND test configs, test structure → REPORT unique conventions")
|
||||
\`\`\`
|
||||
|
||||
<dynamic-agents>
|
||||
**DYNAMIC AGENT SPAWNING**: After bash analysis, spawn ADDITIONAL explore agents based on project scale:
|
||||
|
||||
| Factor | Threshold | Additional Agents |
|
||||
|--------|-----------|-------------------|
|
||||
| **Total files** | >100 | +1 per 100 files |
|
||||
| **Total lines** | >10k | +1 per 10k lines |
|
||||
| **Directory depth** | ≥4 | +2 for deep exploration |
|
||||
| **Large files (>500 lines)** | >10 files | +1 for complexity hotspots |
|
||||
| **Monorepo** | detected | +1 per package/workspace |
|
||||
| **Multiple languages** | >1 | +1 per language |
|
||||
|
||||
### Structural Analysis (bash - run in parallel)
|
||||
\`\`\`bash
|
||||
# Task A: Directory depth analysis
|
||||
find . -type d -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' -not -path '*/dist/*' -not -path '*/build/*' | awk -F/ '{print NF-1}' | sort -n | uniq -c
|
||||
# Measure project scale first
|
||||
total_files=$(find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | wc -l)
|
||||
total_lines=$(find . -type f \\( -name "*.ts" -o -name "*.py" -o -name "*.go" \\) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}')
|
||||
large_files=$(find . -type f \\( -name "*.ts" -o -name "*.py" \\) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | awk '$1 > 500 {count++} END {print count+0}')
|
||||
max_depth=$(find . -type d -not -path '*/node_modules/*' -not -path '*/.git/*' | awk -F/ '{print NF}' | sort -rn | head -1)
|
||||
\`\`\`
|
||||
|
||||
# Task B: File count per directory
|
||||
find . -type f -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -30
|
||||
Example spawning:
|
||||
\`\`\`
|
||||
// 500 files, 50k lines, depth 6, 15 large files → spawn 5+5+2+1 = 13 additional agents
|
||||
background_task(agent="explore", prompt="Large file analysis: FIND files >500 lines, REPORT complexity hotspots")
|
||||
background_task(agent="explore", prompt="Deep modules at depth 4+: FIND hidden patterns, internal conventions")
|
||||
background_task(agent="explore", prompt="Cross-cutting concerns: FIND shared utilities across directories")
|
||||
// ... more based on calculation
|
||||
\`\`\`
|
||||
</dynamic-agents>
|
||||
|
||||
# Task C: Code concentration
|
||||
find . -type f \\( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" -o -name "*.go" -o -name "*.rs" -o -name "*.java" \\) -not -path '*/node_modules/*' -not -path '*/venv/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -20
|
||||
### Main Session: Concurrent Analysis
|
||||
|
||||
# Task D: Existing knowledge files
|
||||
**While background agents run**, main session does:
|
||||
|
||||
#### 1. Bash Structural Analysis
|
||||
\`\`\`bash
|
||||
# Directory depth + file counts
|
||||
find . -type d -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/dist/*' -not -path '*/build/*' | awk -F/ '{print NF-1}' | sort -n | uniq -c
|
||||
|
||||
# Files per directory (top 30)
|
||||
find . -type f -not -path '*/\\.*' -not -path '*/node_modules/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -30
|
||||
|
||||
# Code concentration by extension
|
||||
find . -type f \\( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.go" -o -name "*.rs" \\) -not -path '*/node_modules/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
# Existing AGENTS.md / CLAUDE.md
|
||||
find . -type f \\( -name "AGENTS.md" -o -name "CLAUDE.md" \\) -not -path '*/node_modules/*' 2>/dev/null
|
||||
\`\`\`
|
||||
|
||||
### Context Gathering (Explore agents - background_task in parallel)
|
||||
|
||||
#### 2. Read Existing AGENTS.md
|
||||
\`\`\`
|
||||
background_task(agent="explore", prompt="Project structure: PREDICT standard {lang} patterns → FIND package.json/pyproject.toml/go.mod → REPORT deviations only")
|
||||
|
||||
background_task(agent="explore", prompt="Entry points: PREDICT typical (main.py, index.ts) → FIND actual → REPORT non-standard organization")
|
||||
|
||||
background_task(agent="explore", prompt="Conventions: FIND .cursor/rules, .cursorrules, eslintrc, pyproject.toml → REPORT project-specific rules DIFFERENT from defaults")
|
||||
|
||||
background_task(agent="explore", prompt="Anti-patterns: FIND comments with 'DO NOT', 'NEVER', 'ALWAYS', 'LEGACY', 'DEPRECATED' → REPORT forbidden patterns")
|
||||
|
||||
background_task(agent="explore", prompt="Build/CI: FIND .github/workflows, Makefile, justfile → REPORT non-standard build/deploy patterns")
|
||||
|
||||
background_task(agent="explore", prompt="Test patterns: FIND pytest.ini, jest.config, test structure → REPORT unique testing conventions")
|
||||
For each existing file found:
|
||||
Read(filePath=file)
|
||||
Extract: key insights, conventions, anti-patterns
|
||||
Store in EXISTING_AGENTS map
|
||||
\`\`\`
|
||||
|
||||
### Code Intelligence Analysis (LSP tools - run in parallel)
|
||||
|
||||
LSP provides semantic understanding beyond text search. Use for accurate code mapping.
|
||||
If \`--create-new\`: Read all existing first (preserve context) → then delete all → regenerate.
|
||||
|
||||
#### 3. LSP Codemap (if available)
|
||||
\`\`\`
|
||||
# Step 1: Check LSP availability
|
||||
lsp_servers() # Verify language server is available
|
||||
lsp_servers() # Check availability
|
||||
|
||||
# Step 2: Analyze entry point files (run in parallel)
|
||||
# Find entry points first, then analyze each with lsp_document_symbols
|
||||
lsp_document_symbols(filePath="src/index.ts") # Main entry
|
||||
lsp_document_symbols(filePath="src/main.py") # Python entry
|
||||
lsp_document_symbols(filePath="cmd/main.go") # Go entry
|
||||
# Entry points (parallel)
|
||||
lsp_document_symbols(filePath="src/index.ts")
|
||||
lsp_document_symbols(filePath="main.py")
|
||||
|
||||
# Step 3: Discover key symbols across workspace (run in parallel)
|
||||
lsp_workspace_symbols(filePath=".", query="class") # All classes
|
||||
lsp_workspace_symbols(filePath=".", query="interface") # All interfaces
|
||||
lsp_workspace_symbols(filePath=".", query="function") # Top-level functions
|
||||
lsp_workspace_symbols(filePath=".", query="type") # Type definitions
|
||||
# Key symbols (parallel)
|
||||
lsp_workspace_symbols(filePath=".", query="class")
|
||||
lsp_workspace_symbols(filePath=".", query="interface")
|
||||
lsp_workspace_symbols(filePath=".", query="function")
|
||||
|
||||
# Step 4: Analyze symbol centrality (for top 5-10 key symbols)
|
||||
# High reference count = central/important concept
|
||||
lsp_find_references(filePath="src/index.ts", line=X, character=Y) # Main export
|
||||
# Centrality for top exports
|
||||
lsp_find_references(filePath="...", line=X, character=Y)
|
||||
\`\`\`
|
||||
|
||||
#### LSP Analysis Output Format
|
||||
**LSP Fallback**: If unavailable, rely on explore agents + AST-grep.
|
||||
|
||||
### Collect Background Results
|
||||
|
||||
\`\`\`
|
||||
CODE_INTELLIGENCE = {
|
||||
entry_points: [
|
||||
{ file: "src/index.ts", exports: ["Plugin", "createHook"], symbol_count: 12 }
|
||||
],
|
||||
key_symbols: [
|
||||
{ name: "Plugin", type: "class", file: "src/index.ts", refs: 45, role: "Central orchestrator" },
|
||||
{ name: "createHook", type: "function", file: "src/utils.ts", refs: 23, role: "Hook factory" }
|
||||
],
|
||||
module_boundaries: [
|
||||
{ dir: "src/hooks", exports: 21, imports_from: ["shared/"] },
|
||||
{ dir: "src/tools", exports: 15, imports_from: ["shared/", "hooks/"] }
|
||||
]
|
||||
}
|
||||
// After main session analysis done, collect all task results
|
||||
for each task_id: background_output(task_id="...")
|
||||
\`\`\`
|
||||
|
||||
<critical>
|
||||
**LSP Fallback**: If LSP unavailable (no server installed), skip this section and rely on explore agents + AST-grep patterns.
|
||||
</critical>
|
||||
|
||||
</parallel-tasks>
|
||||
|
||||
**Collect all results. Mark "p1-analysis" as completed.**
|
||||
**Merge: bash + LSP + existing + explore findings. Mark "discovery" as completed.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Complexity Scoring & Location Decision
|
||||
## Phase 2: Scoring & Location Decision
|
||||
|
||||
**Mark "p2-scoring" as in_progress.**
|
||||
**Mark "scoring" as in_progress.**
|
||||
|
||||
### Scoring Matrix
|
||||
|
||||
| Factor | Weight | Threshold | Source |
|
||||
|--------|--------|-----------|--------|
|
||||
| File count | 3x | >20 files = high | bash |
|
||||
| Subdirectory count | 2x | >5 subdirs = high | bash |
|
||||
| Code file ratio | 2x | >70% code = high | bash |
|
||||
| Factor | Weight | High Threshold | Source |
|
||||
|--------|--------|----------------|--------|
|
||||
| File count | 3x | >20 | bash |
|
||||
| Subdir count | 2x | >5 | bash |
|
||||
| Code ratio | 2x | >70% | bash |
|
||||
| Unique patterns | 1x | Has own config | explore |
|
||||
| Module boundary | 2x | Has __init__.py/index.ts | bash |
|
||||
| **Symbol density** | 2x | >30 symbols = high | LSP |
|
||||
| **Export count** | 2x | >10 exports = high | LSP |
|
||||
| **Reference centrality** | 3x | Symbols with >20 refs | LSP |
|
||||
|
||||
<lsp-scoring>
|
||||
**LSP-Enhanced Scoring** (if available):
|
||||
|
||||
\`\`\`
|
||||
For each directory in candidates:
|
||||
symbols = lsp_document_symbols(dir/index.ts or dir/__init__.py)
|
||||
|
||||
symbol_score = len(symbols) > 30 ? 6 : len(symbols) > 15 ? 3 : 0
|
||||
export_score = count(exported symbols) > 10 ? 4 : 0
|
||||
|
||||
# Check if this module is central (many things depend on it)
|
||||
for each exported symbol:
|
||||
refs = lsp_find_references(symbol)
|
||||
if refs > 20: centrality_score += 3
|
||||
|
||||
total_score += symbol_score + export_score + centrality_score
|
||||
\`\`\`
|
||||
</lsp-scoring>
|
||||
| Module boundary | 2x | Has index.ts/__init__.py | bash |
|
||||
| Symbol density | 2x | >30 symbols | LSP |
|
||||
| Export count | 2x | >10 exports | LSP |
|
||||
| Reference centrality | 3x | >20 refs | LSP |
|
||||
|
||||
### Decision Rules
|
||||
|
||||
| Score | Action |
|
||||
|-------|--------|
|
||||
| **Root (.)** | ALWAYS create AGENTS.md |
|
||||
| **High (>15)** | Create dedicated AGENTS.md |
|
||||
| **Medium (8-15)** | Create if distinct domain |
|
||||
| **Low (<8)** | Skip, parent sufficient |
|
||||
|
||||
### Output Format
|
||||
| **Root (.)** | ALWAYS create |
|
||||
| **>15** | Create AGENTS.md |
|
||||
| **8-15** | Create if distinct domain |
|
||||
| **<8** | Skip (parent covers) |
|
||||
|
||||
### Output
|
||||
\`\`\`
|
||||
AGENTS_LOCATIONS = [
|
||||
{ path: ".", type: "root" },
|
||||
{ path: "src/api", score: 18, reason: "high complexity, 45 files" },
|
||||
{ path: "src/hooks", score: 12, reason: "distinct domain, unique patterns" },
|
||||
{ path: "src/hooks", score: 18, reason: "high complexity" },
|
||||
{ path: "src/api", score: 12, reason: "distinct domain" }
|
||||
]
|
||||
\`\`\`
|
||||
|
||||
**Mark "p2-scoring" as completed.**
|
||||
**Mark "scoring" as completed.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Generate Root AGENTS.md
|
||||
## Phase 3: Generate AGENTS.md
|
||||
|
||||
**Mark "p3-root" as in_progress.**
|
||||
**Mark "generate" as in_progress.**
|
||||
|
||||
Root AGENTS.md gets **full treatment** with Predict-then-Compare synthesis.
|
||||
|
||||
### Required Sections
|
||||
### Root AGENTS.md (Full Treatment)
|
||||
|
||||
\`\`\`markdown
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
@@ -209,153 +195,75 @@ Root AGENTS.md gets **full treatment** with Predict-then-Compare synthesis.
|
||||
**Branch:** {BRANCH}
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
{1-2 sentences: what project does, core tech stack}
|
||||
{1-2 sentences: what + core stack}
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
\\\`\\\`\\\`
|
||||
{project-root}/
|
||||
├── {dir}/ # {non-obvious purpose only}
|
||||
└── {entry} # entry point
|
||||
{root}/
|
||||
├── {dir}/ # {non-obvious purpose only}
|
||||
└── {entry}
|
||||
\\\`\\\`\\\`
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Add feature X | \\\`src/x/\\\` | {pattern hint} |
|
||||
|
||||
## CODE MAP
|
||||
|
||||
{Generated from LSP analysis - shows key symbols and their relationships}
|
||||
{From LSP - skip if unavailable or project <10 files}
|
||||
|
||||
| Symbol | Type | Location | Refs | Role |
|
||||
|--------|------|----------|------|------|
|
||||
| {MainClass} | Class | \\\`src/index.ts\\\` | {N} | {Central orchestrator} |
|
||||
| {createX} | Function | \\\`src/utils.ts\\\` | {N} | {Factory pattern} |
|
||||
| {Config} | Interface | \\\`src/types.ts\\\` | {N} | {Configuration contract} |
|
||||
|
||||
### Module Dependencies
|
||||
|
||||
\\\`\\\`\\\`
|
||||
{entry} ──imports──> {core/}
|
||||
│ │
|
||||
└──imports──> {utils/} <──imports── {features/}
|
||||
\\\`\\\`\\\`
|
||||
|
||||
<code-map-note>
|
||||
**Skip CODE MAP if**: LSP unavailable OR project too small (<10 files) OR no clear module boundaries.
|
||||
</code-map-note>
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
{ONLY deviations from standard - skip generic advice}
|
||||
|
||||
- **{rule}**: {specific detail}
|
||||
{ONLY deviations from standard}
|
||||
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
|
||||
{Things explicitly forbidden HERE}
|
||||
|
||||
- **{pattern}**: {why} → {alternative}
|
||||
{Explicitly forbidden here}
|
||||
|
||||
## UNIQUE STYLES
|
||||
|
||||
{Project-specific coding styles}
|
||||
|
||||
- **{style}**: {how different}
|
||||
{Project-specific}
|
||||
|
||||
## COMMANDS
|
||||
|
||||
\\\`\\\`\\\`bash
|
||||
{dev-command}
|
||||
{test-command}
|
||||
{build-command}
|
||||
{dev/test/build}
|
||||
\\\`\\\`\\\`
|
||||
|
||||
## NOTES
|
||||
|
||||
{Gotchas, non-obvious info}
|
||||
{Gotchas}
|
||||
\`\`\`
|
||||
|
||||
### Quality Gates
|
||||
**Quality gates**: 50-150 lines, no generic advice, no obvious info.
|
||||
|
||||
- [ ] Size: 50-150 lines
|
||||
- [ ] No generic advice ("write clean code")
|
||||
- [ ] No obvious info ("tests/ has tests")
|
||||
- [ ] Every item is project-specific
|
||||
### Subdirectory AGENTS.md (Parallel)
|
||||
|
||||
**Mark "p3-root" as completed.**
|
||||
Launch document-writer agents for each location:
|
||||
|
||||
\`\`\`
|
||||
for loc in AGENTS_LOCATIONS (except root):
|
||||
background_task(agent="document-writer", prompt=\\\`
|
||||
Generate AGENTS.md for: \${loc.path}
|
||||
- Reason: \${loc.reason}
|
||||
- 30-80 lines max
|
||||
- NEVER repeat parent content
|
||||
- Sections: OVERVIEW (1 line), STRUCTURE (if >5 subdirs), WHERE TO LOOK, CONVENTIONS (if different), ANTI-PATTERNS
|
||||
\\\`)
|
||||
\`\`\`
|
||||
|
||||
**Wait for all. Mark "generate" as completed.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Generate Subdirectory AGENTS.md
|
||||
## Phase 4: Review & Deduplicate
|
||||
|
||||
**Mark "p4-subdirs" as in_progress.**
|
||||
**Mark "review" as in_progress.**
|
||||
|
||||
For each location in AGENTS_LOCATIONS (except root), launch **parallel document-writer agents**:
|
||||
For each generated file:
|
||||
- Remove generic advice
|
||||
- Remove parent duplicates
|
||||
- Trim to size limits
|
||||
- Verify telegraphic style
|
||||
|
||||
\`\`\`typescript
|
||||
for (const loc of AGENTS_LOCATIONS.filter(l => l.path !== ".")) {
|
||||
background_task({
|
||||
agent: "document-writer",
|
||||
prompt: \\\`
|
||||
Generate AGENTS.md for: \${loc.path}
|
||||
|
||||
CONTEXT:
|
||||
- Complexity reason: \${loc.reason}
|
||||
- Parent AGENTS.md: ./AGENTS.md (already covers project overview)
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Focus ONLY on this directory's specific context
|
||||
2. NEVER repeat parent AGENTS.md content
|
||||
3. Shorter is better - 30-80 lines max
|
||||
4. Telegraphic style - sacrifice grammar
|
||||
|
||||
REQUIRED SECTIONS:
|
||||
- OVERVIEW (1 line: what this directory does)
|
||||
- STRUCTURE (only if >5 subdirs)
|
||||
- WHERE TO LOOK (directory-specific tasks)
|
||||
- CONVENTIONS (only if DIFFERENT from root)
|
||||
- ANTI-PATTERNS (directory-specific only)
|
||||
|
||||
OUTPUT: Write to \${loc.path}/AGENTS.md
|
||||
\\\`
|
||||
})
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**Wait for all agents. Mark "p4-subdirs" as completed.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Review & Deduplicate
|
||||
|
||||
**Mark "p5-review" as in_progress.**
|
||||
|
||||
### Validation Checklist
|
||||
|
||||
For EACH generated AGENTS.md:
|
||||
|
||||
| Check | Action if Fail |
|
||||
|-------|----------------|
|
||||
| Contains generic advice | REMOVE the line |
|
||||
| Repeats parent content | REMOVE the line |
|
||||
| Missing required section | ADD it |
|
||||
| Over 150 lines (root) / 80 lines (subdir) | TRIM |
|
||||
| Verbose explanations | REWRITE telegraphic |
|
||||
|
||||
### Cross-Reference Validation
|
||||
|
||||
\`\`\`
|
||||
For each child AGENTS.md:
|
||||
For each line in child:
|
||||
If similar line exists in parent:
|
||||
REMOVE from child (parent already covers)
|
||||
\`\`\`
|
||||
|
||||
**Mark "p5-review" as completed.**
|
||||
**Mark "review" as completed.**
|
||||
|
||||
---
|
||||
|
||||
@@ -364,31 +272,29 @@ For each child AGENTS.md:
|
||||
\`\`\`
|
||||
=== init-deep Complete ===
|
||||
|
||||
Files Generated:
|
||||
Mode: {update | create-new}
|
||||
|
||||
Files:
|
||||
✓ ./AGENTS.md (root, {N} lines)
|
||||
✓ ./src/hooks/AGENTS.md ({N} lines)
|
||||
✓ ./src/tools/AGENTS.md ({N} lines)
|
||||
|
||||
Directories Analyzed: {N}
|
||||
Dirs Analyzed: {N}
|
||||
AGENTS.md Created: {N}
|
||||
Total Lines: {N}
|
||||
AGENTS.md Updated: {N}
|
||||
|
||||
Hierarchy:
|
||||
./AGENTS.md
|
||||
├── src/hooks/AGENTS.md
|
||||
└── src/tools/AGENTS.md
|
||||
└── src/hooks/AGENTS.md
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns for THIS Command
|
||||
## Anti-Patterns
|
||||
|
||||
- **Over-documenting**: Not every directory needs AGENTS.md
|
||||
- **Redundancy**: Child must NOT repeat parent
|
||||
- **Static agent count**: MUST vary agents based on project size/depth
|
||||
- **Sequential execution**: MUST parallel (explore + LSP concurrent)
|
||||
- **Ignoring existing**: ALWAYS read existing first, even with --create-new
|
||||
- **Over-documenting**: Not every dir needs AGENTS.md
|
||||
- **Redundancy**: Child never repeats parent
|
||||
- **Generic content**: Remove anything that applies to ALL projects
|
||||
- **Sequential execution**: MUST use parallel agents
|
||||
- **Deep nesting**: Rarely need AGENTS.md at depth 4+
|
||||
- **Verbose style**: "This directory contains..." → just list it
|
||||
- **Ignoring LSP**: If LSP available, USE IT - semantic analysis > text grep
|
||||
- **LSP without fallback**: Always have explore agent backup if LSP unavailable
|
||||
- **Over-referencing**: Don't trace refs for EVERY symbol - focus on exports only`
|
||||
- **Verbose style**: Telegraphic or die`
|
||||
|
||||
@@ -62,7 +62,8 @@ $ARGUMENTS
|
||||
function commandsToRecord(commands: LoadedCommand[]): Record<string, CommandDefinition> {
|
||||
const result: Record<string, CommandDefinition> = {}
|
||||
for (const cmd of commands) {
|
||||
result[cmd.name] = cmd.definition
|
||||
const { name: _name, argumentHint: _argumentHint, ...openCodeCompatible } = cmd.definition
|
||||
result[cmd.name] = openCodeCompatible as CommandDefinition
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ $ARGUMENTS
|
||||
|
||||
const formattedDescription = `(plugin: ${plugin.name}) ${data.description || ""}`
|
||||
|
||||
commands[namespacedName] = {
|
||||
const definition = {
|
||||
name: namespacedName,
|
||||
description: formattedDescription,
|
||||
template: wrappedTemplate,
|
||||
@@ -255,6 +255,8 @@ $ARGUMENTS
|
||||
subtask: data.subtask,
|
||||
argumentHint: data["argument-hint"],
|
||||
}
|
||||
const { name: _name, argumentHint: _argumentHint, ...openCodeCompatible } = definition
|
||||
commands[namespacedName] = openCodeCompatible as CommandDefinition
|
||||
|
||||
log(`Loaded plugin command: ${namespacedName}`, { path: commandPath })
|
||||
} catch (error) {
|
||||
@@ -306,12 +308,14 @@ ${body.trim()}
|
||||
$ARGUMENTS
|
||||
</user-request>`
|
||||
|
||||
skills[namespacedName] = {
|
||||
const definition = {
|
||||
name: namespacedName,
|
||||
description: formattedDescription,
|
||||
template: wrappedTemplate,
|
||||
model: sanitizeModelField(data.model),
|
||||
}
|
||||
const { name: _name, ...openCodeCompatible } = definition
|
||||
skills[namespacedName] = openCodeCompatible as CommandDefinition
|
||||
|
||||
log(`Loaded plugin skill: ${namespacedName}`, { path: resolvedPath })
|
||||
} catch (error) {
|
||||
|
||||
@@ -126,7 +126,8 @@ function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkill[]
|
||||
function skillsToRecord(skills: LoadedSkill[]): Record<string, CommandDefinition> {
|
||||
const result: Record<string, CommandDefinition> = {}
|
||||
for (const skill of skills) {
|
||||
result[skill.name] = skill.definition
|
||||
const { name: _name, argumentHint: _argumentHint, ...openCodeCompatible } = skill.definition
|
||||
result[skill.name] = openCodeCompatible as CommandDefinition
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ describe("executeCompact lock management", () => {
|
||||
pendingCompact: new Set<string>(),
|
||||
errorDataBySession: new Map(),
|
||||
retryStateBySession: new Map(),
|
||||
fallbackStateBySession: new Map(),
|
||||
truncateStateBySession: new Map(),
|
||||
dcpStateBySession: new Map(),
|
||||
emptyContentAttemptBySession: new Map(),
|
||||
@@ -68,38 +67,6 @@ describe("executeCompact lock management", () => {
|
||||
expect(autoCompactState.compactionInProgress.has(sessionID)).toBe(false)
|
||||
})
|
||||
|
||||
test("clears lock when revert throws exception", async () => {
|
||||
// #given: Force revert path by exhausting retry attempts and making revert fail
|
||||
mockClient.session.revert = mock(() =>
|
||||
Promise.reject(new Error("Revert failed")),
|
||||
)
|
||||
mockClient.session.messages = mock(() =>
|
||||
Promise.resolve({
|
||||
data: [
|
||||
{ info: { id: "msg1", role: "user" } },
|
||||
{ info: { id: "msg2", role: "assistant" } },
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
// Exhaust retry attempts
|
||||
autoCompactState.retryStateBySession.set(sessionID, {
|
||||
attempt: 5,
|
||||
lastAttemptTime: Date.now(),
|
||||
})
|
||||
autoCompactState.errorDataBySession.set(sessionID, {
|
||||
errorType: "token_limit",
|
||||
currentTokens: 100000,
|
||||
maxTokens: 200000,
|
||||
})
|
||||
|
||||
// #when: Execute compaction
|
||||
await executeCompact(sessionID, msg, autoCompactState, mockClient, directory)
|
||||
|
||||
// #then: Lock cleared even though revert failed
|
||||
expect(autoCompactState.compactionInProgress.has(sessionID)).toBe(false)
|
||||
})
|
||||
|
||||
test("shows toast when lock already held", async () => {
|
||||
// #given: Lock already held
|
||||
autoCompactState.compactionInProgress.add(sessionID)
|
||||
@@ -195,9 +162,6 @@ describe("executeCompact lock management", () => {
|
||||
attempt: 5,
|
||||
lastAttemptTime: Date.now(),
|
||||
})
|
||||
autoCompactState.fallbackStateBySession.set(sessionID, {
|
||||
revertAttempt: 5,
|
||||
})
|
||||
autoCompactState.truncateStateBySession.set(sessionID, {
|
||||
truncateAttempt: 5,
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type {
|
||||
AutoCompactState,
|
||||
DcpState,
|
||||
FallbackState,
|
||||
RetryState,
|
||||
TruncateState,
|
||||
} from "./types";
|
||||
import type { ExperimentalConfig } from "../../config";
|
||||
import { FALLBACK_CONFIG, RETRY_CONFIG, TRUNCATE_CONFIG } from "./types";
|
||||
import { RETRY_CONFIG, TRUNCATE_CONFIG } from "./types";
|
||||
import { executeDynamicContextPruning } from "./pruning-executor";
|
||||
import {
|
||||
findLargestToolResult,
|
||||
@@ -69,17 +68,7 @@ function getOrCreateRetryState(
|
||||
return state;
|
||||
}
|
||||
|
||||
function getOrCreateFallbackState(
|
||||
autoCompactState: AutoCompactState,
|
||||
sessionID: string,
|
||||
): FallbackState {
|
||||
let state = autoCompactState.fallbackStateBySession.get(sessionID);
|
||||
if (!state) {
|
||||
state = { revertAttempt: 0 };
|
||||
autoCompactState.fallbackStateBySession.set(sessionID, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
function getOrCreateTruncateState(
|
||||
autoCompactState: AutoCompactState,
|
||||
@@ -135,58 +124,6 @@ function sanitizeEmptyMessagesBeforeSummarize(sessionID: string): number {
|
||||
return fixedCount;
|
||||
}
|
||||
|
||||
async function getLastMessagePair(
|
||||
sessionID: string,
|
||||
client: Client,
|
||||
directory: string,
|
||||
): Promise<{ userMessageID: string; assistantMessageID?: string } | null> {
|
||||
try {
|
||||
const resp = await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
query: { directory },
|
||||
});
|
||||
|
||||
const data = (resp as { data?: unknown[] }).data;
|
||||
if (
|
||||
!Array.isArray(data) ||
|
||||
data.length < FALLBACK_CONFIG.minMessagesRequired
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reversed = [...data].reverse();
|
||||
|
||||
const lastAssistant = reversed.find((m) => {
|
||||
const msg = m as Record<string, unknown>;
|
||||
const info = msg.info as Record<string, unknown> | undefined;
|
||||
return info?.role === "assistant";
|
||||
});
|
||||
|
||||
const lastUser = reversed.find((m) => {
|
||||
const msg = m as Record<string, unknown>;
|
||||
const info = msg.info as Record<string, unknown> | undefined;
|
||||
return info?.role === "user";
|
||||
});
|
||||
|
||||
if (!lastUser) return null;
|
||||
const userInfo = (lastUser as { info?: Record<string, unknown> }).info;
|
||||
const userMessageID = userInfo?.id as string | undefined;
|
||||
if (!userMessageID) return null;
|
||||
|
||||
let assistantMessageID: string | undefined;
|
||||
if (lastAssistant) {
|
||||
const assistantInfo = (
|
||||
lastAssistant as { info?: Record<string, unknown> }
|
||||
).info;
|
||||
assistantMessageID = assistantInfo?.id as string | undefined;
|
||||
}
|
||||
|
||||
return { userMessageID, assistantMessageID };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
@@ -228,7 +165,6 @@ function clearSessionState(
|
||||
autoCompactState.pendingCompact.delete(sessionID);
|
||||
autoCompactState.errorDataBySession.delete(sessionID);
|
||||
autoCompactState.retryStateBySession.delete(sessionID);
|
||||
autoCompactState.fallbackStateBySession.delete(sessionID);
|
||||
autoCompactState.truncateStateBySession.delete(sessionID);
|
||||
autoCompactState.dcpStateBySession.delete(sessionID);
|
||||
autoCompactState.emptyContentAttemptBySession.delete(sessionID);
|
||||
@@ -642,7 +578,6 @@ export async function executeCompact(
|
||||
|
||||
if (Date.now() - retryState.lastAttemptTime > 300000) {
|
||||
retryState.attempt = 0;
|
||||
autoCompactState.fallbackStateBySession.delete(sessionID);
|
||||
autoCompactState.truncateStateBySession.delete(sessionID);
|
||||
}
|
||||
|
||||
@@ -708,75 +643,7 @@ export async function executeCompact(
|
||||
.showToast({
|
||||
body: {
|
||||
title: "Summarize Skipped",
|
||||
message: "Missing providerID or modelID. Skipping to revert...",
|
||||
variant: "warning",
|
||||
duration: 3000,
|
||||
},
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackState = getOrCreateFallbackState(autoCompactState, sessionID);
|
||||
|
||||
if (fallbackState.revertAttempt < FALLBACK_CONFIG.maxRevertAttempts) {
|
||||
const pair = await getLastMessagePair(
|
||||
sessionID,
|
||||
client as Client,
|
||||
directory,
|
||||
);
|
||||
|
||||
if (pair) {
|
||||
try {
|
||||
await (client as Client).tui
|
||||
.showToast({
|
||||
body: {
|
||||
title: "Emergency Recovery",
|
||||
message: "Removing last message pair...",
|
||||
variant: "warning",
|
||||
duration: 3000,
|
||||
},
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (pair.assistantMessageID) {
|
||||
await (client as Client).session.revert({
|
||||
path: { id: sessionID },
|
||||
body: { messageID: pair.assistantMessageID },
|
||||
query: { directory },
|
||||
});
|
||||
}
|
||||
|
||||
await (client as Client).session.revert({
|
||||
path: { id: sessionID },
|
||||
body: { messageID: pair.userMessageID },
|
||||
query: { directory },
|
||||
});
|
||||
|
||||
fallbackState.revertAttempt++;
|
||||
fallbackState.lastRevertedMessageID = pair.userMessageID;
|
||||
|
||||
// Clear all state after successful revert - don't recurse
|
||||
clearSessionState(autoCompactState, sessionID);
|
||||
|
||||
// Send "Continue" prompt to resume session
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await (client as Client).session.prompt_async({
|
||||
path: { sessionID },
|
||||
body: { parts: [{ type: "text", text: "Continue" }] },
|
||||
query: { directory },
|
||||
});
|
||||
} catch {}
|
||||
}, 500);
|
||||
return;
|
||||
} catch {}
|
||||
} else {
|
||||
await (client as Client).tui
|
||||
.showToast({
|
||||
body: {
|
||||
title: "Revert Skipped",
|
||||
message: "Could not find last message pair to revert.",
|
||||
message: "Missing providerID or modelID.",
|
||||
variant: "warning",
|
||||
duration: 3000,
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ function createRecoveryState(): AutoCompactState {
|
||||
pendingCompact: new Set<string>(),
|
||||
errorDataBySession: new Map<string, ParsedTokenLimitError>(),
|
||||
retryStateBySession: new Map(),
|
||||
fallbackStateBySession: new Map(),
|
||||
truncateStateBySession: new Map(),
|
||||
dcpStateBySession: new Map(),
|
||||
emptyContentAttemptBySession: new Map(),
|
||||
@@ -37,7 +36,6 @@ export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput,
|
||||
autoCompactState.pendingCompact.delete(sessionInfo.id)
|
||||
autoCompactState.errorDataBySession.delete(sessionInfo.id)
|
||||
autoCompactState.retryStateBySession.delete(sessionInfo.id)
|
||||
autoCompactState.fallbackStateBySession.delete(sessionInfo.id)
|
||||
autoCompactState.truncateStateBySession.delete(sessionInfo.id)
|
||||
autoCompactState.dcpStateBySession.delete(sessionInfo.id)
|
||||
autoCompactState.emptyContentAttemptBySession.delete(sessionInfo.id)
|
||||
@@ -154,6 +152,6 @@ export function createAnthropicContextWindowLimitRecoveryHook(ctx: PluginInput,
|
||||
}
|
||||
}
|
||||
|
||||
export type { AutoCompactState, DcpState, FallbackState, ParsedTokenLimitError, TruncateState } from "./types"
|
||||
export type { AutoCompactState, DcpState, ParsedTokenLimitError, TruncateState } from "./types"
|
||||
export { parseAnthropicTokenLimitError } from "./parser"
|
||||
export { executeCompact, getLastAssistant } from "./executor"
|
||||
|
||||
@@ -13,11 +13,6 @@ export interface RetryState {
|
||||
lastAttemptTime: number
|
||||
}
|
||||
|
||||
export interface FallbackState {
|
||||
revertAttempt: number
|
||||
lastRevertedMessageID?: string
|
||||
}
|
||||
|
||||
export interface TruncateState {
|
||||
truncateAttempt: number
|
||||
lastTruncatedPartId?: string
|
||||
@@ -32,7 +27,6 @@ export interface AutoCompactState {
|
||||
pendingCompact: Set<string>
|
||||
errorDataBySession: Map<string, ParsedTokenLimitError>
|
||||
retryStateBySession: Map<string, RetryState>
|
||||
fallbackStateBySession: Map<string, FallbackState>
|
||||
truncateStateBySession: Map<string, TruncateState>
|
||||
dcpStateBySession: Map<string, DcpState>
|
||||
emptyContentAttemptBySession: Map<string, number>
|
||||
@@ -46,11 +40,6 @@ export const RETRY_CONFIG = {
|
||||
maxDelayMs: 30000,
|
||||
} as const
|
||||
|
||||
export const FALLBACK_CONFIG = {
|
||||
maxRevertAttempts: 3,
|
||||
minMessagesRequired: 2,
|
||||
} as const
|
||||
|
||||
export const TRUNCATE_CONFIG = {
|
||||
maxTruncateAttempts: 20,
|
||||
minOutputSizeToTruncate: 500,
|
||||
|
||||
@@ -82,10 +82,12 @@ export function createPreemptiveCompactionHook(
|
||||
const experimental = options?.experimental
|
||||
const onBeforeSummarize = options?.onBeforeSummarize
|
||||
const getModelLimit = options?.getModelLimit
|
||||
const enabled = experimental?.preemptive_compaction === true
|
||||
// Preemptive compaction is now enabled by default.
|
||||
// Backward compatibility: explicit false in experimental config disables the hook.
|
||||
const explicitlyDisabled = experimental?.preemptive_compaction === false
|
||||
const threshold = experimental?.preemptive_compaction_threshold ?? DEFAULT_THRESHOLD
|
||||
|
||||
if (!enabled) {
|
||||
if (explicitlyDisabled) {
|
||||
return { event: async () => {} }
|
||||
}
|
||||
|
||||
|
||||
81
src/index.ts
81
src/index.ts
@@ -61,74 +61,11 @@ import { builtinTools, createCallOmoAgent, createBackgroundTools, createLookAt,
|
||||
import { BackgroundManager } from "./features/background-agent";
|
||||
import { createBuiltinMcps } from "./mcp";
|
||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
|
||||
import { log, deepMerge, getUserConfigDir, addConfigLoadError, parseJsonc, detectConfigFile } from "./shared";
|
||||
import { log, deepMerge, getUserConfigDir, addConfigLoadError, parseJsonc, detectConfigFile, migrateConfigFile } from "./shared";
|
||||
import { PLAN_SYSTEM_PROMPT, PLAN_PERMISSION } from "./agents/plan-prompt";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
// Migration map: old keys → new keys (for backward compatibility)
|
||||
const AGENT_NAME_MAP: Record<string, string> = {
|
||||
// Legacy names (backward compatibility)
|
||||
omo: "Sisyphus",
|
||||
"OmO": "Sisyphus",
|
||||
"OmO-Plan": "Planner-Sisyphus",
|
||||
"omo-plan": "Planner-Sisyphus",
|
||||
// Current names
|
||||
sisyphus: "Sisyphus",
|
||||
"planner-sisyphus": "Planner-Sisyphus",
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
explore: "explore",
|
||||
"frontend-ui-ux-engineer": "frontend-ui-ux-engineer",
|
||||
"document-writer": "document-writer",
|
||||
"multimodal-looker": "multimodal-looker",
|
||||
};
|
||||
|
||||
function migrateAgentNames(agents: Record<string, unknown>): { migrated: Record<string, unknown>; changed: boolean } {
|
||||
const migrated: Record<string, unknown> = {};
|
||||
let changed = false;
|
||||
|
||||
for (const [key, value] of Object.entries(agents)) {
|
||||
const newKey = AGENT_NAME_MAP[key.toLowerCase()] ?? AGENT_NAME_MAP[key] ?? key;
|
||||
if (newKey !== key) {
|
||||
changed = true;
|
||||
}
|
||||
migrated[newKey] = value;
|
||||
}
|
||||
|
||||
return { migrated, changed };
|
||||
}
|
||||
|
||||
function migrateConfigFile(configPath: string, rawConfig: Record<string, unknown>): boolean {
|
||||
let needsWrite = false;
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const { migrated, changed } = migrateAgentNames(rawConfig.agents as Record<string, unknown>);
|
||||
if (changed) {
|
||||
rawConfig.agents = migrated;
|
||||
needsWrite = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.omo_agent) {
|
||||
rawConfig.sisyphus_agent = rawConfig.omo_agent;
|
||||
delete rawConfig.omo_agent;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
if (needsWrite) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8");
|
||||
log(`Migrated config file: ${configPath} (OmO → Sisyphus)`);
|
||||
} catch (err) {
|
||||
log(`Failed to write migrated config to ${configPath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return needsWrite;
|
||||
}
|
||||
|
||||
function loadConfigFromPath(configPath: string, ctx: any): OhMyOpenCodeConfig | null {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
@@ -279,12 +216,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
dcpForCompaction: pluginConfig.experimental?.dcp_for_compaction,
|
||||
})
|
||||
: null;
|
||||
const compactionContextInjector = createCompactionContextInjector();
|
||||
const preemptiveCompaction = createPreemptiveCompactionHook(ctx, {
|
||||
experimental: pluginConfig.experimental,
|
||||
onBeforeSummarize: compactionContextInjector,
|
||||
getModelLimit,
|
||||
});
|
||||
const compactionContextInjector = isHookEnabled("compaction-context-injector")
|
||||
? createCompactionContextInjector()
|
||||
: undefined;
|
||||
const preemptiveCompaction = isHookEnabled("preemptive-compaction")
|
||||
? createPreemptiveCompactionHook(ctx, {
|
||||
experimental: pluginConfig.experimental,
|
||||
onBeforeSummarize: compactionContextInjector,
|
||||
getModelLimit,
|
||||
})
|
||||
: null;
|
||||
const rulesInjector = isHookEnabled("rules-injector")
|
||||
? createRulesInjectorHook(ctx)
|
||||
: null;
|
||||
|
||||
82
src/shared/AGENTS.md
Normal file
82
src/shared/AGENTS.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# SHARED UTILITIES KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Cross-cutting utility functions used across agents, hooks, tools, and features. Path resolution, config management, text processing, and Claude Code compatibility helpers.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
shared/
|
||||
├── index.ts # Barrel export (import { x } from "../shared")
|
||||
├── claude-config-dir.ts # Resolve ~/.claude directory
|
||||
├── command-executor.ts # Shell command execution with variable expansion
|
||||
├── config-errors.ts # Global config error tracking
|
||||
├── config-path.ts # User/project config path resolution
|
||||
├── data-path.ts # XDG data directory resolution
|
||||
├── deep-merge.ts # Type-safe recursive object merging
|
||||
├── dynamic-truncator.ts # Token-aware output truncation
|
||||
├── file-reference-resolver.ts # @filename syntax resolution
|
||||
├── file-utils.ts # Symlink resolution, markdown detection
|
||||
├── frontmatter.ts # YAML frontmatter parsing
|
||||
├── hook-disabled.ts # Check if hook is disabled in config
|
||||
├── jsonc-parser.ts # JSON with Comments parsing
|
||||
├── logger.ts # File-based logging to OS temp
|
||||
├── migration.ts # Legacy name compatibility (omo -> Sisyphus)
|
||||
├── model-sanitizer.ts # Normalize model names
|
||||
├── pattern-matcher.ts # Tool name matching with wildcards
|
||||
├── snake-case.ts # Case conversion for objects
|
||||
└── tool-name.ts # Normalize tool names to PascalCase
|
||||
```
|
||||
|
||||
## UTILITY CATEGORIES
|
||||
|
||||
| Category | Utilities | Used By |
|
||||
|----------|-----------|---------|
|
||||
| Path Resolution | `getClaudeConfigDir`, `getUserConfigPath`, `getProjectConfigPath`, `getDataDir` | Features, Hooks |
|
||||
| Config Management | `deepMerge`, `parseJsonc`, `isHookDisabled`, `configErrors` | index.ts, CLI |
|
||||
| Text Processing | `resolveCommandsInText`, `resolveFileReferencesInText`, `parseFrontmatter` | Commands, Rules |
|
||||
| Output Control | `dynamicTruncate` | Tools (Grep, LSP) |
|
||||
| Normalization | `transformToolName`, `objectToSnakeCase`, `sanitizeModelName` | Hooks, Agents |
|
||||
| Compatibility | `migration.ts` | Config loading |
|
||||
|
||||
## WHEN TO USE WHAT
|
||||
|
||||
| Task | Utility | Notes |
|
||||
|------|---------|-------|
|
||||
| Find Claude Code configs | `getClaudeConfigDir()` | Never hardcode `~/.claude` |
|
||||
| Merge settings (default → user → project) | `deepMerge(base, override)` | Arrays replaced, objects merged |
|
||||
| Parse user config files | `parseJsonc()` | Supports comments and trailing commas |
|
||||
| Check if hook should run | `isHookDisabled(name, disabledHooks)` | Respects `disabled_hooks` config |
|
||||
| Truncate large tool output | `dynamicTruncate(text, budget, reserved)` | Token-aware, prevents overflow |
|
||||
| Resolve `@file` references | `resolveFileReferencesInText()` | maxDepth=3 prevents infinite loops |
|
||||
| Execute shell commands | `resolveCommandsInText()` | Supports `!`\`command\`\` syntax |
|
||||
| Handle legacy agent names | `migrateLegacyAgentNames()` | `omo` → `Sisyphus` |
|
||||
|
||||
## CRITICAL PATTERNS
|
||||
|
||||
### Dynamic Truncation
|
||||
```typescript
|
||||
import { dynamicTruncate } from "../shared"
|
||||
// Keep 50% headroom, max 50k tokens
|
||||
const output = dynamicTruncate(result, remainingTokens, 0.5)
|
||||
```
|
||||
|
||||
### Deep Merge Priority
|
||||
```typescript
|
||||
const final = deepMerge(defaults, userConfig)
|
||||
final = deepMerge(final, projectConfig) // Project wins
|
||||
```
|
||||
|
||||
### Safe JSONC Parsing
|
||||
```typescript
|
||||
const { config, error } = parseJsoncSafe(content)
|
||||
if (error) return fallback
|
||||
```
|
||||
|
||||
## ANTI-PATTERNS (SHARED)
|
||||
|
||||
- **Hardcoding paths**: Use `getClaudeConfigDir()`, `getUserConfigPath()`
|
||||
- **Manual JSON.parse**: Use `parseJsonc()` for user files (comments allowed)
|
||||
- **Ignoring truncation**: Large outputs MUST use `dynamicTruncate`
|
||||
- **Direct string concat for configs**: Use `deepMerge` for proper priority
|
||||
@@ -15,3 +15,4 @@ export * from "./data-path"
|
||||
export * from "./config-errors"
|
||||
export * from "./claude-config-dir"
|
||||
export * from "./jsonc-parser"
|
||||
export * from "./migration"
|
||||
|
||||
243
src/shared/migration.test.ts
Normal file
243
src/shared/migration.test.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import {
|
||||
AGENT_NAME_MAP,
|
||||
HOOK_NAME_MAP,
|
||||
migrateAgentNames,
|
||||
migrateHookNames,
|
||||
migrateConfigFile,
|
||||
} from "./migration"
|
||||
|
||||
describe("migrateAgentNames", () => {
|
||||
test("migrates legacy OmO names to Sisyphus", () => {
|
||||
// #given: Config with legacy OmO agent names
|
||||
const agents = {
|
||||
omo: { model: "anthropic/claude-opus-4-5" },
|
||||
OmO: { temperature: 0.5 },
|
||||
"OmO-Plan": { prompt: "custom prompt" },
|
||||
}
|
||||
|
||||
// #when: Migrate agent names
|
||||
const { migrated, changed } = migrateAgentNames(agents)
|
||||
|
||||
// #then: Legacy names should be migrated to Sisyphus
|
||||
expect(changed).toBe(true)
|
||||
expect(migrated["Sisyphus"]).toEqual({ temperature: 0.5 })
|
||||
expect(migrated["Planner-Sisyphus"]).toEqual({ prompt: "custom prompt" })
|
||||
expect(migrated["omo"]).toBeUndefined()
|
||||
expect(migrated["OmO"]).toBeUndefined()
|
||||
expect(migrated["OmO-Plan"]).toBeUndefined()
|
||||
})
|
||||
|
||||
test("preserves current agent names unchanged", () => {
|
||||
// #given: Config with current agent names
|
||||
const agents = {
|
||||
oracle: { model: "openai/gpt-5.2" },
|
||||
librarian: { model: "google/gemini-3-flash" },
|
||||
explore: { model: "opencode/grok-code" },
|
||||
}
|
||||
|
||||
// #when: Migrate agent names
|
||||
const { migrated, changed } = migrateAgentNames(agents)
|
||||
|
||||
// #then: Current names should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.2" })
|
||||
expect(migrated["librarian"]).toEqual({ model: "google/gemini-3-flash" })
|
||||
expect(migrated["explore"]).toEqual({ model: "opencode/grok-code" })
|
||||
})
|
||||
|
||||
test("handles case-insensitive migration", () => {
|
||||
// #given: Config with mixed case agent names
|
||||
const agents = {
|
||||
SISYPHUS: { model: "test" },
|
||||
"PLANNER-SISYPHUS": { prompt: "test" },
|
||||
}
|
||||
|
||||
// #when: Migrate agent names
|
||||
const { migrated, changed } = migrateAgentNames(agents)
|
||||
|
||||
// #then: Case-insensitive lookup should migrate correctly
|
||||
expect(migrated["Sisyphus"]).toEqual({ model: "test" })
|
||||
expect(migrated["Planner-Sisyphus"]).toEqual({ prompt: "test" })
|
||||
})
|
||||
|
||||
test("passes through unknown agent names unchanged", () => {
|
||||
// #given: Config with unknown agent name
|
||||
const agents = {
|
||||
"custom-agent": { model: "custom/model" },
|
||||
}
|
||||
|
||||
// #when: Migrate agent names
|
||||
const { migrated, changed } = migrateAgentNames(agents)
|
||||
|
||||
// #then: Unknown names should pass through
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated["custom-agent"]).toEqual({ model: "custom/model" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateHookNames", () => {
|
||||
test("migrates anthropic-auto-compact to anthropic-context-window-limit-recovery", () => {
|
||||
// #given: Config with legacy hook name
|
||||
const hooks = ["anthropic-auto-compact", "comment-checker"]
|
||||
|
||||
// #when: Migrate hook names
|
||||
const { migrated, changed } = migrateHookNames(hooks)
|
||||
|
||||
// #then: Legacy hook name should be migrated
|
||||
expect(changed).toBe(true)
|
||||
expect(migrated).toContain("anthropic-context-window-limit-recovery")
|
||||
expect(migrated).toContain("comment-checker")
|
||||
expect(migrated).not.toContain("anthropic-auto-compact")
|
||||
})
|
||||
|
||||
test("preserves current hook names unchanged", () => {
|
||||
// #given: Config with current hook names
|
||||
const hooks = [
|
||||
"anthropic-context-window-limit-recovery",
|
||||
"todo-continuation-enforcer",
|
||||
"session-recovery",
|
||||
]
|
||||
|
||||
// #when: Migrate hook names
|
||||
const { migrated, changed } = migrateHookNames(hooks)
|
||||
|
||||
// #then: Current names should remain unchanged
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated).toEqual(hooks)
|
||||
})
|
||||
|
||||
test("handles empty hooks array", () => {
|
||||
// #given: Empty hooks array
|
||||
const hooks: string[] = []
|
||||
|
||||
// #when: Migrate hook names
|
||||
const { migrated, changed } = migrateHookNames(hooks)
|
||||
|
||||
// #then: Should return empty array with no changes
|
||||
expect(changed).toBe(false)
|
||||
expect(migrated).toEqual([])
|
||||
})
|
||||
|
||||
test("migrates multiple legacy hook names", () => {
|
||||
// #given: Multiple legacy hook names (if more are added in future)
|
||||
const hooks = ["anthropic-auto-compact"]
|
||||
|
||||
// #when: Migrate hook names
|
||||
const { migrated, changed } = migrateHookNames(hooks)
|
||||
|
||||
// #then: All legacy names should be migrated
|
||||
expect(changed).toBe(true)
|
||||
expect(migrated).toEqual(["anthropic-context-window-limit-recovery"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateConfigFile", () => {
|
||||
const testConfigPath = "/tmp/nonexistent-path-for-test.json"
|
||||
|
||||
test("migrates omo_agent to sisyphus_agent", () => {
|
||||
// #given: Config with legacy omo_agent key
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
omo_agent: { disabled: false },
|
||||
}
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: omo_agent should be migrated to sisyphus_agent
|
||||
expect(needsWrite).toBe(true)
|
||||
expect(rawConfig.sisyphus_agent).toEqual({ disabled: false })
|
||||
expect(rawConfig.omo_agent).toBeUndefined()
|
||||
})
|
||||
|
||||
test("migrates legacy agent names in agents object", () => {
|
||||
// #given: Config with legacy agent names
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
agents: {
|
||||
omo: { model: "test" },
|
||||
OmO: { temperature: 0.5 },
|
||||
},
|
||||
}
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Agent names should be migrated
|
||||
expect(needsWrite).toBe(true)
|
||||
const agents = rawConfig.agents as Record<string, unknown>
|
||||
expect(agents["Sisyphus"]).toBeDefined()
|
||||
})
|
||||
|
||||
test("migrates legacy hook names in disabled_hooks", () => {
|
||||
// #given: Config with legacy hook names
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
disabled_hooks: ["anthropic-auto-compact", "comment-checker"],
|
||||
}
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: Hook names should be migrated
|
||||
expect(needsWrite).toBe(true)
|
||||
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
|
||||
expect(rawConfig.disabled_hooks).not.toContain("anthropic-auto-compact")
|
||||
})
|
||||
|
||||
test("does not write if no migration needed", () => {
|
||||
// #given: Config with current names
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
sisyphus_agent: { disabled: false },
|
||||
agents: {
|
||||
Sisyphus: { model: "test" },
|
||||
},
|
||||
disabled_hooks: ["anthropic-context-window-limit-recovery"],
|
||||
}
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: No write should be needed
|
||||
expect(needsWrite).toBe(false)
|
||||
})
|
||||
|
||||
test("handles migration of all legacy items together", () => {
|
||||
// #given: Config with all legacy items
|
||||
const rawConfig: Record<string, unknown> = {
|
||||
omo_agent: { disabled: false },
|
||||
agents: {
|
||||
omo: { model: "test" },
|
||||
"OmO-Plan": { prompt: "custom" },
|
||||
},
|
||||
disabled_hooks: ["anthropic-auto-compact"],
|
||||
}
|
||||
|
||||
// #when: Migrate config file
|
||||
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
|
||||
|
||||
// #then: All legacy items should be migrated
|
||||
expect(needsWrite).toBe(true)
|
||||
expect(rawConfig.sisyphus_agent).toEqual({ disabled: false })
|
||||
expect(rawConfig.omo_agent).toBeUndefined()
|
||||
const agents = rawConfig.agents as Record<string, unknown>
|
||||
expect(agents["Sisyphus"]).toBeDefined()
|
||||
expect(agents["Planner-Sisyphus"]).toBeDefined()
|
||||
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
|
||||
})
|
||||
})
|
||||
|
||||
describe("migration maps", () => {
|
||||
test("AGENT_NAME_MAP contains all expected legacy mappings", () => {
|
||||
// #given/#when: Check AGENT_NAME_MAP
|
||||
// #then: Should contain all legacy → current mappings
|
||||
expect(AGENT_NAME_MAP["omo"]).toBe("Sisyphus")
|
||||
expect(AGENT_NAME_MAP["OmO"]).toBe("Sisyphus")
|
||||
expect(AGENT_NAME_MAP["OmO-Plan"]).toBe("Planner-Sisyphus")
|
||||
expect(AGENT_NAME_MAP["omo-plan"]).toBe("Planner-Sisyphus")
|
||||
})
|
||||
|
||||
test("HOOK_NAME_MAP contains anthropic-auto-compact migration", () => {
|
||||
// #given/#when: Check HOOK_NAME_MAP
|
||||
// #then: Should contain the legacy hook name mapping
|
||||
expect(HOOK_NAME_MAP["anthropic-auto-compact"]).toBe("anthropic-context-window-limit-recovery")
|
||||
})
|
||||
})
|
||||
94
src/shared/migration.ts
Normal file
94
src/shared/migration.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as fs from "fs"
|
||||
import { log } from "./logger"
|
||||
|
||||
// Migration map: old keys → new keys (for backward compatibility)
|
||||
export const AGENT_NAME_MAP: Record<string, string> = {
|
||||
// Legacy names (backward compatibility)
|
||||
omo: "Sisyphus",
|
||||
"OmO": "Sisyphus",
|
||||
"OmO-Plan": "Planner-Sisyphus",
|
||||
"omo-plan": "Planner-Sisyphus",
|
||||
// Current names
|
||||
sisyphus: "Sisyphus",
|
||||
"planner-sisyphus": "Planner-Sisyphus",
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
explore: "explore",
|
||||
"frontend-ui-ux-engineer": "frontend-ui-ux-engineer",
|
||||
"document-writer": "document-writer",
|
||||
"multimodal-looker": "multimodal-looker",
|
||||
}
|
||||
|
||||
// Migration map: old hook names → new hook names (for backward compatibility)
|
||||
export const HOOK_NAME_MAP: Record<string, string> = {
|
||||
// Legacy names (backward compatibility)
|
||||
"anthropic-auto-compact": "anthropic-context-window-limit-recovery",
|
||||
}
|
||||
|
||||
export function migrateAgentNames(agents: Record<string, unknown>): { migrated: Record<string, unknown>; changed: boolean } {
|
||||
const migrated: Record<string, unknown> = {}
|
||||
let changed = false
|
||||
|
||||
for (const [key, value] of Object.entries(agents)) {
|
||||
const newKey = AGENT_NAME_MAP[key.toLowerCase()] ?? AGENT_NAME_MAP[key] ?? key
|
||||
if (newKey !== key) {
|
||||
changed = true
|
||||
}
|
||||
migrated[newKey] = value
|
||||
}
|
||||
|
||||
return { migrated, changed }
|
||||
}
|
||||
|
||||
export function migrateHookNames(hooks: string[]): { migrated: string[]; changed: boolean } {
|
||||
const migrated: string[] = []
|
||||
let changed = false
|
||||
|
||||
for (const hook of hooks) {
|
||||
const newHook = HOOK_NAME_MAP[hook] ?? hook
|
||||
if (newHook !== hook) {
|
||||
changed = true
|
||||
}
|
||||
migrated.push(newHook)
|
||||
}
|
||||
|
||||
return { migrated, changed }
|
||||
}
|
||||
|
||||
export function migrateConfigFile(configPath: string, rawConfig: Record<string, unknown>): boolean {
|
||||
let needsWrite = false
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
const { migrated, changed } = migrateAgentNames(rawConfig.agents as Record<string, unknown>)
|
||||
if (changed) {
|
||||
rawConfig.agents = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
if (rawConfig.omo_agent) {
|
||||
rawConfig.sisyphus_agent = rawConfig.omo_agent
|
||||
delete rawConfig.omo_agent
|
||||
needsWrite = true
|
||||
}
|
||||
|
||||
if (rawConfig.disabled_hooks && Array.isArray(rawConfig.disabled_hooks)) {
|
||||
const { migrated, changed } = migrateHookNames(rawConfig.disabled_hooks as string[])
|
||||
if (changed) {
|
||||
rawConfig.disabled_hooks = migrated
|
||||
needsWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsWrite) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8")
|
||||
log(`Migrated config file: ${configPath}`)
|
||||
} catch (err) {
|
||||
log(`Failed to write migrated config to ${configPath}:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return needsWrite
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { getClaudeConfigDir } from "../../shared"
|
||||
export const OPENCODE_STORAGE = getOpenCodeStorageDir()
|
||||
export const MESSAGE_STORAGE = join(OPENCODE_STORAGE, "message")
|
||||
export const PART_STORAGE = join(OPENCODE_STORAGE, "part")
|
||||
export const SESSION_STORAGE = join(OPENCODE_STORAGE, "session")
|
||||
export const TODO_DIR = join(getClaudeConfigDir(), "todos")
|
||||
export const TRANSCRIPT_DIR = join(getClaudeConfigDir(), "transcripts")
|
||||
export const SESSION_LIST_DESCRIPTION = `List all OpenCode sessions with optional filtering.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os"
|
||||
const TEST_DIR = join(tmpdir(), "omo-test-session-manager")
|
||||
const TEST_MESSAGE_STORAGE = join(TEST_DIR, "message")
|
||||
const TEST_PART_STORAGE = join(TEST_DIR, "part")
|
||||
const TEST_SESSION_STORAGE = join(TEST_DIR, "session")
|
||||
const TEST_TODO_DIR = join(TEST_DIR, "todos")
|
||||
const TEST_TRANSCRIPT_DIR = join(TEST_DIR, "transcripts")
|
||||
|
||||
@@ -13,6 +14,7 @@ mock.module("./constants", () => ({
|
||||
OPENCODE_STORAGE: TEST_DIR,
|
||||
MESSAGE_STORAGE: TEST_MESSAGE_STORAGE,
|
||||
PART_STORAGE: TEST_PART_STORAGE,
|
||||
SESSION_STORAGE: TEST_SESSION_STORAGE,
|
||||
TODO_DIR: TEST_TODO_DIR,
|
||||
TRANSCRIPT_DIR: TEST_TRANSCRIPT_DIR,
|
||||
SESSION_LIST_DESCRIPTION: "test",
|
||||
@@ -26,6 +28,8 @@ mock.module("./constants", () => ({
|
||||
const { getAllSessions, getMessageDir, sessionExists, readSessionMessages, readSessionTodos, getSessionInfo } =
|
||||
await import("./storage")
|
||||
|
||||
const storage = await import("./storage")
|
||||
|
||||
describe("session-manager storage", () => {
|
||||
beforeEach(() => {
|
||||
if (existsSync(TEST_DIR)) {
|
||||
@@ -34,6 +38,7 @@ describe("session-manager storage", () => {
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
mkdirSync(TEST_MESSAGE_STORAGE, { recursive: true })
|
||||
mkdirSync(TEST_PART_STORAGE, { recursive: true })
|
||||
mkdirSync(TEST_SESSION_STORAGE, { recursive: true })
|
||||
mkdirSync(TEST_TODO_DIR, { recursive: true })
|
||||
mkdirSync(TEST_TRANSCRIPT_DIR, { recursive: true })
|
||||
})
|
||||
@@ -174,3 +179,137 @@ describe("session-manager storage", () => {
|
||||
expect(info?.agents_used).toContain("oracle")
|
||||
})
|
||||
})
|
||||
|
||||
describe("session-manager storage - getMainSessions", () => {
|
||||
beforeEach(() => {
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||
}
|
||||
mkdirSync(TEST_DIR, { recursive: true })
|
||||
mkdirSync(TEST_MESSAGE_STORAGE, { recursive: true })
|
||||
mkdirSync(TEST_PART_STORAGE, { recursive: true })
|
||||
mkdirSync(TEST_SESSION_STORAGE, { recursive: true })
|
||||
mkdirSync(TEST_TODO_DIR, { recursive: true })
|
||||
mkdirSync(TEST_TRANSCRIPT_DIR, { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function createSessionMetadata(
|
||||
projectID: string,
|
||||
sessionID: string,
|
||||
opts: { parentID?: string; directory: string; updated: number }
|
||||
) {
|
||||
const projectDir = join(TEST_SESSION_STORAGE, projectID)
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFileSync(
|
||||
join(projectDir, `${sessionID}.json`),
|
||||
JSON.stringify({
|
||||
id: sessionID,
|
||||
projectID,
|
||||
directory: opts.directory,
|
||||
parentID: opts.parentID,
|
||||
time: { created: opts.updated - 1000, updated: opts.updated },
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function createMessageForSession(sessionID: string, msgID: string, created: number) {
|
||||
const sessionPath = join(TEST_MESSAGE_STORAGE, sessionID)
|
||||
mkdirSync(sessionPath, { recursive: true })
|
||||
writeFileSync(
|
||||
join(sessionPath, `${msgID}.json`),
|
||||
JSON.stringify({ id: msgID, role: "user", time: { created } })
|
||||
)
|
||||
}
|
||||
|
||||
test("getMainSessions returns only sessions without parentID", async () => {
|
||||
// #given
|
||||
const projectID = "proj_abc123"
|
||||
const now = Date.now()
|
||||
|
||||
createSessionMetadata(projectID, "ses_main1", { directory: "/test/path", updated: now })
|
||||
createSessionMetadata(projectID, "ses_main2", { directory: "/test/path", updated: now - 1000 })
|
||||
createSessionMetadata(projectID, "ses_child1", { directory: "/test/path", updated: now, parentID: "ses_main1" })
|
||||
|
||||
createMessageForSession("ses_main1", "msg_001", now)
|
||||
createMessageForSession("ses_main2", "msg_001", now - 1000)
|
||||
createMessageForSession("ses_child1", "msg_001", now)
|
||||
|
||||
// #when
|
||||
const sessions = await storage.getMainSessions({ directory: "/test/path" })
|
||||
|
||||
// #then
|
||||
expect(sessions.length).toBe(2)
|
||||
expect(sessions.map((s) => s.id)).not.toContain("ses_child1")
|
||||
})
|
||||
|
||||
test("getMainSessions sorts by time.updated descending (most recent first)", async () => {
|
||||
// #given
|
||||
const projectID = "proj_abc123"
|
||||
const now = Date.now()
|
||||
|
||||
createSessionMetadata(projectID, "ses_old", { directory: "/test/path", updated: now - 5000 })
|
||||
createSessionMetadata(projectID, "ses_mid", { directory: "/test/path", updated: now - 2000 })
|
||||
createSessionMetadata(projectID, "ses_new", { directory: "/test/path", updated: now })
|
||||
|
||||
createMessageForSession("ses_old", "msg_001", now - 5000)
|
||||
createMessageForSession("ses_mid", "msg_001", now - 2000)
|
||||
createMessageForSession("ses_new", "msg_001", now)
|
||||
|
||||
// #when
|
||||
const sessions = await storage.getMainSessions({ directory: "/test/path" })
|
||||
|
||||
// #then
|
||||
expect(sessions.length).toBe(3)
|
||||
expect(sessions[0].id).toBe("ses_new")
|
||||
expect(sessions[1].id).toBe("ses_mid")
|
||||
expect(sessions[2].id).toBe("ses_old")
|
||||
})
|
||||
|
||||
test("getMainSessions filters by directory (project path)", async () => {
|
||||
// #given
|
||||
const projectA = "proj_aaa"
|
||||
const projectB = "proj_bbb"
|
||||
const now = Date.now()
|
||||
|
||||
createSessionMetadata(projectA, "ses_projA", { directory: "/path/to/projectA", updated: now })
|
||||
createSessionMetadata(projectB, "ses_projB", { directory: "/path/to/projectB", updated: now })
|
||||
|
||||
createMessageForSession("ses_projA", "msg_001", now)
|
||||
createMessageForSession("ses_projB", "msg_001", now)
|
||||
|
||||
// #when
|
||||
const sessionsA = await storage.getMainSessions({ directory: "/path/to/projectA" })
|
||||
const sessionsB = await storage.getMainSessions({ directory: "/path/to/projectB" })
|
||||
|
||||
// #then
|
||||
expect(sessionsA.length).toBe(1)
|
||||
expect(sessionsA[0].id).toBe("ses_projA")
|
||||
expect(sessionsB.length).toBe(1)
|
||||
expect(sessionsB[0].id).toBe("ses_projB")
|
||||
})
|
||||
|
||||
test("getMainSessions returns all main sessions when directory is not specified", async () => {
|
||||
// #given
|
||||
const projectA = "proj_aaa"
|
||||
const projectB = "proj_bbb"
|
||||
const now = Date.now()
|
||||
|
||||
createSessionMetadata(projectA, "ses_projA", { directory: "/path/to/projectA", updated: now })
|
||||
createSessionMetadata(projectB, "ses_projB", { directory: "/path/to/projectB", updated: now - 1000 })
|
||||
|
||||
createMessageForSession("ses_projA", "msg_001", now)
|
||||
createMessageForSession("ses_projB", "msg_001", now - 1000)
|
||||
|
||||
// #when
|
||||
const sessions = await storage.getMainSessions({})
|
||||
|
||||
// #then
|
||||
expect(sessions.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
import { existsSync, readdirSync } from "node:fs"
|
||||
import { readdir, readFile } from "node:fs/promises"
|
||||
import { join } from "node:path"
|
||||
import { MESSAGE_STORAGE, PART_STORAGE, TODO_DIR, TRANSCRIPT_DIR } from "./constants"
|
||||
import type { SessionMessage, SessionInfo, TodoItem } from "./types"
|
||||
import { MESSAGE_STORAGE, PART_STORAGE, SESSION_STORAGE, TODO_DIR, TRANSCRIPT_DIR } from "./constants"
|
||||
import type { SessionMessage, SessionInfo, TodoItem, SessionMetadata } from "./types"
|
||||
|
||||
export interface GetMainSessionsOptions {
|
||||
directory?: string
|
||||
}
|
||||
|
||||
export async function getMainSessions(options: GetMainSessionsOptions): Promise<SessionMetadata[]> {
|
||||
if (!existsSync(SESSION_STORAGE)) return []
|
||||
|
||||
const sessions: SessionMetadata[] = []
|
||||
|
||||
try {
|
||||
const projectDirs = await readdir(SESSION_STORAGE, { withFileTypes: true })
|
||||
for (const projectDir of projectDirs) {
|
||||
if (!projectDir.isDirectory()) continue
|
||||
|
||||
const projectPath = join(SESSION_STORAGE, projectDir.name)
|
||||
const sessionFiles = await readdir(projectPath)
|
||||
|
||||
for (const file of sessionFiles) {
|
||||
if (!file.endsWith(".json")) continue
|
||||
|
||||
try {
|
||||
const content = await readFile(join(projectPath, file), "utf-8")
|
||||
const meta = JSON.parse(content) as SessionMetadata
|
||||
|
||||
if (meta.parentID) continue
|
||||
|
||||
if (options.directory && meta.directory !== options.directory) continue
|
||||
|
||||
sessions.push(meta)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
return sessions.sort((a, b) => b.time.updated - a.time.updated)
|
||||
}
|
||||
|
||||
export async function getAllSessions(): Promise<string[]> {
|
||||
if (!existsSync(MESSAGE_STORAGE)) return []
|
||||
|
||||
@@ -31,6 +31,27 @@ describe("session-manager tools", () => {
|
||||
expect(typeof result).toBe("string")
|
||||
})
|
||||
|
||||
test("session_list filters by project_path", async () => {
|
||||
// #given
|
||||
const projectPath = "/Users/yeongyu/local-workspaces/oh-my-opencode"
|
||||
|
||||
// #when
|
||||
const result = await session_list.execute({ project_path: projectPath }, mockContext)
|
||||
|
||||
// #then
|
||||
expect(typeof result).toBe("string")
|
||||
})
|
||||
|
||||
test("session_list uses process.cwd() as default project_path", async () => {
|
||||
// #given - no project_path provided
|
||||
|
||||
// #when
|
||||
const result = await session_list.execute({}, mockContext)
|
||||
|
||||
// #then - should not throw and return string (uses process.cwd() internally)
|
||||
expect(typeof result).toBe("string")
|
||||
})
|
||||
|
||||
test("session_read handles non-existent session", async () => {
|
||||
const result = await session_read.execute({ session_id: "ses_nonexistent" }, mockContext)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SESSION_SEARCH_DESCRIPTION,
|
||||
SESSION_INFO_DESCRIPTION,
|
||||
} from "./constants"
|
||||
import { getAllSessions, getSessionInfo, readSessionMessages, readSessionTodos, sessionExists } from "./storage"
|
||||
import { getAllSessions, getMainSessions, getSessionInfo, readSessionMessages, readSessionTodos, sessionExists } from "./storage"
|
||||
import {
|
||||
filterSessionsByDate,
|
||||
formatSessionInfo,
|
||||
@@ -32,20 +32,23 @@ export const session_list: ToolDefinition = tool({
|
||||
limit: tool.schema.number().optional().describe("Maximum number of sessions to return"),
|
||||
from_date: tool.schema.string().optional().describe("Filter sessions from this date (ISO 8601 format)"),
|
||||
to_date: tool.schema.string().optional().describe("Filter sessions until this date (ISO 8601 format)"),
|
||||
project_path: tool.schema.string().optional().describe("Filter sessions by project path (default: current working directory)"),
|
||||
},
|
||||
execute: async (args: SessionListArgs, _context) => {
|
||||
try {
|
||||
let sessions = await getAllSessions()
|
||||
const directory = args.project_path ?? process.cwd()
|
||||
let sessions = await getMainSessions({ directory })
|
||||
let sessionIDs = sessions.map((s) => s.id)
|
||||
|
||||
if (args.from_date || args.to_date) {
|
||||
sessions = await filterSessionsByDate(sessions, args.from_date, args.to_date)
|
||||
sessionIDs = await filterSessionsByDate(sessionIDs, args.from_date, args.to_date)
|
||||
}
|
||||
|
||||
if (args.limit && args.limit > 0) {
|
||||
sessions = sessions.slice(0, args.limit)
|
||||
sessionIDs = sessionIDs.slice(0, args.limit)
|
||||
}
|
||||
|
||||
return await formatSessionList(sessions)
|
||||
return await formatSessionList(sessionIDs)
|
||||
} catch (e) {
|
||||
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
}
|
||||
|
||||
@@ -49,11 +49,30 @@ export interface SearchResult {
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface SessionMetadata {
|
||||
id: string
|
||||
version?: string
|
||||
projectID: string
|
||||
directory: string
|
||||
title?: string
|
||||
parentID?: string
|
||||
time: {
|
||||
created: number
|
||||
updated: number
|
||||
}
|
||||
summary?: {
|
||||
additions: number
|
||||
deletions: number
|
||||
files: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface SessionListArgs {
|
||||
limit?: number
|
||||
offset?: number
|
||||
from_date?: string
|
||||
to_date?: string
|
||||
project_path?: string
|
||||
}
|
||||
|
||||
export interface SessionReadArgs {
|
||||
|
||||
Reference in New Issue
Block a user