opencode-ultra 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -15
- package/dist/index.js +600 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
# opencode-ultra
|
|
2
2
|
|
|
3
3
|
[oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) をベースに、micode / opencode-skillful の良い部分を取り込んだ OpenCode 1.2.x プラグイン。
|
|
4
|
-
マルチエージェントオーケストレーション・キーワード駆動モード切替・ルール注入・セッション継続・AST
|
|
4
|
+
マルチエージェントオーケストレーション・キーワード駆動モード切替・ルール注入・セッション継続・AST検索・自己改善 (evolve) を軽量な単一プラグインで実現する。
|
|
5
5
|
|
|
6
6
|
## 機能一覧
|
|
7
7
|
|
|
8
|
-
### ツール (
|
|
8
|
+
### ツール (8)
|
|
9
9
|
|
|
10
10
|
| ツール | 説明 |
|
|
11
11
|
|--------|------|
|
|
12
|
-
| `spawn_agent` | 並列エージェント実行 (ConcurrencyPool
|
|
13
|
-
| `ralph_loop` | 自律ループ実行 (`<promise>DONE</promise>`
|
|
12
|
+
| `spawn_agent` | 並列エージェント実行 (ConcurrencyPool + spawn limit + timeout + sanitizer) |
|
|
13
|
+
| `ralph_loop` | 自律ループ実行 (`<promise>DONE</promise>` で完了検知, per-iteration timeout) |
|
|
14
14
|
| `cancel_ralph` | 実行中の Ralph Loop をキャンセル |
|
|
15
15
|
| `batch_read` | 複数ファイル並列読み込み (最大20) |
|
|
16
16
|
| `ledger_save` | Continuity Ledger の保存 (.opencode/ledgers/) |
|
|
17
17
|
| `ledger_load` | Continuity Ledger の読み込み (名前指定 or 最新) |
|
|
18
18
|
| `ast_search` | AST-aware コード検索 (ast-grep/sg バイナリ使用、未インストール時は自動スキップ) |
|
|
19
|
+
| `evolve_apply` | プラグイン推薦の信頼度スコア評価 + OpenCode 設定への自動適用 (dry-run/backup 対応) |
|
|
19
20
|
|
|
20
21
|
### フック (9)
|
|
21
22
|
|
|
22
23
|
| フック | hook ポイント | 説明 |
|
|
23
24
|
|--------|-------------|------|
|
|
24
|
-
| `keyword-detector` | chat.message | ultrawork/search/analyze/think キーワード検知 |
|
|
25
|
+
| `keyword-detector` | chat.message | ultrawork/search/analyze/think/evolve キーワード検知 |
|
|
25
26
|
| `rules-injector` | system.transform | .opencode/rules.md を system prompt に注入 |
|
|
26
27
|
| `context-injector` | system.transform | ARCHITECTURE.md / CODE_STYLE.md を自動注入 |
|
|
27
28
|
| `fragment-injector` | system.transform | エージェント毎のカスタムプロンプト断片注入 |
|
|
@@ -31,6 +32,45 @@
|
|
|
31
32
|
| `todo-enforcer` | event (session.idle) | 未完了 TODO を検知して強制継続 |
|
|
32
33
|
| `session-compaction` | experimental.session.compacting | セッション圧縮時に structured summary 生成 |
|
|
33
34
|
|
|
35
|
+
### Safety
|
|
36
|
+
|
|
37
|
+
| 機能 | 説明 |
|
|
38
|
+
|------|------|
|
|
39
|
+
| **Prompt Injection Sanitizer** | エージェント出力から注入パターンを検知・無害化 (17パターン, 6カテゴリ) |
|
|
40
|
+
| **Spawn Limit** | 同時実行セッション上限 (default: 15) — エージェント爆発防止 |
|
|
41
|
+
| **Agent Timeout** | per-agent タイムアウト (default: 180s) — ハング防止 |
|
|
42
|
+
| **Trust Score** | npm パッケージの信頼度スコア (0-100) — evolve 推薦の品質ゲート |
|
|
43
|
+
|
|
44
|
+
### Evolve (自己改善)
|
|
45
|
+
|
|
46
|
+
| キーワード | 効果 |
|
|
47
|
+
|-----------|------|
|
|
48
|
+
| `evolve` / `self-improve` / `自己改善` / `進化` | evolve モード起動 |
|
|
49
|
+
|
|
50
|
+
evolve モードでは:
|
|
51
|
+
1. **scout** エージェントが npm/GitHub を検索し、プラグインのメタデータを収集
|
|
52
|
+
2. **explore** エージェントが opencode-ultra 自身の機能をカタログ化
|
|
53
|
+
3. 発見されたプラグインに **Trust Score** (0-100) を自動計算
|
|
54
|
+
4. ギャップ分析と改善提案を生成
|
|
55
|
+
5. ユーザー承認後、`evolve_apply` で設定に自動適用 (バックアップ付き)
|
|
56
|
+
|
|
57
|
+
Trust Score 評価基準:
|
|
58
|
+
|
|
59
|
+
| カテゴリ | 配点 | チェック項目 |
|
|
60
|
+
|----------|------|-------------|
|
|
61
|
+
| Recency | 25 | 最終 publish からの日数 |
|
|
62
|
+
| Popularity | 25 | 週間 DL 数 + GitHub stars |
|
|
63
|
+
| Quality | 20 | ライセンス, README, メンテナ数, description |
|
|
64
|
+
| Repository | 15 | GitHub/GitLab リポジトリの存在 |
|
|
65
|
+
| Safety | 15 | typosquat 検知, 依存数, 既知スコープ |
|
|
66
|
+
|
|
67
|
+
| スコア | レベル | 自動インストール |
|
|
68
|
+
|--------|--------|----------------|
|
|
69
|
+
| 90-100 | HIGH | 可 |
|
|
70
|
+
| 70-89 | MEDIUM | レビュー推奨 |
|
|
71
|
+
| 40-69 | LOW | 要注意 |
|
|
72
|
+
| 0-39 | RISKY | ブロック |
|
|
73
|
+
|
|
34
74
|
### その他
|
|
35
75
|
|
|
36
76
|
| 機能 | 説明 |
|
|
@@ -46,7 +86,7 @@
|
|
|
46
86
|
```
|
|
47
87
|
User
|
|
48
88
|
│
|
|
49
|
-
│ "ultrawork" / "ulw" / "think hard" キーワード
|
|
89
|
+
│ "ultrawork" / "ulw" / "evolve" / "think hard" キーワード
|
|
50
90
|
│
|
|
51
91
|
▼
|
|
52
92
|
┌──────────────────────────────────────────────┐
|
|
@@ -54,7 +94,7 @@ User
|
|
|
54
94
|
│ │
|
|
55
95
|
│ chat.message hook │
|
|
56
96
|
│ ├─ keyword-detector │
|
|
57
|
-
│ │ └─ ultrawork
|
|
97
|
+
│ │ └─ ultrawork/search/analyze/think/evolve │
|
|
58
98
|
│ └─ ultrawork → variant: "max" │
|
|
59
99
|
│ │
|
|
60
100
|
│ system.transform hook │
|
|
@@ -80,11 +120,20 @@ User
|
|
|
80
120
|
│ └─ demote built-in agents to subagent │
|
|
81
121
|
│ │
|
|
82
122
|
│ tools │
|
|
83
|
-
│ ├─ spawn_agent (並列実行 +
|
|
123
|
+
│ ├─ spawn_agent (並列実行 + 安全制御) │
|
|
124
|
+
│ │ ├─ ConcurrencyPool │
|
|
125
|
+
│ │ ├─ Spawn limit (max 15) │
|
|
126
|
+
│ │ ├─ Agent timeout (180s) │
|
|
127
|
+
│ │ └─ Prompt injection sanitizer │
|
|
84
128
|
│ ├─ ralph_loop / cancel_ralph │
|
|
85
129
|
│ ├─ batch_read (複数ファイル並列読み込み) │
|
|
86
130
|
│ ├─ ledger_save / ledger_load (文脈継続) │
|
|
87
|
-
│
|
|
131
|
+
│ ├─ ast_search (構文木検索, optional) │
|
|
132
|
+
│ └─ evolve_apply (信頼度スコア + 設定適用) │
|
|
133
|
+
│ │
|
|
134
|
+
│ safety │
|
|
135
|
+
│ ├─ sanitizer (17 injection patterns) │
|
|
136
|
+
│ └─ trust-score (5-factor, 0-100) │
|
|
88
137
|
│ │
|
|
89
138
|
│ MCP registration │
|
|
90
139
|
│ └─ context7 (自動登録) │
|
|
@@ -119,6 +168,7 @@ Sisyphus (オーケストレーター) が直接コードを読み、実装は
|
|
|
119
168
|
| **momus** | コードレビュー・品質チェック | anthropic/claude-sonnet-4-5 | subagent |
|
|
120
169
|
| **atlas** | タスク管理・進捗追跡 | anthropic/claude-sonnet-4-5 | subagent |
|
|
121
170
|
| **multimodal-looker** | 画像・スクリーンショット解析 | anthropic/claude-sonnet-4-5 | subagent |
|
|
171
|
+
| **scout** | プラグインエコシステム調査・信頼度メタデータ収集 | anthropic/claude-sonnet-4-5 | subagent |
|
|
122
172
|
|
|
123
173
|
全てのモデルは `oh-my-opencode.json` でオーバーライド可能。
|
|
124
174
|
|
|
@@ -190,6 +240,7 @@ ledger_load({})
|
|
|
190
240
|
| `search` / `find` / `探して` 等 | search | 網羅的検索 |
|
|
191
241
|
| `analyze` / `調査` / `debug` 等 | analyze | コンテキスト収集 |
|
|
192
242
|
| `think hard` / `じっくり` / `熟考` 等 | think | Extended thinking 有効化 |
|
|
243
|
+
| `evolve` / `self-improve` / `自己改善` 等 | evolve | 自己改善サイクル起動 |
|
|
193
244
|
|
|
194
245
|
日本語・中国語キーワードにも対応。
|
|
195
246
|
|
|
@@ -231,8 +282,8 @@ ledger_load({})
|
|
|
231
282
|
"disabled_hooks": [], // keyword-detector, rules-injector, context-injector,
|
|
232
283
|
// fragment-injector, prompt-renderer, comment-checker,
|
|
233
284
|
// token-truncation, todo-enforcer, session-compaction
|
|
234
|
-
"disabled_tools": [], // spawn_agent, ralph_loop, cancel_ralph,
|
|
235
|
-
//
|
|
285
|
+
"disabled_tools": [], // spawn_agent, ralph_loop, cancel_ralph, batch_read,
|
|
286
|
+
// ledger_save, ledger_load, ast_search, evolve_apply
|
|
236
287
|
"disabled_mcps": [], // context7
|
|
237
288
|
|
|
238
289
|
// Built-in Agent Demotion (default: true)
|
|
@@ -245,6 +296,12 @@ ledger_load({})
|
|
|
245
296
|
"modelConcurrency": { "openai/gpt-5.2": 2 }
|
|
246
297
|
},
|
|
247
298
|
|
|
299
|
+
// Safety
|
|
300
|
+
"safety": {
|
|
301
|
+
"maxTotalSpawned": 15, // 同時実行セッション上限
|
|
302
|
+
"agentTimeoutMs": 180000 // per-agent タイムアウト (ms)
|
|
303
|
+
},
|
|
304
|
+
|
|
248
305
|
// Token Truncation
|
|
249
306
|
"token_truncation": { "maxChars": 30000 },
|
|
250
307
|
|
|
@@ -270,7 +327,7 @@ opencode-ultra/
|
|
|
270
327
|
│ │ ├── types.ts # AgentDef 型定義
|
|
271
328
|
│ │ └── index.ts # ビルトインエージェント + 動的プロンプト生成
|
|
272
329
|
│ ├── hooks/
|
|
273
|
-
│ │ ├── keyword-detector.ts # ultrawork/search/analyze/think 検知
|
|
330
|
+
│ │ ├── keyword-detector.ts # ultrawork/search/analyze/think/evolve 検知
|
|
274
331
|
│ │ ├── rules-injector.ts # rules.md + ARCHITECTURE.md + CODE_STYLE.md 注入
|
|
275
332
|
│ │ ├── fragment-injector.ts # エージェント毎の断片注入
|
|
276
333
|
│ │ ├── prompt-renderer.ts # モデル別フォーマット変換
|
|
@@ -279,16 +336,18 @@ opencode-ultra/
|
|
|
279
336
|
│ │ ├── todo-enforcer.ts # 未完了 TODO 強制継続
|
|
280
337
|
│ │ └── session-compaction.ts # セッション圧縮サマリ生成
|
|
281
338
|
│ ├── tools/
|
|
282
|
-
│ │ ├── spawn-agent.ts # 並列エージェント実行
|
|
339
|
+
│ │ ├── spawn-agent.ts # 並列エージェント実行 + 安全制御
|
|
283
340
|
│ │ ├── ralph-loop.ts # 自律ループ実行
|
|
284
341
|
│ │ ├── batch-read.ts # 複数ファイル並列読み込み
|
|
285
342
|
│ │ ├── continuity-ledger.ts # セッション間文脈継続
|
|
286
|
-
│ │
|
|
343
|
+
│ │ ├── ast-search.ts # AST-aware コード検索
|
|
344
|
+
│ │ └── evolve-apply.ts # 信頼度スコア評価 + 設定適用
|
|
287
345
|
│ ├── concurrency/ # Semaphore + ConcurrencyPool
|
|
288
346
|
│ ├── categories/ # ビルトインカテゴリ
|
|
347
|
+
│ ├── safety/ # sanitizer + trust-score
|
|
289
348
|
│ ├── mcp/ # MCP サーバー登録
|
|
290
349
|
│ └── shared/ # ユーティリティ
|
|
291
|
-
├── __test__/ # Bun テスト
|
|
350
|
+
├── __test__/ # Bun テスト
|
|
292
351
|
├── package.json
|
|
293
352
|
├── tsconfig.json
|
|
294
353
|
└── .gitignore
|
package/dist/index.js
CHANGED
|
@@ -13552,6 +13552,9 @@ import { join } from "path";
|
|
|
13552
13552
|
function getConfigDir() {
|
|
13553
13553
|
return process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
|
|
13554
13554
|
}
|
|
13555
|
+
function getCacheDir() {
|
|
13556
|
+
return process.env.XDG_CACHE_HOME ? join(process.env.XDG_CACHE_HOME, "opencode") : join(homedir(), ".cache", "opencode");
|
|
13557
|
+
}
|
|
13555
13558
|
// node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
13556
13559
|
function createScanner(text, ignoreTrivia = false) {
|
|
13557
13560
|
const len = text.length;
|
|
@@ -14465,7 +14468,11 @@ var PluginConfigSchema = exports_external.object({
|
|
|
14465
14468
|
todo_enforcer: exports_external.object({
|
|
14466
14469
|
maxEnforcements: exports_external.number().min(0).optional()
|
|
14467
14470
|
}).optional(),
|
|
14468
|
-
mcp_api_keys: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
14471
|
+
mcp_api_keys: exports_external.record(exports_external.string(), exports_external.string()).optional(),
|
|
14472
|
+
safety: exports_external.object({
|
|
14473
|
+
maxTotalSpawned: exports_external.number().min(1).optional(),
|
|
14474
|
+
agentTimeoutMs: exports_external.number().min(1000).optional()
|
|
14475
|
+
}).optional()
|
|
14469
14476
|
}).passthrough();
|
|
14470
14477
|
function parsePluginConfig(raw) {
|
|
14471
14478
|
const result = PluginConfigSchema.safeParse(raw);
|
|
@@ -14636,12 +14643,12 @@ var BUILTIN_AGENTS = {
|
|
|
14636
14643
|
},
|
|
14637
14644
|
scout: {
|
|
14638
14645
|
model: "anthropic/claude-sonnet-4-5",
|
|
14639
|
-
description: "Plugin ecosystem researcher \u2014 finds, analyzes, and compares OpenCode plugins
|
|
14646
|
+
description: "Plugin ecosystem researcher \u2014 finds, analyzes, and compares OpenCode plugins with trust scoring",
|
|
14640
14647
|
prompt: `You are Scout, an OpenCode plugin ecosystem researcher.
|
|
14641
14648
|
|
|
14642
14649
|
## YOUR MISSION
|
|
14643
14650
|
Search the web (npm, GitHub, OpenCode community) for OpenCode plugins and extensions.
|
|
14644
|
-
|
|
14651
|
+
Collect STRUCTURED METADATA for trust scoring. Compare with opencode-ultra.
|
|
14645
14652
|
|
|
14646
14653
|
## SEARCH STRATEGY
|
|
14647
14654
|
1. Search npm for "opencode-plugin", "opencode-ai", "@opencode" packages
|
|
@@ -14649,17 +14656,48 @@ Analyze their features, architecture, and quality. Compare with opencode-ultra.
|
|
|
14649
14656
|
3. Look at package.json dependencies on @opencode-ai/plugin or @opencode-ai/sdk
|
|
14650
14657
|
4. Read README files and source code of discovered plugins
|
|
14651
14658
|
|
|
14659
|
+
## CRITICAL: METADATA COLLECTION
|
|
14660
|
+
For EACH plugin found, you MUST collect these fields (used for trust scoring):
|
|
14661
|
+
- **name**: exact npm package name
|
|
14662
|
+
- **version**: latest version string
|
|
14663
|
+
- **lastPublished**: ISO date of last npm publish (e.g. "2026-01-15")
|
|
14664
|
+
- **weeklyDownloads**: weekly npm download count (number)
|
|
14665
|
+
- **stars**: GitHub stars (number, 0 if unknown)
|
|
14666
|
+
- **repository**: GitHub/GitLab repo URL
|
|
14667
|
+
- **license**: SPDX license identifier (e.g. "MIT", "ISC")
|
|
14668
|
+
- **hasReadme**: true/false
|
|
14669
|
+
- **maintainerCount**: number of npm maintainers
|
|
14670
|
+
- **dependencyCount**: number of production dependencies
|
|
14671
|
+
- **description**: one-line description
|
|
14672
|
+
- **features**: bullet list of capabilities
|
|
14673
|
+
- **uniqueIdeas**: features that opencode-ultra does NOT have
|
|
14674
|
+
|
|
14652
14675
|
## OUTPUT FORMAT
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
|
|
14656
|
-
|
|
14657
|
-
-
|
|
14658
|
-
|
|
14659
|
-
|
|
14676
|
+
Return a JSON array of plugin objects with the fields above. Example:
|
|
14677
|
+
\`\`\`json
|
|
14678
|
+
[
|
|
14679
|
+
{
|
|
14680
|
+
"name": "opencode-supermemory",
|
|
14681
|
+
"version": "1.2.0",
|
|
14682
|
+
"lastPublished": "2026-02-01",
|
|
14683
|
+
"weeklyDownloads": 500,
|
|
14684
|
+
"stars": 45,
|
|
14685
|
+
"repository": "https://github.com/supermemoryai/opencode-supermemory",
|
|
14686
|
+
"license": "MIT",
|
|
14687
|
+
"hasReadme": true,
|
|
14688
|
+
"maintainerCount": 2,
|
|
14689
|
+
"dependencyCount": 5,
|
|
14690
|
+
"description": "Persistent memory across OpenCode sessions",
|
|
14691
|
+
"features": ["Session memory", "Cross-project recall"],
|
|
14692
|
+
"uniqueIdeas": ["Long-term memory that opencode-ultra lacks"]
|
|
14693
|
+
}
|
|
14694
|
+
]
|
|
14695
|
+
\`\`\`
|
|
14696
|
+
|
|
14697
|
+
Also include a text summary with gap analysis after the JSON block.
|
|
14660
14698
|
|
|
14661
14699
|
## COMPARISON
|
|
14662
|
-
After listing plugins, generate
|
|
14700
|
+
After listing plugins, generate:
|
|
14663
14701
|
- Features others have that opencode-ultra lacks
|
|
14664
14702
|
- Features opencode-ultra has that others lack (competitive advantages)
|
|
14665
14703
|
- Improvement priority list (high/medium/low impact)
|
|
@@ -14902,30 +14940,58 @@ var THINK_MESSAGE = `Extended thinking enabled. Take your time to reason thoroug
|
|
|
14902
14940
|
var EVOLVE_MESSAGE = `[evolve-mode] SELF-IMPROVEMENT CYCLE ACTIVATED.
|
|
14903
14941
|
|
|
14904
14942
|
## MISSION
|
|
14905
|
-
Search the OpenCode plugin ecosystem,
|
|
14943
|
+
Search the OpenCode plugin ecosystem, evaluate trust/quality, identify gaps, and optionally apply improvements.
|
|
14944
|
+
|
|
14945
|
+
## TOOLS AVAILABLE
|
|
14946
|
+
- **spawn_agent** \u2014 run scout + explore agents in parallel for data gathering
|
|
14947
|
+
- **evolve_apply** \u2014 apply plugin recommendations to OpenCode config (with trust scoring and backup)
|
|
14906
14948
|
|
|
14907
14949
|
## STEPS
|
|
14908
|
-
1. **SCOUT** \u2014 Spawn the **scout** agent to search npm/GitHub for OpenCode plugins
|
|
14909
|
-
2. **READ SELF** \u2014 Read opencode-ultra's own
|
|
14910
|
-
3. **
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14950
|
+
1. **SCOUT** \u2014 Spawn the **scout** agent to search npm/GitHub for OpenCode plugins. Scout MUST return package metadata (lastPublished, weeklyDownloads, stars, repository, license, maintainerCount, dependencyCount) for each plugin found.
|
|
14951
|
+
2. **READ SELF** \u2014 Read opencode-ultra's own capabilities via explore agent
|
|
14952
|
+
3. **SCORE** \u2014 For each discovered plugin, the evolve_apply tool computes a trust score (0-100):
|
|
14953
|
+
- 90-100: HIGH trust (safe to auto-install)
|
|
14954
|
+
- 70-89: MEDIUM trust (review recommended)
|
|
14955
|
+
- 40-69: LOW trust (caution)
|
|
14956
|
+
- 0-39: RISKY (blocked from install)
|
|
14957
|
+
4. **COMPARE** \u2014 Generate gap analysis with trust scores
|
|
14958
|
+
5. **PROPOSE** \u2014 Present scored recommendations to the user
|
|
14959
|
+
6. **APPLY** \u2014 If user approves, call evolve_apply to update config:
|
|
14960
|
+
|
|
14961
|
+
\`\`\`
|
|
14962
|
+
evolve_apply({
|
|
14963
|
+
plugins: [
|
|
14964
|
+
{
|
|
14965
|
+
name: "opencode-supermemory",
|
|
14966
|
+
version: "latest",
|
|
14967
|
+
reason: "Persistent memory across sessions",
|
|
14968
|
+
metadata: {name: "opencode-supermemory", lastPublished: "2026-02-01", weeklyDownloads: 500, stars: 45, repository: "https://github.com/...", license: "MIT", maintainerCount: 2}
|
|
14969
|
+
}
|
|
14970
|
+
],
|
|
14971
|
+
dryRun: true
|
|
14972
|
+
})
|
|
14973
|
+
\`\`\`
|
|
14974
|
+
|
|
14975
|
+
Use dryRun: true FIRST to preview, then dryRun: false after user approval.
|
|
14976
|
+
|
|
14977
|
+
7. **SAVE** \u2014 Save findings to ledger: ledger_save({name: "evolve-scan-YYYY-MM-DD", content: "..."})
|
|
14914
14978
|
|
|
14915
14979
|
## EXECUTION
|
|
14916
14980
|
\`\`\`
|
|
14917
14981
|
spawn_agent({
|
|
14918
14982
|
agents: [
|
|
14919
|
-
{agent: "scout", prompt: "Search npm and GitHub for OpenCode 1.2.x plugins.
|
|
14983
|
+
{agent: "scout", prompt: "Search npm and GitHub for OpenCode 1.2.x plugins. For EACH plugin found, collect: name, version, lastPublished date, weeklyDownloads, GitHub stars, repository URL, license, maintainerCount, dependencyCount. Return structured data.", description: "Plugin ecosystem scan + metadata"},
|
|
14920
14984
|
{agent: "explore", prompt: "Read opencode-ultra's README.md, src/index.ts, src/agents/index.ts to catalog current features", description: "Self-analysis"}
|
|
14921
14985
|
]
|
|
14922
14986
|
})
|
|
14923
14987
|
\`\`\`
|
|
14924
14988
|
|
|
14925
|
-
After gathering results
|
|
14926
|
-
|
|
14989
|
+
After gathering results:
|
|
14990
|
+
1. Call evolve_apply with dryRun: true to preview trust scores
|
|
14991
|
+
2. Present results to user with recommendation
|
|
14992
|
+
3. If approved, call evolve_apply with dryRun: false
|
|
14927
14993
|
|
|
14928
|
-
**
|
|
14994
|
+
**Trust scoring prevents bad plugins from entering your system. Always dry-run first.**`;
|
|
14929
14995
|
|
|
14930
14996
|
// src/hooks/rules-injector.ts
|
|
14931
14997
|
import * as fs2 from "fs";
|
|
@@ -27426,16 +27492,265 @@ function resolveCategory(categoryName, configCategories) {
|
|
|
27426
27492
|
return { ...builtin, ...override };
|
|
27427
27493
|
}
|
|
27428
27494
|
|
|
27495
|
+
// src/safety/sanitizer.ts
|
|
27496
|
+
var INJECTION_PATTERNS = [
|
|
27497
|
+
{ pattern: /ignore\s+(?:all\s+)?(?:previous\s+|prior\s+|above\s+)?instructions/i, label: "instruction-override" },
|
|
27498
|
+
{ pattern: /disregard\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|guidelines)/i, label: "instruction-override" },
|
|
27499
|
+
{ pattern: /forget\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|context|rules)/i, label: "instruction-override" },
|
|
27500
|
+
{ pattern: /override\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|system)/i, label: "instruction-override" },
|
|
27501
|
+
{ pattern: /you\s+are\s+now\s+(?:a\s+|an\s+|in\s+)?/i, label: "role-hijack" },
|
|
27502
|
+
{ pattern: /pretend\s+(?:you\s+are|to\s+be|you're)\s+/i, label: "role-hijack" },
|
|
27503
|
+
{ pattern: /act\s+as\s+(?:if\s+)?(?:you\s+(?:are|were)\s+)?/i, label: "role-hijack" },
|
|
27504
|
+
{ pattern: /role[\s-]?play\s+as/i, label: "role-hijack" },
|
|
27505
|
+
{ pattern: /switch\s+(?:to\s+)?(?:your\s+)?(?:new\s+)?(?:role|persona|identity)/i, label: "role-hijack" },
|
|
27506
|
+
{ pattern: /<\/?system(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27507
|
+
{ pattern: /<\/?instructions?(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27508
|
+
{ pattern: /<\/?prompt(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27509
|
+
{ pattern: /<\/?assistant(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27510
|
+
{ pattern: /<\/?human(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27511
|
+
{ pattern: /\[SYSTEM\](?:\s*:)?/i, label: "fake-tag" },
|
|
27512
|
+
{ pattern: /\[INST\]|\[\/INST\]/i, label: "fake-tag" },
|
|
27513
|
+
{ pattern: /new\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
|
|
27514
|
+
{ pattern: /updated?\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
|
|
27515
|
+
{ pattern: /revised?\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
|
|
27516
|
+
{ pattern: /(?:run|execute|eval)\s*\(\s*['"`].*(?:rm\s+-rf|curl\s+.*\|\s*(?:sh|bash)|wget\s+.*\|\s*(?:sh|bash))/i, label: "dangerous-command" },
|
|
27517
|
+
{ pattern: /\brm\s+-rf\s+[\/~]/i, label: "dangerous-command" },
|
|
27518
|
+
{ pattern: /(?:\u5168\u3066\u306E|\u3059\u3079\u3066\u306E)?(?:\u524D\u306E|\u4EE5\u524D\u306E)?\u6307\u793A\u3092(?:\u7121\u8996|\u5FD8\u308C|\u53D6\u308A\u6D88)/i, label: "instruction-override-ja" },
|
|
27519
|
+
{ pattern: /(?:\u65B0\u3057\u3044|\u66F4\u65B0\u3055\u308C\u305F)\u6307\u793A\s*[:\uFF1A]/i, label: "new-instructions-ja" }
|
|
27520
|
+
];
|
|
27521
|
+
var SUSPICIOUS_UNICODE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g;
|
|
27522
|
+
function sanitizeAgentOutput(text) {
|
|
27523
|
+
const warnings = [];
|
|
27524
|
+
const invisibleCount = (text.match(SUSPICIOUS_UNICODE) || []).length;
|
|
27525
|
+
if (invisibleCount > 0) {
|
|
27526
|
+
warnings.push(`Stripped ${invisibleCount} suspicious Unicode characters`);
|
|
27527
|
+
text = text.replace(SUSPICIOUS_UNICODE, "");
|
|
27528
|
+
}
|
|
27529
|
+
for (const { pattern, label } of INJECTION_PATTERNS) {
|
|
27530
|
+
if (pattern.test(text)) {
|
|
27531
|
+
warnings.push(`Injection pattern detected: ${label}`);
|
|
27532
|
+
text = text.replace(pattern, (match) => `[SANITIZED:${label}]`);
|
|
27533
|
+
}
|
|
27534
|
+
}
|
|
27535
|
+
const flagged = warnings.length > 0;
|
|
27536
|
+
if (flagged) {
|
|
27537
|
+
log("Sanitizer flagged content", { warnings });
|
|
27538
|
+
}
|
|
27539
|
+
return { text, flagged, warnings };
|
|
27540
|
+
}
|
|
27541
|
+
function sanitizeSpawnResult(result) {
|
|
27542
|
+
const { text, flagged, warnings } = sanitizeAgentOutput(result);
|
|
27543
|
+
if (!flagged)
|
|
27544
|
+
return text;
|
|
27545
|
+
const banner = `
|
|
27546
|
+
|
|
27547
|
+
> **[Safety] Potential prompt injection detected in agent output.**
|
|
27548
|
+
> Patterns: ${warnings.join(", ")}
|
|
27549
|
+
> The flagged content has been neutralized.
|
|
27550
|
+
`;
|
|
27551
|
+
return banner + `
|
|
27552
|
+
` + text;
|
|
27553
|
+
}
|
|
27554
|
+
// src/safety/trust-score.ts
|
|
27555
|
+
var KNOWN_SCOPES = [
|
|
27556
|
+
"@opencode-ai/",
|
|
27557
|
+
"opencode-",
|
|
27558
|
+
"@tarquinen/",
|
|
27559
|
+
"@azumag/",
|
|
27560
|
+
"@hoangp8/",
|
|
27561
|
+
"@gitlab/"
|
|
27562
|
+
];
|
|
27563
|
+
var TYPOSQUAT_PATTERNS = [
|
|
27564
|
+
/^0pencode/i,
|
|
27565
|
+
/^opencodd/i,
|
|
27566
|
+
/^openc0de/i,
|
|
27567
|
+
/^opencodee/i,
|
|
27568
|
+
/^opeencode/i,
|
|
27569
|
+
/^opemcode/i,
|
|
27570
|
+
/^opencode-.{1,3}$/i
|
|
27571
|
+
];
|
|
27572
|
+
function computeTrustScore(meta3) {
|
|
27573
|
+
const factors = [];
|
|
27574
|
+
factors.push(scoreRecency(meta3));
|
|
27575
|
+
factors.push(scorePopularity(meta3));
|
|
27576
|
+
factors.push(scoreQuality(meta3));
|
|
27577
|
+
factors.push(scoreRepository(meta3));
|
|
27578
|
+
factors.push(scoreSafety(meta3));
|
|
27579
|
+
const totalScore = Math.min(100, factors.reduce((sum, f) => sum + f.score, 0));
|
|
27580
|
+
const level = classifyLevel(totalScore);
|
|
27581
|
+
const summary = buildSummary(meta3.name, totalScore, level, factors);
|
|
27582
|
+
log("Trust score computed", { package: meta3.name, score: totalScore, level });
|
|
27583
|
+
return { score: totalScore, level, factors, summary };
|
|
27584
|
+
}
|
|
27585
|
+
function scoreRecency(meta3) {
|
|
27586
|
+
const maxScore = 25;
|
|
27587
|
+
if (!meta3.lastPublished) {
|
|
27588
|
+
return { name: "recency", score: 0, maxScore, detail: "No publish date available" };
|
|
27589
|
+
}
|
|
27590
|
+
const daysAgo = daysSince(meta3.lastPublished);
|
|
27591
|
+
if (daysAgo < 30)
|
|
27592
|
+
return { name: "recency", score: 25, maxScore, detail: `Updated ${daysAgo}d ago` };
|
|
27593
|
+
if (daysAgo < 90)
|
|
27594
|
+
return { name: "recency", score: 20, maxScore, detail: `Updated ${daysAgo}d ago` };
|
|
27595
|
+
if (daysAgo < 180)
|
|
27596
|
+
return { name: "recency", score: 15, maxScore, detail: `Updated ${daysAgo}d ago` };
|
|
27597
|
+
if (daysAgo < 365)
|
|
27598
|
+
return { name: "recency", score: 10, maxScore, detail: `Updated ${daysAgo}d ago \u2014 getting stale` };
|
|
27599
|
+
return { name: "recency", score: 3, maxScore, detail: `Updated ${daysAgo}d ago \u2014 likely abandoned` };
|
|
27600
|
+
}
|
|
27601
|
+
function scorePopularity(meta3) {
|
|
27602
|
+
const maxScore = 25;
|
|
27603
|
+
let score = 0;
|
|
27604
|
+
const details = [];
|
|
27605
|
+
const dl = meta3.weeklyDownloads ?? 0;
|
|
27606
|
+
if (dl > 1e4) {
|
|
27607
|
+
score += 15;
|
|
27608
|
+
details.push(`${dl.toLocaleString()} weekly DL`);
|
|
27609
|
+
} else if (dl > 1000) {
|
|
27610
|
+
score += 12;
|
|
27611
|
+
details.push(`${dl.toLocaleString()} weekly DL`);
|
|
27612
|
+
} else if (dl > 100) {
|
|
27613
|
+
score += 8;
|
|
27614
|
+
details.push(`${dl} weekly DL`);
|
|
27615
|
+
} else if (dl > 10) {
|
|
27616
|
+
score += 4;
|
|
27617
|
+
details.push(`${dl} weekly DL \u2014 low`);
|
|
27618
|
+
} else {
|
|
27619
|
+
score += 0;
|
|
27620
|
+
details.push(`${dl} weekly DL \u2014 very low`);
|
|
27621
|
+
}
|
|
27622
|
+
const stars = meta3.stars ?? 0;
|
|
27623
|
+
if (stars > 100) {
|
|
27624
|
+
score += 10;
|
|
27625
|
+
details.push(`${stars} stars`);
|
|
27626
|
+
} else if (stars > 20) {
|
|
27627
|
+
score += 7;
|
|
27628
|
+
details.push(`${stars} stars`);
|
|
27629
|
+
} else if (stars > 5) {
|
|
27630
|
+
score += 4;
|
|
27631
|
+
details.push(`${stars} stars`);
|
|
27632
|
+
} else {
|
|
27633
|
+
score += 0;
|
|
27634
|
+
details.push(`${stars} stars`);
|
|
27635
|
+
}
|
|
27636
|
+
return { name: "popularity", score: Math.min(maxScore, score), maxScore, detail: details.join(", ") };
|
|
27637
|
+
}
|
|
27638
|
+
function scoreQuality(meta3) {
|
|
27639
|
+
const maxScore = 20;
|
|
27640
|
+
let score = 0;
|
|
27641
|
+
const details = [];
|
|
27642
|
+
if (meta3.license && meta3.license !== "NONE" && meta3.license !== "UNLICENSED") {
|
|
27643
|
+
score += 7;
|
|
27644
|
+
details.push(`License: ${meta3.license}`);
|
|
27645
|
+
} else {
|
|
27646
|
+
details.push("No license");
|
|
27647
|
+
}
|
|
27648
|
+
if (meta3.hasReadme !== false) {
|
|
27649
|
+
score += 5;
|
|
27650
|
+
details.push("Has README");
|
|
27651
|
+
} else {
|
|
27652
|
+
details.push("No README");
|
|
27653
|
+
}
|
|
27654
|
+
const maintainers = meta3.maintainerCount ?? 0;
|
|
27655
|
+
if (maintainers >= 2) {
|
|
27656
|
+
score += 5;
|
|
27657
|
+
details.push(`${maintainers} maintainers`);
|
|
27658
|
+
} else if (maintainers === 1) {
|
|
27659
|
+
score += 3;
|
|
27660
|
+
details.push("1 maintainer");
|
|
27661
|
+
} else {
|
|
27662
|
+
details.push("No maintainer info");
|
|
27663
|
+
}
|
|
27664
|
+
if (meta3.description && meta3.description.length > 20) {
|
|
27665
|
+
score += 3;
|
|
27666
|
+
} else {
|
|
27667
|
+
details.push("Weak description");
|
|
27668
|
+
}
|
|
27669
|
+
return { name: "quality", score: Math.min(maxScore, score), maxScore, detail: details.join(", ") };
|
|
27670
|
+
}
|
|
27671
|
+
function scoreRepository(meta3) {
|
|
27672
|
+
const maxScore = 15;
|
|
27673
|
+
if (!meta3.repository) {
|
|
27674
|
+
return { name: "repository", score: 0, maxScore, detail: "No repository link \u2014 suspicious" };
|
|
27675
|
+
}
|
|
27676
|
+
if (meta3.repository.includes("github.com")) {
|
|
27677
|
+
return { name: "repository", score: 15, maxScore, detail: `Repo: ${meta3.repository}` };
|
|
27678
|
+
}
|
|
27679
|
+
if (meta3.repository.includes("gitlab.com") || meta3.repository.includes("bitbucket.org")) {
|
|
27680
|
+
return { name: "repository", score: 12, maxScore, detail: `Repo: ${meta3.repository}` };
|
|
27681
|
+
}
|
|
27682
|
+
return { name: "repository", score: 5, maxScore, detail: `Repo: ${meta3.repository} (unknown host)` };
|
|
27683
|
+
}
|
|
27684
|
+
function scoreSafety(meta3) {
|
|
27685
|
+
const maxScore = 15;
|
|
27686
|
+
let score = 15;
|
|
27687
|
+
const details = [];
|
|
27688
|
+
if (isTyposquatSuspect(meta3.name)) {
|
|
27689
|
+
score -= 15;
|
|
27690
|
+
details.push("TYPOSQUAT SUSPECT");
|
|
27691
|
+
} else {
|
|
27692
|
+
details.push("Name OK");
|
|
27693
|
+
}
|
|
27694
|
+
if (KNOWN_SCOPES.some((s) => meta3.name.startsWith(s))) {
|
|
27695
|
+
score = Math.min(maxScore, score + 3);
|
|
27696
|
+
details.push("Known scope");
|
|
27697
|
+
}
|
|
27698
|
+
const deps = meta3.dependencyCount ?? 0;
|
|
27699
|
+
if (deps > 50) {
|
|
27700
|
+
score = Math.max(0, score - 5);
|
|
27701
|
+
details.push(`${deps} deps \u2014 heavy`);
|
|
27702
|
+
} else if (deps > 20) {
|
|
27703
|
+
score = Math.max(0, score - 2);
|
|
27704
|
+
details.push(`${deps} deps`);
|
|
27705
|
+
}
|
|
27706
|
+
return { name: "safety", score: Math.max(0, score), maxScore, detail: details.join(", ") };
|
|
27707
|
+
}
|
|
27708
|
+
function isTyposquatSuspect(name) {
|
|
27709
|
+
return TYPOSQUAT_PATTERNS.some((p) => p.test(name));
|
|
27710
|
+
}
|
|
27711
|
+
function classifyLevel(score) {
|
|
27712
|
+
if (score >= 90)
|
|
27713
|
+
return "high";
|
|
27714
|
+
if (score >= 70)
|
|
27715
|
+
return "medium";
|
|
27716
|
+
if (score >= 40)
|
|
27717
|
+
return "low";
|
|
27718
|
+
return "risky";
|
|
27719
|
+
}
|
|
27720
|
+
function daysSince(isoDate) {
|
|
27721
|
+
const then = new Date(isoDate).getTime();
|
|
27722
|
+
const now = Date.now();
|
|
27723
|
+
return Math.floor((now - then) / (1000 * 60 * 60 * 24));
|
|
27724
|
+
}
|
|
27725
|
+
function buildSummary(name, score, level, factors) {
|
|
27726
|
+
const icon = level === "high" ? "V" : level === "medium" ? "~" : level === "low" ? "!" : "X";
|
|
27727
|
+
const weakest = [...factors].sort((a, b) => a.score / a.maxScore - b.score / b.maxScore)[0];
|
|
27728
|
+
return `[${icon}] ${name}: ${score}/100 (${level}) \u2014 weakest: ${weakest.name} (${weakest.score}/${weakest.maxScore}: ${weakest.detail})`;
|
|
27729
|
+
}
|
|
27429
27730
|
// src/tools/spawn-agent.ts
|
|
27731
|
+
var DEFAULT_MAX_TOTAL_SPAWNED = 15;
|
|
27732
|
+
var DEFAULT_AGENT_TIMEOUT_MS = 180000;
|
|
27430
27733
|
function showToast(ctx, title, message, variant = "info") {
|
|
27431
27734
|
const client = ctx.client;
|
|
27432
27735
|
client.tui?.showToast?.({
|
|
27433
27736
|
body: { title, message, variant, duration: 2000 }
|
|
27434
27737
|
})?.catch?.(() => {});
|
|
27435
27738
|
}
|
|
27739
|
+
async function withTimeout(promise3, ms, label) {
|
|
27740
|
+
let timer;
|
|
27741
|
+
const timeout = new Promise((_, reject) => {
|
|
27742
|
+
timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
27743
|
+
});
|
|
27744
|
+
try {
|
|
27745
|
+
return await Promise.race([promise3, timeout]);
|
|
27746
|
+
} finally {
|
|
27747
|
+
clearTimeout(timer);
|
|
27748
|
+
}
|
|
27749
|
+
}
|
|
27436
27750
|
async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
27437
27751
|
const { agent, prompt, description } = task;
|
|
27438
27752
|
const t0 = Date.now();
|
|
27753
|
+
const timeoutMs = deps.agentTimeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
27439
27754
|
const elapsed = () => ((Date.now() - (progress?.startTime ?? t0)) / 1000).toFixed(0);
|
|
27440
27755
|
const updateTitle = (status) => {
|
|
27441
27756
|
toolCtx.metadata({ title: status });
|
|
@@ -27461,26 +27776,27 @@ async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
|
27461
27776
|
**Error**: Failed to create session`;
|
|
27462
27777
|
}
|
|
27463
27778
|
internalSessions.add(sessionID);
|
|
27464
|
-
await ctx.client.session.prompt({
|
|
27779
|
+
await withTimeout(ctx.client.session.prompt({
|
|
27465
27780
|
path: { id: sessionID },
|
|
27466
27781
|
body: {
|
|
27467
27782
|
parts: [{ type: "text", text: prompt }],
|
|
27468
27783
|
agent
|
|
27469
27784
|
},
|
|
27470
27785
|
query: { directory: ctx.directory }
|
|
27471
|
-
});
|
|
27786
|
+
}), timeoutMs, `${agent} (${description})`);
|
|
27472
27787
|
const messagesResp = await ctx.client.session.messages({
|
|
27473
27788
|
path: { id: sessionID },
|
|
27474
27789
|
query: { directory: ctx.directory }
|
|
27475
27790
|
});
|
|
27476
27791
|
const messages = messagesResp.data ?? [];
|
|
27477
27792
|
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
27478
|
-
const
|
|
27793
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
27479
27794
|
`) ?? "(No response from agent)";
|
|
27480
27795
|
internalSessions.delete(sessionID);
|
|
27481
27796
|
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
27482
27797
|
const agentTime = ((Date.now() - t0) / 1000).toFixed(1);
|
|
27483
27798
|
log(`spawn_agent: ${agent} done`, { seconds: agentTime, description });
|
|
27799
|
+
const result = sanitizeSpawnResult(rawResult);
|
|
27484
27800
|
return `## ${description} (${agentTime}s)
|
|
27485
27801
|
|
|
27486
27802
|
**Agent**: ${agent}
|
|
@@ -27494,7 +27810,14 @@ ${result}`;
|
|
|
27494
27810
|
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
27495
27811
|
}
|
|
27496
27812
|
const msg = error92 instanceof Error ? error92.message : String(error92);
|
|
27497
|
-
|
|
27813
|
+
const isTimeout = msg.startsWith("Timeout:");
|
|
27814
|
+
log(`spawn_agent: ${agent} ${isTimeout ? "timeout" : "error"}`, { error: msg });
|
|
27815
|
+
if (isTimeout) {
|
|
27816
|
+
return `## ${description}
|
|
27817
|
+
|
|
27818
|
+
**Agent**: ${agent}
|
|
27819
|
+
**Timeout**: Agent did not complete within ${(timeoutMs / 1000).toFixed(0)}s`;
|
|
27820
|
+
}
|
|
27498
27821
|
return `## ${description}
|
|
27499
27822
|
|
|
27500
27823
|
**Agent**: ${agent}
|
|
@@ -27502,6 +27825,7 @@ ${result}`;
|
|
|
27502
27825
|
}
|
|
27503
27826
|
}
|
|
27504
27827
|
function createSpawnAgentTool(ctx, internalSessions, deps = {}) {
|
|
27828
|
+
const maxTotalSpawned = deps.maxTotalSpawned ?? DEFAULT_MAX_TOTAL_SPAWNED;
|
|
27505
27829
|
return tool({
|
|
27506
27830
|
description: `Spawn subagents to execute tasks in PARALLEL.
|
|
27507
27831
|
All agents in the array run concurrently (respecting concurrency limits if configured).
|
|
@@ -27546,6 +27870,11 @@ spawn_agent({
|
|
|
27546
27870
|
return `Error: agents[${i}].description is required`;
|
|
27547
27871
|
}
|
|
27548
27872
|
}
|
|
27873
|
+
const currentActive = internalSessions.size;
|
|
27874
|
+
if (currentActive + agents.length > maxTotalSpawned) {
|
|
27875
|
+
log("spawn_agent: BLOCKED by spawn limit", { currentActive, requested: agents.length, max: maxTotalSpawned });
|
|
27876
|
+
return `Error: Too many concurrent spawned agents. Active: ${currentActive}, requested: ${agents.length}, max: ${maxTotalSpawned}. Wait for active agents to complete or increase safety.maxTotalSpawned.`;
|
|
27877
|
+
}
|
|
27549
27878
|
const agentNames = agents.map((a) => a.agent);
|
|
27550
27879
|
log("spawn_agent", { count: agents.length, agents: agentNames });
|
|
27551
27880
|
showToast(ctx, "spawn_agent", `${agents.length} agents: ${agentNames.join(", ")}`);
|
|
@@ -27622,8 +27951,21 @@ function resolveTaskModel(task, deps) {
|
|
|
27622
27951
|
// src/tools/ralph-loop.ts
|
|
27623
27952
|
var COMPLETION_MARKER = "<promise>DONE</promise>";
|
|
27624
27953
|
var DEFAULT_MAX_ITERATIONS = 10;
|
|
27954
|
+
var DEFAULT_ITERATION_TIMEOUT_MS = 180000;
|
|
27625
27955
|
var activeLoops = new Map;
|
|
27626
|
-
function
|
|
27956
|
+
async function withTimeout2(promise3, ms, label) {
|
|
27957
|
+
let timer;
|
|
27958
|
+
const timeout = new Promise((_, reject) => {
|
|
27959
|
+
timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
27960
|
+
});
|
|
27961
|
+
try {
|
|
27962
|
+
return await Promise.race([promise3, timeout]);
|
|
27963
|
+
} finally {
|
|
27964
|
+
clearTimeout(timer);
|
|
27965
|
+
}
|
|
27966
|
+
}
|
|
27967
|
+
function createRalphLoopTools(ctx, internalSessions, deps = {}) {
|
|
27968
|
+
const iterationTimeoutMs = deps.iterationTimeoutMs ?? DEFAULT_ITERATION_TIMEOUT_MS;
|
|
27627
27969
|
const ralph_loop = tool({
|
|
27628
27970
|
description: `Start autonomous loop. Agent keeps working until it outputs ${COMPLETION_MARKER} or max iterations reached.
|
|
27629
27971
|
Use this for tasks that require multiple rounds of autonomous work (implementation, refactoring, migration).
|
|
@@ -27651,29 +27993,43 @@ The agent will be prompted to continue from where it left off each iteration.`,
|
|
|
27651
27993
|
sessionID
|
|
27652
27994
|
};
|
|
27653
27995
|
activeLoops.set(sessionID, state);
|
|
27654
|
-
log(`Ralph Loop started: ${agentName}, max ${maxIter} iterations`);
|
|
27996
|
+
log(`Ralph Loop started: ${agentName}, max ${maxIter} iterations, timeout ${iterationTimeoutMs}ms/iter`);
|
|
27655
27997
|
try {
|
|
27656
27998
|
for (let i = 0;i < maxIter && state.active; i++) {
|
|
27657
27999
|
state.iteration = i + 1;
|
|
27658
28000
|
toolCtx.metadata({ title: `Ralph Loop [${i + 1}/${maxIter}] \u2014 ${agentName}` });
|
|
27659
28001
|
const prompt = i === 0 ? args.prompt : buildContinuationPrompt(args.prompt, i + 1);
|
|
27660
|
-
|
|
27661
|
-
|
|
27662
|
-
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
|
|
27667
|
-
|
|
28002
|
+
try {
|
|
28003
|
+
await withTimeout2(ctx.client.session.prompt({
|
|
28004
|
+
path: { id: sessionID },
|
|
28005
|
+
body: {
|
|
28006
|
+
parts: [{ type: "text", text: prompt }],
|
|
28007
|
+
agent: agentName
|
|
28008
|
+
},
|
|
28009
|
+
query: { directory: ctx.directory }
|
|
28010
|
+
}), iterationTimeoutMs, `Ralph Loop iteration ${i + 1}`);
|
|
28011
|
+
} catch (iterError) {
|
|
28012
|
+
const msg = iterError instanceof Error ? iterError.message : String(iterError);
|
|
28013
|
+
if (msg.startsWith("Timeout:")) {
|
|
28014
|
+
log(`Ralph Loop iteration ${i + 1} timed out`);
|
|
28015
|
+
toolCtx.metadata({ title: `Ralph Loop TIMEOUT [${i + 1}/${maxIter}]` });
|
|
28016
|
+
return `## Ralph Loop Timeout (iteration ${i + 1}/${maxIter})
|
|
28017
|
+
|
|
28018
|
+
**Agent**: ${agentName}
|
|
28019
|
+
**Timeout**: Iteration did not complete within ${(iterationTimeoutMs / 1000).toFixed(0)}s`;
|
|
28020
|
+
}
|
|
28021
|
+
throw iterError;
|
|
28022
|
+
}
|
|
27668
28023
|
const messagesResp = await ctx.client.session.messages({
|
|
27669
28024
|
path: { id: sessionID },
|
|
27670
28025
|
query: { directory: ctx.directory }
|
|
27671
28026
|
});
|
|
27672
28027
|
const messages = messagesResp.data ?? [];
|
|
27673
28028
|
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
27674
|
-
const
|
|
28029
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
27675
28030
|
`) ?? "";
|
|
27676
|
-
|
|
28031
|
+
const resultText = sanitizeSpawnResult(rawResult);
|
|
28032
|
+
if (rawResult.includes(COMPLETION_MARKER)) {
|
|
27677
28033
|
toolCtx.metadata({ title: `Ralph Loop DONE [${i + 1}/${maxIter}]` });
|
|
27678
28034
|
log(`Ralph Loop completed at iteration ${i + 1}`);
|
|
27679
28035
|
const cleaned = resultText.replace(COMPLETION_MARKER, "").trim();
|
|
@@ -27968,6 +28324,199 @@ function createAstSearchTool(ctx, binaryPath) {
|
|
|
27968
28324
|
});
|
|
27969
28325
|
}
|
|
27970
28326
|
|
|
28327
|
+
// src/tools/evolve-apply.ts
|
|
28328
|
+
import * as fs7 from "fs";
|
|
28329
|
+
import * as path7 from "path";
|
|
28330
|
+
function readJson(filePath) {
|
|
28331
|
+
try {
|
|
28332
|
+
return JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
28333
|
+
} catch {
|
|
28334
|
+
return null;
|
|
28335
|
+
}
|
|
28336
|
+
}
|
|
28337
|
+
function writeJson(filePath, data) {
|
|
28338
|
+
fs7.writeFileSync(filePath, JSON.stringify(data, null, 2) + `
|
|
28339
|
+
`, "utf-8");
|
|
28340
|
+
}
|
|
28341
|
+
function createBackup(configDir) {
|
|
28342
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
28343
|
+
const backupDir = path7.join(configDir, "backups", `pre-evolve-${ts}`);
|
|
28344
|
+
fs7.mkdirSync(backupDir, { recursive: true });
|
|
28345
|
+
const configPath = path7.join(configDir, "opencode.json");
|
|
28346
|
+
const pkgPath = path7.join(configDir, "package.json");
|
|
28347
|
+
const cachePkgPath = path7.join(getCacheDir(), "package.json");
|
|
28348
|
+
if (fs7.existsSync(configPath)) {
|
|
28349
|
+
fs7.copyFileSync(configPath, path7.join(backupDir, "opencode.json"));
|
|
28350
|
+
}
|
|
28351
|
+
if (fs7.existsSync(pkgPath)) {
|
|
28352
|
+
fs7.copyFileSync(pkgPath, path7.join(backupDir, "package.json"));
|
|
28353
|
+
}
|
|
28354
|
+
if (fs7.existsSync(cachePkgPath)) {
|
|
28355
|
+
fs7.copyFileSync(cachePkgPath, path7.join(backupDir, "cache-package.json"));
|
|
28356
|
+
}
|
|
28357
|
+
log("Evolve backup created", { path: backupDir });
|
|
28358
|
+
return backupDir;
|
|
28359
|
+
}
|
|
28360
|
+
function createEvolveApplyTool(ctx) {
|
|
28361
|
+
return tool({
|
|
28362
|
+
description: `Apply evolve scan recommendations to OpenCode configuration.
|
|
28363
|
+
Takes a list of plugin recommendations (from evolve scan), computes trust scores,
|
|
28364
|
+
creates a backup, and adds approved plugins to opencode.json and package.json.
|
|
28365
|
+
|
|
28366
|
+
IMPORTANT: Only plugins with trust score >= 40 (low or above) are added.
|
|
28367
|
+
Risky plugins (score < 40) are skipped with a warning.
|
|
28368
|
+
|
|
28369
|
+
Use dryRun: true to preview changes without applying them.
|
|
28370
|
+
|
|
28371
|
+
Example:
|
|
28372
|
+
evolve_apply({
|
|
28373
|
+
plugins: [
|
|
28374
|
+
{name: "opencode-supermemory", version: "latest", reason: "Persistent memory across sessions"},
|
|
28375
|
+
{name: "@azumag/opencode-rate-limit-fallback", version: "^1.0.0", reason: "Rate limit handling"}
|
|
28376
|
+
],
|
|
28377
|
+
dryRun: false
|
|
28378
|
+
})`,
|
|
28379
|
+
args: {
|
|
28380
|
+
plugins: tool.schema.array(tool.schema.object({
|
|
28381
|
+
name: tool.schema.string().describe("npm package name"),
|
|
28382
|
+
version: tool.schema.string().optional().describe("Version spec (default: latest)"),
|
|
28383
|
+
reason: tool.schema.string().describe("Why this plugin is recommended"),
|
|
28384
|
+
metadata: tool.schema.object({
|
|
28385
|
+
name: tool.schema.string(),
|
|
28386
|
+
lastPublished: tool.schema.string().optional(),
|
|
28387
|
+
weeklyDownloads: tool.schema.number().optional(),
|
|
28388
|
+
stars: tool.schema.number().optional(),
|
|
28389
|
+
repository: tool.schema.string().optional(),
|
|
28390
|
+
license: tool.schema.string().optional(),
|
|
28391
|
+
hasReadme: tool.schema.boolean().optional(),
|
|
28392
|
+
maintainerCount: tool.schema.number().optional(),
|
|
28393
|
+
dependencyCount: tool.schema.number().optional()
|
|
28394
|
+
}).optional().describe("Package metadata for trust scoring (if available)")
|
|
28395
|
+
})).describe("Plugins to evaluate and potentially install"),
|
|
28396
|
+
dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: false)"),
|
|
28397
|
+
minTrustScore: tool.schema.number().optional().describe("Minimum trust score to auto-install (default: 40)")
|
|
28398
|
+
},
|
|
28399
|
+
execute: async (args) => {
|
|
28400
|
+
const { plugins } = args;
|
|
28401
|
+
const dryRun = args.dryRun ?? false;
|
|
28402
|
+
const minScore = args.minTrustScore ?? 40;
|
|
28403
|
+
if (!plugins || plugins.length === 0) {
|
|
28404
|
+
return "No plugins specified.";
|
|
28405
|
+
}
|
|
28406
|
+
const configDir = getConfigDir();
|
|
28407
|
+
const configPath = path7.join(configDir, "opencode.json");
|
|
28408
|
+
const cachePkgPath = path7.join(getCacheDir(), "package.json");
|
|
28409
|
+
const config3 = readJson(configPath);
|
|
28410
|
+
if (!config3) {
|
|
28411
|
+
return `Error: Cannot read ${configPath}`;
|
|
28412
|
+
}
|
|
28413
|
+
const currentPlugins = config3.plugin ?? [];
|
|
28414
|
+
const cachePkg = readJson(cachePkgPath);
|
|
28415
|
+
const currentDeps = cachePkg?.dependencies ?? {};
|
|
28416
|
+
const added = [];
|
|
28417
|
+
const skipped = [];
|
|
28418
|
+
const trustLines = [];
|
|
28419
|
+
for (const rec of plugins) {
|
|
28420
|
+
const alreadyInConfig = currentPlugins.some((p) => p === rec.name || p.startsWith(rec.name + "@"));
|
|
28421
|
+
const alreadyInDeps = rec.name in currentDeps;
|
|
28422
|
+
if (alreadyInConfig || alreadyInDeps) {
|
|
28423
|
+
skipped.push({ name: rec.name, reason: "Already installed" });
|
|
28424
|
+
trustLines.push(` SKIP ${rec.name} \u2014 already installed`);
|
|
28425
|
+
continue;
|
|
28426
|
+
}
|
|
28427
|
+
const meta3 = rec.metadata ?? { name: rec.name };
|
|
28428
|
+
const trustResult = computeTrustScore(meta3);
|
|
28429
|
+
const scoreTag = `[${trustResult.score}/100 ${trustResult.level}]`;
|
|
28430
|
+
trustLines.push(` ${scoreTag} ${rec.name} \u2014 ${rec.reason}`);
|
|
28431
|
+
for (const f of trustResult.factors) {
|
|
28432
|
+
trustLines.push(` ${f.name}: ${f.score}/${f.maxScore} (${f.detail})`);
|
|
28433
|
+
}
|
|
28434
|
+
if (trustResult.score < minScore) {
|
|
28435
|
+
skipped.push({
|
|
28436
|
+
name: rec.name,
|
|
28437
|
+
reason: `Trust score ${trustResult.score} < ${minScore} (${trustResult.level})`
|
|
28438
|
+
});
|
|
28439
|
+
trustLines.push(` -> REJECTED (below threshold ${minScore})`);
|
|
28440
|
+
continue;
|
|
28441
|
+
}
|
|
28442
|
+
added.push(rec.name);
|
|
28443
|
+
trustLines.push(` -> APPROVED`);
|
|
28444
|
+
}
|
|
28445
|
+
const trustReport = trustLines.join(`
|
|
28446
|
+
`);
|
|
28447
|
+
if (added.length === 0) {
|
|
28448
|
+
log("Evolve apply: nothing to add", { skipped: skipped.length });
|
|
28449
|
+
return formatResult({
|
|
28450
|
+
added: [],
|
|
28451
|
+
skipped,
|
|
28452
|
+
dryRun,
|
|
28453
|
+
trustReport
|
|
28454
|
+
});
|
|
28455
|
+
}
|
|
28456
|
+
if (dryRun) {
|
|
28457
|
+
log("Evolve apply: dry run", { wouldAdd: added });
|
|
28458
|
+
return formatResult({ added, skipped, dryRun: true, trustReport });
|
|
28459
|
+
}
|
|
28460
|
+
const backupDir = createBackup(configDir);
|
|
28461
|
+
const pluginEntry = (name, version3) => version3 && version3 !== "latest" ? `${name}@${version3}` : name;
|
|
28462
|
+
for (const name of added) {
|
|
28463
|
+
const rec = plugins.find((p) => p.name === name);
|
|
28464
|
+
const entry = pluginEntry(name, rec.version);
|
|
28465
|
+
if (!currentPlugins.includes(entry)) {
|
|
28466
|
+
currentPlugins.push(entry);
|
|
28467
|
+
}
|
|
28468
|
+
}
|
|
28469
|
+
config3.plugin = currentPlugins;
|
|
28470
|
+
writeJson(configPath, config3);
|
|
28471
|
+
log("Evolve apply: updated opencode.json", { plugins: currentPlugins });
|
|
28472
|
+
if (cachePkg) {
|
|
28473
|
+
const deps = cachePkg.dependencies ?? {};
|
|
28474
|
+
for (const name of added) {
|
|
28475
|
+
const rec = plugins.find((p) => p.name === name);
|
|
28476
|
+
deps[name] = rec.version ?? "latest";
|
|
28477
|
+
}
|
|
28478
|
+
cachePkg.dependencies = deps;
|
|
28479
|
+
writeJson(cachePkgPath, cachePkg);
|
|
28480
|
+
log("Evolve apply: updated cache/package.json");
|
|
28481
|
+
}
|
|
28482
|
+
return formatResult({ added, skipped, backup: backupDir, dryRun: false, trustReport });
|
|
28483
|
+
}
|
|
28484
|
+
});
|
|
28485
|
+
}
|
|
28486
|
+
function formatResult(result) {
|
|
28487
|
+
const lines = [];
|
|
28488
|
+
lines.push(result.dryRun ? "## Evolve Apply (DRY RUN)" : "## Evolve Apply Results");
|
|
28489
|
+
lines.push("");
|
|
28490
|
+
if (result.backup) {
|
|
28491
|
+
lines.push(`**Backup**: \`${result.backup}\``);
|
|
28492
|
+
lines.push("");
|
|
28493
|
+
}
|
|
28494
|
+
lines.push("### Trust Score Report");
|
|
28495
|
+
lines.push("```");
|
|
28496
|
+
lines.push(result.trustReport);
|
|
28497
|
+
lines.push("```");
|
|
28498
|
+
lines.push("");
|
|
28499
|
+
if (result.added.length > 0) {
|
|
28500
|
+
lines.push(result.dryRun ? "### Would Add" : "### Added");
|
|
28501
|
+
for (const name of result.added) {
|
|
28502
|
+
lines.push(`- ${name}`);
|
|
28503
|
+
}
|
|
28504
|
+
lines.push("");
|
|
28505
|
+
}
|
|
28506
|
+
if (result.skipped.length > 0) {
|
|
28507
|
+
lines.push("### Skipped");
|
|
28508
|
+
for (const { name, reason } of result.skipped) {
|
|
28509
|
+
lines.push(`- ${name} \u2014 ${reason}`);
|
|
28510
|
+
}
|
|
28511
|
+
lines.push("");
|
|
28512
|
+
}
|
|
28513
|
+
if (!result.dryRun && result.added.length > 0) {
|
|
28514
|
+
lines.push("> **Note**: Run `bun install` in `~/.cache/opencode/` to install the new packages, then restart OpenCode.");
|
|
28515
|
+
}
|
|
28516
|
+
return lines.join(`
|
|
28517
|
+
`);
|
|
28518
|
+
}
|
|
28519
|
+
|
|
27971
28520
|
// src/hooks/todo-enforcer.ts
|
|
27972
28521
|
var DEFAULT_MAX_ENFORCEMENTS = 5;
|
|
27973
28522
|
var sessionState = new Map;
|
|
@@ -28050,7 +28599,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
|
|
|
28050
28599
|
}
|
|
28051
28600
|
|
|
28052
28601
|
// src/hooks/comment-checker.ts
|
|
28053
|
-
import * as
|
|
28602
|
+
import * as fs8 from "fs";
|
|
28054
28603
|
var AI_SLOP_PATTERNS = [
|
|
28055
28604
|
/\/\/ .{80,}/,
|
|
28056
28605
|
/\/\/ (This|The|We|Here|Note:)/i,
|
|
@@ -28129,7 +28678,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
|
|
|
28129
28678
|
if (!filePath || !isCodeFile(filePath))
|
|
28130
28679
|
return;
|
|
28131
28680
|
try {
|
|
28132
|
-
const content =
|
|
28681
|
+
const content = fs8.readFileSync(filePath, "utf-8");
|
|
28133
28682
|
const result = checkComments(content, filePath, maxRatio, slopThreshold);
|
|
28134
28683
|
if (result.shouldWarn) {
|
|
28135
28684
|
output.output += `
|
|
@@ -28421,17 +28970,23 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
28421
28970
|
const resolveAgentModel = (agent) => {
|
|
28422
28971
|
return agents[agent]?.model;
|
|
28423
28972
|
};
|
|
28973
|
+
const safetyConfig = pluginConfig.safety ?? {};
|
|
28424
28974
|
const spawnAgent = createSpawnAgentTool(ctx, internalSessions, {
|
|
28425
28975
|
pool,
|
|
28426
28976
|
categories: pluginConfig.categories,
|
|
28427
|
-
resolveAgentModel
|
|
28977
|
+
resolveAgentModel,
|
|
28978
|
+
maxTotalSpawned: safetyConfig.maxTotalSpawned,
|
|
28979
|
+
agentTimeoutMs: safetyConfig.agentTimeoutMs
|
|
28980
|
+
});
|
|
28981
|
+
const ralphTools = createRalphLoopTools(ctx, internalSessions, {
|
|
28982
|
+
iterationTimeoutMs: safetyConfig.agentTimeoutMs
|
|
28428
28983
|
});
|
|
28429
|
-
const ralphTools = createRalphLoopTools(ctx, internalSessions);
|
|
28430
28984
|
const batchRead = createBatchReadTool(ctx);
|
|
28431
28985
|
const ledgerSave = createLedgerSaveTool(ctx);
|
|
28432
28986
|
const ledgerLoad = createLedgerLoadTool(ctx);
|
|
28433
28987
|
const astGrepBin = findAstGrepBinary();
|
|
28434
28988
|
const astSearch = astGrepBin ? createAstSearchTool(ctx, astGrepBin) : null;
|
|
28989
|
+
const evolveApply = createEvolveApplyTool(ctx);
|
|
28435
28990
|
const todoEnforcer = createTodoEnforcer(ctx, internalSessions, pluginConfig.todo_enforcer?.maxEnforcements);
|
|
28436
28991
|
const commentCheckerHook = createCommentCheckerHook(internalSessions, pluginConfig.comment_checker?.maxRatio, pluginConfig.comment_checker?.slopThreshold);
|
|
28437
28992
|
const tokenTruncationHook = createTokenTruncationHook(internalSessions, pluginConfig.token_truncation?.maxChars);
|
|
@@ -28470,6 +29025,9 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
28470
29025
|
if (!disabledTools.has("ast_search") && astSearch) {
|
|
28471
29026
|
toolRegistry.ast_search = astSearch;
|
|
28472
29027
|
}
|
|
29028
|
+
if (!disabledTools.has("evolve_apply")) {
|
|
29029
|
+
toolRegistry.evolve_apply = evolveApply;
|
|
29030
|
+
}
|
|
28473
29031
|
return {
|
|
28474
29032
|
tool: toolRegistry,
|
|
28475
29033
|
config: async (config3) => {
|
package/package.json
CHANGED