opencode-ultra 0.8.0 → 0.8.2
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 +40 -3
- package/dist/config.d.ts +6 -0
- package/dist/hooks/keyword-detector.d.ts +1 -1
- package/dist/index.js +713 -14
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ OpenCode 1.2.x プラグイン。マルチエージェントオーケストレ
|
|
|
4
4
|
|
|
5
5
|
## 機能一覧
|
|
6
6
|
|
|
7
|
-
### ツール (
|
|
7
|
+
### ツール (12)
|
|
8
8
|
|
|
9
9
|
| ツール | 説明 |
|
|
10
10
|
|--------|------|
|
|
@@ -16,7 +16,10 @@ OpenCode 1.2.x プラグイン。マルチエージェントオーケストレ
|
|
|
16
16
|
| `ledger_load` | Continuity Ledger の読み込み (名前指定 or 最新) |
|
|
17
17
|
| `ast_search` | AST-aware コード検索 (ast-grep/sg バイナリ使用、未インストール時は自動スキップ) |
|
|
18
18
|
| `evolve_apply` | プラグイン推薦の信頼度スコア評価 + OpenCode 設定への自動適用 (dry-run/backup 対応) |
|
|
19
|
+
| `evolve_scan` | 改善提案 JSONL の検証・マージ・スコアリング・番号付きレポート生成 |
|
|
19
20
|
| `evolve_score` | 改善提案のスコアリング・ランキング (Priority x Effort 重み付け) |
|
|
21
|
+
| `evolve_exe` | 提案の自律実行パイプライン (git branch → implement → test → build → review → merge/rollback) |
|
|
22
|
+
| `evolve_publish` | バージョンアップ + テスト + ビルド + npm publish (デプロイ手順出力対応) |
|
|
20
23
|
|
|
21
24
|
### フック (9)
|
|
22
25
|
|
|
@@ -70,7 +73,32 @@ OpenCode 1.2.x プラグイン。マルチエージェントオーケストレ
|
|
|
70
73
|
|
|
71
74
|
P0+Low=30 (最優先) / P2+High=1 (最低)
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
### Evolve Execution (自律実行)
|
|
77
|
+
|
|
78
|
+
提案の実装を `evolve_exe` で完全自動化できる。各提案ごとに git branch で隔離し、失敗時は自動ロールバック。
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
evolve_exe({ proposals: "1,3" }) — 提案 #1 と #3 を自律実行
|
|
82
|
+
evolve_exe({ proposals: "all" }) — accepted 全提案を実行
|
|
83
|
+
evolve_exe({ proposals: "1", dryRun: true }) — 実行プランのみ表示
|
|
84
|
+
evolve_exe({ proposals: "1,2", publish: true }) — 実行後に自動 publish
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Phase | 内容 |
|
|
88
|
+
|-------|------|
|
|
89
|
+
| 1. GIT BRANCH | `evolve/{slug}` ブランチ作成 |
|
|
90
|
+
| 2. IMPLEMENT | hephaestus が ralph_loop パターンで自律実装 |
|
|
91
|
+
| 3. TEST | `bun test` 実行 (失敗 → rollback) |
|
|
92
|
+
| 4. BUILD | `bun run build` 実行 (失敗 → rollback) |
|
|
93
|
+
| 5. REVIEW | momus がコードレビュー (BLOCK はログ記録) |
|
|
94
|
+
| 6. MERGE | `--no-ff` で元ブランチにマージ、ブランチ削除 |
|
|
95
|
+
|
|
96
|
+
実装後のパブリッシュ:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
evolve_publish({ bump: "patch" }) — test → build → version bump → npm publish
|
|
100
|
+
evolve_publish({ bump: "minor", deploy: "ssh-124" }) — publish + SSH デプロイ手順出力
|
|
101
|
+
```
|
|
74
102
|
|
|
75
103
|
## エージェント構成
|
|
76
104
|
|
|
@@ -147,7 +175,8 @@ Sisyphus (オーケストレーター) が読み込み・分析・計画を行
|
|
|
147
175
|
// fragment-injector, prompt-renderer, comment-checker,
|
|
148
176
|
// token-truncation, todo-enforcer, session-compaction
|
|
149
177
|
"disabled_tools": [], // spawn_agent, ralph_loop, cancel_ralph, batch_read,
|
|
150
|
-
// ledger_save, ledger_load, ast_search, evolve_apply,
|
|
178
|
+
// ledger_save, ledger_load, ast_search, evolve_apply,
|
|
179
|
+
// evolve_scan, evolve_score, evolve_exe, evolve_publish
|
|
151
180
|
"disabled_mcps": [], // context7
|
|
152
181
|
|
|
153
182
|
// Built-in Agent Demotion (default: true)
|
|
@@ -166,6 +195,14 @@ Sisyphus (オーケストレーター) が読み込み・分析・計画を行
|
|
|
166
195
|
"agentTimeoutMs": 180000
|
|
167
196
|
},
|
|
168
197
|
|
|
198
|
+
// Evolve Execution
|
|
199
|
+
"evolve_exe": {
|
|
200
|
+
"maxIterations": 10, // hephaestus の最大イテレーション (1-20)
|
|
201
|
+
"iterationTimeoutMs": 300000, // per-iteration タイムアウト
|
|
202
|
+
"skipReview": false, // momus レビューをスキップ
|
|
203
|
+
"skipTests": false // bun test をスキップ
|
|
204
|
+
},
|
|
205
|
+
|
|
169
206
|
// Token Truncation
|
|
170
207
|
"token_truncation": { "maxChars": 30000 },
|
|
171
208
|
|
package/dist/config.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ declare const PluginConfigSchema: z.ZodObject<{
|
|
|
73
73
|
maxTotalSpawned: z.ZodOptional<z.ZodNumber>;
|
|
74
74
|
agentTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
75
75
|
}, z.core.$strip>>;
|
|
76
|
+
evolve_exe: z.ZodOptional<z.ZodObject<{
|
|
77
|
+
maxIterations: z.ZodOptional<z.ZodNumber>;
|
|
78
|
+
iterationTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
skipReview: z.ZodOptional<z.ZodBoolean>;
|
|
80
|
+
skipTests: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
}, z.core.$strip>>;
|
|
76
82
|
}, z.core.$loose>;
|
|
77
83
|
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
78
84
|
export declare function parsePluginConfig(raw: unknown): PluginConfig;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface DetectedKeyword {
|
|
2
|
-
type: "ultrawork" | "search" | "analyze" | "think" | "evolve";
|
|
2
|
+
type: "ultrawork" | "search" | "analyze" | "think" | "evolve" | "evolve-exe" | "evolve-scan" | "evolve-publish";
|
|
3
3
|
message: string;
|
|
4
4
|
}
|
|
5
5
|
export declare function removeCodeBlocks(text: string): string;
|
package/dist/index.js
CHANGED
|
@@ -14472,6 +14472,12 @@ var PluginConfigSchema = exports_external.object({
|
|
|
14472
14472
|
safety: exports_external.object({
|
|
14473
14473
|
maxTotalSpawned: exports_external.number().min(1).optional(),
|
|
14474
14474
|
agentTimeoutMs: exports_external.number().min(1000).optional()
|
|
14475
|
+
}).optional(),
|
|
14476
|
+
evolve_exe: exports_external.object({
|
|
14477
|
+
maxIterations: exports_external.number().min(1).max(20).optional(),
|
|
14478
|
+
iterationTimeoutMs: exports_external.number().min(1e4).optional(),
|
|
14479
|
+
skipReview: exports_external.boolean().optional(),
|
|
14480
|
+
skipTests: exports_external.boolean().optional()
|
|
14475
14481
|
}).optional()
|
|
14476
14482
|
}).passthrough();
|
|
14477
14483
|
function parsePluginConfig(raw) {
|
|
@@ -14530,7 +14536,8 @@ function mergeConfigs(base, override) {
|
|
|
14530
14536
|
])],
|
|
14531
14537
|
background_task: override.background_task ?? base.background_task,
|
|
14532
14538
|
comment_checker: override.comment_checker ?? base.comment_checker,
|
|
14533
|
-
todo_enforcer: override.todo_enforcer ?? base.todo_enforcer
|
|
14539
|
+
todo_enforcer: override.todo_enforcer ?? base.todo_enforcer,
|
|
14540
|
+
evolve_exe: override.evolve_exe ?? base.evolve_exe
|
|
14534
14541
|
};
|
|
14535
14542
|
}
|
|
14536
14543
|
function detectConfigFile(basePath) {
|
|
@@ -14819,6 +14826,9 @@ var ULTRAWORK_PATTERN = /\b(ultrawork|ulw)\b/i;
|
|
|
14819
14826
|
var THINK_PATTERN = /\b(think\s+hard|think\s+through|think\s+deeply|think\s+carefully)\b|\u3058\u3063\u304F\u308A|\u6DF1\u304F\u8003\u3048\u3066|\u719F\u8003/i;
|
|
14820
14827
|
var SEARCH_PATTERN = /\b(search|find|locate|lookup|look\s*up|explore|discover|scan|grep|query|browse|detect|trace|seek|track|pinpoint|hunt)\b|where\s+is|show\s+me|list\s+all|\u691C\u7D22|\u63A2\u3057\u3066|\u898B\u3064\u3051\u3066|\u30B5\u30FC\u30C1|\u63A2\u7D22|\u30B9\u30AD\u30E3\u30F3|\u3069\u3053|\u767A\u898B|\u635C\u7D22|\u898B\u3064\u3051\u51FA\u3059|\u4E00\u89A7|\u641C\u7D22|\u67E5\u627E|\u5BFB\u627E|\u67E5\u8BE2|\u68C0\u7D22|\u5B9A\u4F4D|\u626B\u63CF|\u53D1\u73B0|\u5728\u54EA\u91CC|\u627E\u51FA\u6765|\u5217\u51FA/i;
|
|
14821
14828
|
var ANALYZE_PATTERN = /\b(analyze|analyse|investigate|examine|research|study|deep[\s-]?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to|\u5206\u6790|\u8ABF\u67FB|\u89E3\u6790|\u691C\u8A0E|\u7814\u7A76|\u8A3A\u65AD|\u7406\u89E3|\u8AAC\u660E|\u691C\u8A3C|\u7CBE\u67FB|\u7A76\u660E|\u30C7\u30D0\u30C3\u30B0|\u306A\u305C|\u3069\u3046|\u4ED5\u7D44\u307F|\u8C03\u67E5|\u68C0\u67E5|\u5256\u6790|\u6DF1\u5165|\u8BCA\u65AD|\u89E3\u91CA|\u8C03\u8BD5|\u4E3A\u4EC0\u4E48|\u539F\u7406|\u641E\u6E05\u695A|\u5F04\u660E\u767D/i;
|
|
14829
|
+
var EVOLVE_EXE_PATTERN = /\bevolve[\s\u3000_-]*(exe|exec|execute|run|\u5B9F\u884C)/i;
|
|
14830
|
+
var EVOLVE_SCAN_PATTERN = /\bevolve[\s\u3000_-]*(scan|\u30B9\u30AD\u30E3\u30F3|report|\u30EC\u30DD\u30FC\u30C8)/i;
|
|
14831
|
+
var EVOLVE_PUBLISH_PATTERN = /\bevolve[\s\u3000_-]*(publish|\u516C\u958B|\u30EA\u30EA\u30FC\u30B9|release)/i;
|
|
14822
14832
|
var EVOLVE_PATTERN = /\b(evolve|self[\s-]?improve|self[\s-]?upgrade|plugin[\s-]?scout|ecosystem[\s-]?scan)\b|\u81EA\u5DF1\u6539\u5584|\u9032\u5316|\u30D7\u30E9\u30B0\u30A4\u30F3\u63A2\u7D22|\u30A8\u30B3\u30B7\u30B9\u30C6\u30E0|\u81EA\u6211\u8FDB\u5316|\u63D2\u4EF6\u641C\u7D22/i;
|
|
14823
14833
|
function removeCodeBlocks(text) {
|
|
14824
14834
|
return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "");
|
|
@@ -14831,11 +14841,19 @@ var KEYWORD_DEFS = [
|
|
|
14831
14841
|
{ pattern: SEARCH_PATTERN, type: "search", getMessage: () => SEARCH_MESSAGE },
|
|
14832
14842
|
{ pattern: ANALYZE_PATTERN, type: "analyze", getMessage: () => ANALYZE_MESSAGE },
|
|
14833
14843
|
{ pattern: THINK_PATTERN, type: "think", getMessage: () => THINK_MESSAGE },
|
|
14844
|
+
{ pattern: EVOLVE_EXE_PATTERN, type: "evolve-exe", getMessage: (_ctx, text) => buildEvolveExeMessage(text) },
|
|
14845
|
+
{ pattern: EVOLVE_SCAN_PATTERN, type: "evolve-scan", getMessage: () => EVOLVE_SCAN_MESSAGE },
|
|
14846
|
+
{ pattern: EVOLVE_PUBLISH_PATTERN, type: "evolve-publish", getMessage: () => EVOLVE_PUBLISH_MESSAGE },
|
|
14834
14847
|
{ pattern: EVOLVE_PATTERN, type: "evolve", getMessage: (ctx) => buildEvolveMessage(ctx) }
|
|
14835
14848
|
];
|
|
14836
14849
|
function detectKeywords(text, evolveCtx) {
|
|
14837
14850
|
const clean = removeCodeBlocks(text);
|
|
14838
|
-
|
|
14851
|
+
const matched = KEYWORD_DEFS.filter(({ pattern }) => pattern.test(clean)).map(({ type, getMessage }) => ({ type, message: getMessage(evolveCtx, clean) }));
|
|
14852
|
+
const hasSubCommand = matched.some((k) => k.type === "evolve-exe" || k.type === "evolve-scan" || k.type === "evolve-publish");
|
|
14853
|
+
if (hasSubCommand) {
|
|
14854
|
+
return matched.filter((k) => k.type !== "evolve");
|
|
14855
|
+
}
|
|
14856
|
+
return matched;
|
|
14839
14857
|
}
|
|
14840
14858
|
var ULTRAWORK_MESSAGE = `<ultrawork-mode>
|
|
14841
14859
|
|
|
@@ -14921,6 +14939,47 @@ IF COMPLEX \u2014 DO NOT STRUGGLE ALONE. Consult specialists:
|
|
|
14921
14939
|
|
|
14922
14940
|
SYNTHESIZE findings before proceeding.`;
|
|
14923
14941
|
var THINK_MESSAGE = `Extended thinking enabled. Take your time to reason thoroughly.`;
|
|
14942
|
+
function extractEvolveExeArgs(text) {
|
|
14943
|
+
if (!text)
|
|
14944
|
+
return "";
|
|
14945
|
+
const match = text.match(/\bevolve[\s_-]*(?:exe|exec|execute|run|\u5B9F\u884C)\s+(.+?)$/im);
|
|
14946
|
+
return match?.[1]?.trim() ?? "";
|
|
14947
|
+
}
|
|
14948
|
+
function buildEvolveExeMessage(text) {
|
|
14949
|
+
const args = extractEvolveExeArgs(text);
|
|
14950
|
+
const proposalsArg = args || "all";
|
|
14951
|
+
return `[evolve-exe] EXECUTE EVOLVE PROPOSALS.
|
|
14952
|
+
|
|
14953
|
+
You MUST call the \`evolve_exe\` tool immediately. Do NOT research, do NOT propose \u2014 just execute.
|
|
14954
|
+
|
|
14955
|
+
\`\`\`
|
|
14956
|
+
evolve_exe({ proposals: "${proposalsArg}" })
|
|
14957
|
+
\`\`\`
|
|
14958
|
+
|
|
14959
|
+
If the user said "dry" or "dryrun", add \`dryRun: true\`.
|
|
14960
|
+
If the user said "publish", add \`publish: true\`.
|
|
14961
|
+
If no proposals file exists, tell the user to run \`evolve scan\` first.`;
|
|
14962
|
+
}
|
|
14963
|
+
var EVOLVE_SCAN_MESSAGE = `[evolve-scan] SCAN EXISTING PROPOSALS.
|
|
14964
|
+
|
|
14965
|
+
You MUST call the \`evolve_scan\` tool immediately to read and report on existing proposals.
|
|
14966
|
+
Do NOT do ecosystem research. Just read .opencode/evolve-proposals.jsonl and generate the numbered report.
|
|
14967
|
+
|
|
14968
|
+
\`\`\`
|
|
14969
|
+
evolve_scan({})
|
|
14970
|
+
\`\`\`
|
|
14971
|
+
|
|
14972
|
+
Show the numbered proposal list to the user after the scan completes.`;
|
|
14973
|
+
var EVOLVE_PUBLISH_MESSAGE = `[evolve-publish] PUBLISH THE PLUGIN.
|
|
14974
|
+
|
|
14975
|
+
You MUST call the \`evolve_publish\` tool immediately.
|
|
14976
|
+
|
|
14977
|
+
\`\`\`
|
|
14978
|
+
evolve_publish({ bump: "patch" })
|
|
14979
|
+
\`\`\`
|
|
14980
|
+
|
|
14981
|
+
If the user specified a bump level (minor, major), use that instead of patch.
|
|
14982
|
+
If the user mentioned a deploy target (e.g. "ssh-124"), add \`deploy: "ssh-124"\`.`;
|
|
14924
14983
|
function buildEvolveMessage(ctx) {
|
|
14925
14984
|
const inventory = ctx ? buildSelfInventory(ctx) : "(No capability data available \u2014 read source manually)";
|
|
14926
14985
|
return `[evolve-mode] SELF-IMPROVEMENT CYCLE ACTIVATED.
|
|
@@ -14982,6 +15041,17 @@ ledger_save({
|
|
|
14982
15041
|
})
|
|
14983
15042
|
\`\`\`
|
|
14984
15043
|
|
|
15044
|
+
## EXECUTION MODE
|
|
15045
|
+
|
|
15046
|
+
After proposals are scored and saved, the user can execute them:
|
|
15047
|
+
- \`evolve_exe({ proposals: "1,3" })\` \u2014 Implement proposals #1 and #3 autonomously
|
|
15048
|
+
- \`evolve_exe({ proposals: "all" })\` \u2014 Implement all accepted proposals
|
|
15049
|
+
- \`evolve_exe({ proposals: "1", dryRun: true })\` \u2014 Preview execution plan
|
|
15050
|
+
- \`evolve_exe({ proposals: "1,2", publish: true })\` \u2014 Implement + auto-publish
|
|
15051
|
+
|
|
15052
|
+
Each proposal runs on a git branch. Failed proposals are rolled back automatically.
|
|
15053
|
+
After implementation: \`evolve_publish({ bump: "patch" })\` to publish.
|
|
15054
|
+
|
|
14985
15055
|
## RULES
|
|
14986
15056
|
- Use the capability inventory above as ground truth.
|
|
14987
15057
|
- Do NOT use evolve_apply. Proposals only. Humans decide what to implement.
|
|
@@ -28893,10 +28963,10 @@ function buildReport(args) {
|
|
|
28893
28963
|
if (args.accepted.length === 0) {
|
|
28894
28964
|
lines.push("_(none)_");
|
|
28895
28965
|
} else {
|
|
28896
|
-
lines.push("| Title | Score | Priority | Effort | Description | Inspiration | Current state |");
|
|
28897
|
-
lines.push("
|
|
28966
|
+
lines.push("| # | Title | Score | Priority | Effort | Description | Inspiration | Current state |");
|
|
28967
|
+
lines.push("|---:|---|---:|---|---|---|---|---|");
|
|
28898
28968
|
for (const p of args.accepted) {
|
|
28899
|
-
lines.push(`| ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(formatMaybeCode(p.description))} | ${markdownEscape(p.inspiration ? formatMaybeCode(p.inspiration) : "")} | ${markdownEscape(p.currentState ? formatMaybeCode(p.currentState) : "")} |`);
|
|
28969
|
+
lines.push(`| #${p.index} | ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(formatMaybeCode(p.description))} | ${markdownEscape(p.inspiration ? formatMaybeCode(p.inspiration) : "")} | ${markdownEscape(p.currentState ? formatMaybeCode(p.currentState) : "")} |`);
|
|
28900
28970
|
}
|
|
28901
28971
|
}
|
|
28902
28972
|
lines.push("");
|
|
@@ -28905,10 +28975,10 @@ function buildReport(args) {
|
|
|
28905
28975
|
if (args.rejected.length === 0) {
|
|
28906
28976
|
lines.push("_(none)_");
|
|
28907
28977
|
} else {
|
|
28908
|
-
lines.push("| Title | Score | Priority | Effort | Reason |");
|
|
28909
|
-
lines.push("
|
|
28978
|
+
lines.push("| # | Title | Score | Priority | Effort | Reason |");
|
|
28979
|
+
lines.push("|---:|---|---:|---|---|---|");
|
|
28910
28980
|
for (const p of args.rejected) {
|
|
28911
|
-
lines.push(`| ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(p.reason ? formatMaybeCode(p.reason) : "")} |`);
|
|
28981
|
+
lines.push(`| #${p.index} | ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(p.reason ? formatMaybeCode(p.reason) : "")} |`);
|
|
28912
28982
|
}
|
|
28913
28983
|
}
|
|
28914
28984
|
lines.push("");
|
|
@@ -28964,8 +29034,12 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
28964
29034
|
const existingDupesRemoved = existingParsed.proposals.length - existingDeduped.length;
|
|
28965
29035
|
const incomingDupesSkipped = incomingParsed.proposals.length - addedCount;
|
|
28966
29036
|
const filtered = filterProposals(merged, { minScore, maxProposals });
|
|
28967
|
-
const
|
|
28968
|
-
|
|
29037
|
+
const titleToIndex = new Map;
|
|
29038
|
+
for (let i = 0;i < merged.length; i++) {
|
|
29039
|
+
titleToIndex.set(merged[i].title, i + 1);
|
|
29040
|
+
}
|
|
29041
|
+
const accepted = filtered.filter((p) => p.accepted).map((p) => ({ ...p, score: p.score, index: titleToIndex.get(p.title) ?? 0 }));
|
|
29042
|
+
const rejected = filtered.filter((p) => !p.accepted).map((p) => ({ ...p, score: p.score, reason: p.reason, index: titleToIndex.get(p.title) ?? 0 }));
|
|
28969
29043
|
const jsonlOut = merged.map((p) => JSON.stringify(proposalToJson(p))).join(`
|
|
28970
29044
|
`) + (merged.length > 0 ? `
|
|
28971
29045
|
` : "");
|
|
@@ -28983,11 +29057,12 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
28983
29057
|
rejectedCount: rejected.length,
|
|
28984
29058
|
existingErrors: existingParsed.errors,
|
|
28985
29059
|
incomingErrors: incomingParsed.errors,
|
|
28986
|
-
accepted: accepted.map((p) => ({ ...p, score: scoreProposal(p) })),
|
|
28987
|
-
rejected: rejected.map((p) => ({ ...p, score: scoreProposal(p) }))
|
|
29060
|
+
accepted: accepted.map((p) => ({ ...p, score: scoreProposal(p), index: p.index })),
|
|
29061
|
+
rejected: rejected.map((p) => ({ ...p, score: scoreProposal(p), index: p.index }))
|
|
28988
29062
|
});
|
|
28989
29063
|
fs8.writeFileSync(reportPath, report, "utf-8");
|
|
28990
29064
|
const skippedInvalid = existingParsed.errors.length + incomingParsed.errors.length;
|
|
29065
|
+
const proposalSummary = [...accepted, ...rejected].sort((a, b) => a.index - b.index).map((p) => ` #${p.index} ${p.title} (${p.priority}/${p.effort}, score=${scoreProposal(p)})${p.accepted ? "" : " [rejected]"}`);
|
|
28991
29066
|
return [
|
|
28992
29067
|
"## evolve_scan complete",
|
|
28993
29068
|
`- Wrote: ${proposalsPath}`,
|
|
@@ -28995,6 +29070,7 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
28995
29070
|
`- Existing proposals: ${existingParsed.proposals.length} (dedup removed ${existingDupesRemoved})`,
|
|
28996
29071
|
`- Incoming proposals: ${incomingParsed.proposals.length} (dedup skipped ${incomingDupesSkipped}, invalid skipped ${incomingParsed.errors.length})`,
|
|
28997
29072
|
`- Merged proposals: ${merged.length} (accepted ${accepted.length}, rejected ${rejected.length})`,
|
|
29073
|
+
...proposalSummary.length > 0 ? proposalSummary : [],
|
|
28998
29074
|
`- Invalid JSONL lines skipped (total): ${skippedInvalid}`
|
|
28999
29075
|
].join(`
|
|
29000
29076
|
`);
|
|
@@ -29002,6 +29078,605 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
29002
29078
|
});
|
|
29003
29079
|
}
|
|
29004
29080
|
|
|
29081
|
+
// src/tools/evolve-exe.ts
|
|
29082
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
29083
|
+
import * as fs9 from "fs";
|
|
29084
|
+
import * as path9 from "path";
|
|
29085
|
+
var COMPLETION_MARKER2 = "<promise>DONE</promise>";
|
|
29086
|
+
var DEFAULT_MAX_ITERATIONS2 = 10;
|
|
29087
|
+
var DEFAULT_ITERATION_TIMEOUT_MS2 = 300000;
|
|
29088
|
+
async function withTimeout3(promise3, ms, label) {
|
|
29089
|
+
let timer;
|
|
29090
|
+
const timeout = new Promise((_, reject) => {
|
|
29091
|
+
timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
29092
|
+
});
|
|
29093
|
+
try {
|
|
29094
|
+
return await Promise.race([promise3, timeout]);
|
|
29095
|
+
} finally {
|
|
29096
|
+
clearTimeout(timer);
|
|
29097
|
+
}
|
|
29098
|
+
}
|
|
29099
|
+
function readProposalsJsonl(cwd) {
|
|
29100
|
+
const jsonlPath = path9.join(cwd, ".opencode", "evolve-proposals.jsonl");
|
|
29101
|
+
if (!fs9.existsSync(jsonlPath)) {
|
|
29102
|
+
return { proposals: [], raw: [] };
|
|
29103
|
+
}
|
|
29104
|
+
const text = fs9.readFileSync(jsonlPath, "utf-8");
|
|
29105
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
29106
|
+
const proposals = [];
|
|
29107
|
+
const raw = [];
|
|
29108
|
+
for (const line of lines) {
|
|
29109
|
+
const v = validateProposalJsonl(line);
|
|
29110
|
+
if (v.ok) {
|
|
29111
|
+
proposals.push(v.proposal);
|
|
29112
|
+
raw.push(line);
|
|
29113
|
+
}
|
|
29114
|
+
}
|
|
29115
|
+
return { proposals, raw };
|
|
29116
|
+
}
|
|
29117
|
+
function selectProposals(proposals, selection) {
|
|
29118
|
+
const errors5 = [];
|
|
29119
|
+
if (selection.trim().toLowerCase() === "all") {
|
|
29120
|
+
const filtered = filterProposals(proposals);
|
|
29121
|
+
const accepted = filtered.filter((p) => p.accepted);
|
|
29122
|
+
return {
|
|
29123
|
+
selected: accepted.map((p) => {
|
|
29124
|
+
const idx = proposals.findIndex((orig) => orig.title === p.title);
|
|
29125
|
+
return { ...p, index: idx + 1 };
|
|
29126
|
+
}),
|
|
29127
|
+
errors: errors5
|
|
29128
|
+
};
|
|
29129
|
+
}
|
|
29130
|
+
const indices = selection.split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseInt(s, 10));
|
|
29131
|
+
const selected = [];
|
|
29132
|
+
for (const idx of indices) {
|
|
29133
|
+
if (isNaN(idx) || idx < 1 || idx > proposals.length) {
|
|
29134
|
+
errors5.push(`Invalid index: ${idx} (valid range: 1-${proposals.length})`);
|
|
29135
|
+
continue;
|
|
29136
|
+
}
|
|
29137
|
+
selected.push({ ...proposals[idx - 1], index: idx });
|
|
29138
|
+
}
|
|
29139
|
+
return { selected, errors: errors5 };
|
|
29140
|
+
}
|
|
29141
|
+
function slugify2(title) {
|
|
29142
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
29143
|
+
}
|
|
29144
|
+
function gitCmd(args, cwd) {
|
|
29145
|
+
const result = spawnSync2("git", args, { cwd, encoding: "utf-8", timeout: 30000 });
|
|
29146
|
+
return {
|
|
29147
|
+
ok: result.status === 0,
|
|
29148
|
+
stdout: (result.stdout ?? "").trim(),
|
|
29149
|
+
stderr: (result.stderr ?? "").trim()
|
|
29150
|
+
};
|
|
29151
|
+
}
|
|
29152
|
+
function buildProjectInfo(cwd) {
|
|
29153
|
+
try {
|
|
29154
|
+
const pkg = JSON.parse(fs9.readFileSync(path9.join(cwd, "package.json"), "utf-8"));
|
|
29155
|
+
const srcDir = path9.join(cwd, "src");
|
|
29156
|
+
const testDir = path9.join(cwd, "__test__");
|
|
29157
|
+
const srcFiles = fs9.existsSync(srcDir) ? fs9.readdirSync(srcDir, { recursive: true }).filter((f) => f.toString().endsWith(".ts")).map((f) => `src/${f}`) : [];
|
|
29158
|
+
const testFiles = fs9.existsSync(testDir) ? fs9.readdirSync(testDir, { recursive: true }).filter((f) => f.toString().endsWith(".ts")).map((f) => `__test__/${f}`) : [];
|
|
29159
|
+
return [
|
|
29160
|
+
`- **Package**: ${pkg.name}@${pkg.version}`,
|
|
29161
|
+
`- **Type**: ${pkg.type ?? "commonjs"}`,
|
|
29162
|
+
`- **Build**: ${pkg.scripts?.build ?? "(none)"}`,
|
|
29163
|
+
`- **Test**: ${pkg.scripts?.test ?? "(none)"}`,
|
|
29164
|
+
`- **Source files**: ${srcFiles.join(", ")}`,
|
|
29165
|
+
`- **Test files**: ${testFiles.join(", ")}`
|
|
29166
|
+
].join(`
|
|
29167
|
+
`);
|
|
29168
|
+
} catch {
|
|
29169
|
+
return "- (Could not read package.json)";
|
|
29170
|
+
}
|
|
29171
|
+
}
|
|
29172
|
+
var IMPLEMENT_PROMPT = (proposal, projectInfo) => `
|
|
29173
|
+
[EVOLVE IMPLEMENTATION \u2014 AUTONOMOUS MODE]
|
|
29174
|
+
|
|
29175
|
+
## Task
|
|
29176
|
+
Implement the following improvement proposal for this project:
|
|
29177
|
+
|
|
29178
|
+
**Title**: ${proposal.title}
|
|
29179
|
+
**Priority**: ${proposal.priority} | **Effort**: ${proposal.effort}
|
|
29180
|
+
**Description**: ${proposal.description}
|
|
29181
|
+
${proposal.currentState ? `**Current State**: ${proposal.currentState}` : ""}
|
|
29182
|
+
${proposal.files?.length ? `**Target Files**: ${proposal.files.join(", ")}` : ""}
|
|
29183
|
+
${proposal.inspiration ? `**Inspiration**: ${proposal.inspiration}` : ""}
|
|
29184
|
+
|
|
29185
|
+
## Project Context
|
|
29186
|
+
${projectInfo}
|
|
29187
|
+
|
|
29188
|
+
## Requirements
|
|
29189
|
+
1. Implement the feature described above
|
|
29190
|
+
2. Write unit tests in \`__test__/\` directory (same naming pattern as existing tests)
|
|
29191
|
+
3. Ensure all existing tests still pass
|
|
29192
|
+
4. Follow existing code patterns and conventions:
|
|
29193
|
+
- Use \`@opencode-ai/plugin\` tool() API for new tools
|
|
29194
|
+
- Pure functions where possible, side effects isolated
|
|
29195
|
+
- TypeScript strict mode compatible
|
|
29196
|
+
- Use zod for schema validation
|
|
29197
|
+
- Export types from the module
|
|
29198
|
+
5. Do NOT modify unrelated files
|
|
29199
|
+
6. Do NOT add unnecessary dependencies
|
|
29200
|
+
|
|
29201
|
+
## Completion
|
|
29202
|
+
When fully implemented and tested, output: ${COMPLETION_MARKER2}
|
|
29203
|
+
`;
|
|
29204
|
+
var REVIEW_PROMPT = (proposal, diff, testOutput) => `
|
|
29205
|
+
[CODE REVIEW \u2014 EVOLVE IMPLEMENTATION]
|
|
29206
|
+
|
|
29207
|
+
## Proposal Implemented
|
|
29208
|
+
**Title**: ${proposal.title}
|
|
29209
|
+
**Description**: ${proposal.description}
|
|
29210
|
+
|
|
29211
|
+
## Changes (git diff)
|
|
29212
|
+
\`\`\`diff
|
|
29213
|
+
${diff.slice(0, 8000)}
|
|
29214
|
+
\`\`\`
|
|
29215
|
+
|
|
29216
|
+
## Test Results
|
|
29217
|
+
\`\`\`
|
|
29218
|
+
${testOutput.slice(0, 2000)}
|
|
29219
|
+
\`\`\`
|
|
29220
|
+
|
|
29221
|
+
## Review Criteria
|
|
29222
|
+
1. **Correctness**: Does the implementation match the proposal?
|
|
29223
|
+
2. **Tests**: Are there adequate tests? Do they cover edge cases?
|
|
29224
|
+
3. **Code Quality**: Clean, readable, follows existing patterns?
|
|
29225
|
+
4. **Regressions**: Could this break existing functionality?
|
|
29226
|
+
5. **Security**: No injection vulnerabilities, no unsafe operations?
|
|
29227
|
+
|
|
29228
|
+
## Output Format
|
|
29229
|
+
Rate each criterion: PASS / WARN / FAIL
|
|
29230
|
+
Then overall: APPROVE / APPROVE_WITH_WARNINGS / BLOCK
|
|
29231
|
+
|
|
29232
|
+
If BLOCK: explain what must be fixed before merging.
|
|
29233
|
+
`;
|
|
29234
|
+
function buildContinuationPrompt2(original, iteration) {
|
|
29235
|
+
return `[Evolve Exe \u2014 Iteration ${iteration}]
|
|
29236
|
+
|
|
29237
|
+
IMPORTANT:
|
|
29238
|
+
- Review your progress so far
|
|
29239
|
+
- Continue from where you left off
|
|
29240
|
+
- When FULLY complete, output exactly: ${COMPLETION_MARKER2}
|
|
29241
|
+
- Do not stop until the task is truly done
|
|
29242
|
+
|
|
29243
|
+
Original task:
|
|
29244
|
+
${original}`;
|
|
29245
|
+
}
|
|
29246
|
+
function createEvolveExeTool(ctx, internalSessions, deps = {}) {
|
|
29247
|
+
const maxIterations = deps.evolveExeConfig?.maxIterations ?? DEFAULT_MAX_ITERATIONS2;
|
|
29248
|
+
const iterationTimeoutMs = deps.evolveExeConfig?.iterationTimeoutMs ?? deps.agentTimeoutMs ?? DEFAULT_ITERATION_TIMEOUT_MS2;
|
|
29249
|
+
const configSkipReview = deps.evolveExeConfig?.skipReview ?? false;
|
|
29250
|
+
const configSkipTests = deps.evolveExeConfig?.skipTests ?? false;
|
|
29251
|
+
return tool({
|
|
29252
|
+
description: `Execute evolve proposals autonomously. For each proposal:
|
|
29253
|
+
GIT BRANCH \u2192 IMPLEMENT (hephaestus) \u2192 TEST \u2192 BUILD \u2192 REVIEW (momus) \u2192 MERGE/ROLLBACK.
|
|
29254
|
+
|
|
29255
|
+
Proposals are identified by their JSONL line number (1-based) from .opencode/evolve-proposals.jsonl.
|
|
29256
|
+
Use evolve_scan first to see numbered proposals.
|
|
29257
|
+
|
|
29258
|
+
Examples:
|
|
29259
|
+
evolve_exe({ proposals: "1,3" }) \u2014 Execute proposals #1 and #3
|
|
29260
|
+
evolve_exe({ proposals: "all" }) \u2014 Execute all accepted proposals
|
|
29261
|
+
evolve_exe({ proposals: "1", dryRun: true }) \u2014 Preview execution plan only
|
|
29262
|
+
evolve_exe({ proposals: "1,2", publish: true }) \u2014 Execute + auto-publish after`,
|
|
29263
|
+
args: {
|
|
29264
|
+
proposals: tool.schema.string().describe('Proposal numbers: "1,3" or "all" (JSONL line numbers, 1-based)'),
|
|
29265
|
+
dryRun: tool.schema.boolean().optional().describe("Preview plan only, no execution (default: false)"),
|
|
29266
|
+
skipReview: tool.schema.boolean().optional().describe("Skip momus code review (default: false)"),
|
|
29267
|
+
skipTests: tool.schema.boolean().optional().describe("Skip bun test (default: false)"),
|
|
29268
|
+
publish: tool.schema.boolean().optional().describe("Run evolve_publish after all proposals succeed (default: false)")
|
|
29269
|
+
},
|
|
29270
|
+
execute: async (args, toolCtx) => {
|
|
29271
|
+
const cwd = ctx.directory;
|
|
29272
|
+
const dryRun = args.dryRun ?? false;
|
|
29273
|
+
const skipReview = args.skipReview ?? configSkipReview;
|
|
29274
|
+
const skipTests = args.skipTests ?? configSkipTests;
|
|
29275
|
+
toolCtx.metadata({ title: "evolve_exe: reading proposals..." });
|
|
29276
|
+
const { proposals } = readProposalsJsonl(cwd);
|
|
29277
|
+
if (proposals.length === 0) {
|
|
29278
|
+
return "## evolve_exe: No proposals found\n\nRun `evolve_scan` first to generate .opencode/evolve-proposals.jsonl";
|
|
29279
|
+
}
|
|
29280
|
+
const { selected, errors: errors5 } = selectProposals(proposals, args.proposals);
|
|
29281
|
+
if (errors5.length > 0 && selected.length === 0) {
|
|
29282
|
+
return `## evolve_exe: Selection errors
|
|
29283
|
+
|
|
29284
|
+
${errors5.map((e) => `- ${e}`).join(`
|
|
29285
|
+
`)}`;
|
|
29286
|
+
}
|
|
29287
|
+
if (selected.length === 0) {
|
|
29288
|
+
return `## evolve_exe: No proposals selected
|
|
29289
|
+
|
|
29290
|
+
No accepted proposals match the selection.`;
|
|
29291
|
+
}
|
|
29292
|
+
if (dryRun) {
|
|
29293
|
+
const lines2 = [
|
|
29294
|
+
"## evolve_exe: Execution Plan (DRY RUN)",
|
|
29295
|
+
"",
|
|
29296
|
+
`**Selected**: ${selected.length} proposal(s)`,
|
|
29297
|
+
`**Skip tests**: ${skipTests}`,
|
|
29298
|
+
`**Skip review**: ${skipReview}`,
|
|
29299
|
+
`**Auto-publish**: ${args.publish ?? false}`,
|
|
29300
|
+
"",
|
|
29301
|
+
"### Proposals",
|
|
29302
|
+
""
|
|
29303
|
+
];
|
|
29304
|
+
for (const p of selected) {
|
|
29305
|
+
lines2.push(`- **#${p.index}** ${p.title} (${p.priority}/${p.effort}, score=${scoreProposal(p)})`);
|
|
29306
|
+
lines2.push(` Branch: \`evolve/${slugify2(p.title)}\``);
|
|
29307
|
+
if (p.files?.length)
|
|
29308
|
+
lines2.push(` Files: ${p.files.join(", ")}`);
|
|
29309
|
+
}
|
|
29310
|
+
if (errors5.length > 0) {
|
|
29311
|
+
lines2.push("", "### Warnings", "");
|
|
29312
|
+
for (const e of errors5)
|
|
29313
|
+
lines2.push(`- ${e}`);
|
|
29314
|
+
}
|
|
29315
|
+
return lines2.join(`
|
|
29316
|
+
`);
|
|
29317
|
+
}
|
|
29318
|
+
const originalBranch = gitCmd(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
29319
|
+
if (!originalBranch.ok) {
|
|
29320
|
+
return `## evolve_exe: Error
|
|
29321
|
+
|
|
29322
|
+
Failed to determine current git branch. Is this a git repository?`;
|
|
29323
|
+
}
|
|
29324
|
+
const results = [];
|
|
29325
|
+
const projectInfo = buildProjectInfo(cwd);
|
|
29326
|
+
for (let i = 0;i < selected.length; i++) {
|
|
29327
|
+
const proposal = selected[i];
|
|
29328
|
+
const result = await executeProposal({
|
|
29329
|
+
ctx,
|
|
29330
|
+
toolCtx,
|
|
29331
|
+
internalSessions,
|
|
29332
|
+
proposal,
|
|
29333
|
+
originalBranch: originalBranch.stdout,
|
|
29334
|
+
cwd,
|
|
29335
|
+
projectInfo,
|
|
29336
|
+
skipTests,
|
|
29337
|
+
skipReview,
|
|
29338
|
+
maxIterations,
|
|
29339
|
+
iterationTimeoutMs,
|
|
29340
|
+
progressLabel: `[${i + 1}/${selected.length}]`
|
|
29341
|
+
});
|
|
29342
|
+
results.push(result);
|
|
29343
|
+
}
|
|
29344
|
+
const successCount = results.filter((r) => r.status === "success").length;
|
|
29345
|
+
const failedCount = results.filter((r) => r.status === "failed").length;
|
|
29346
|
+
const lines = [
|
|
29347
|
+
"## evolve_exe: Execution Complete",
|
|
29348
|
+
"",
|
|
29349
|
+
`**Results**: ${successCount} succeeded, ${failedCount} failed, ${results.length - successCount - failedCount} skipped`,
|
|
29350
|
+
""
|
|
29351
|
+
];
|
|
29352
|
+
for (const r of results) {
|
|
29353
|
+
const icon = r.status === "success" ? "PASS" : r.status === "failed" ? "FAIL" : "SKIP";
|
|
29354
|
+
lines.push(`### #${r.index} ${r.title} \u2014 ${icon}`);
|
|
29355
|
+
if (r.branch)
|
|
29356
|
+
lines.push(`- Branch: \`${r.branch}\``);
|
|
29357
|
+
if (r.error)
|
|
29358
|
+
lines.push(`- Error: ${r.error}`);
|
|
29359
|
+
if (r.reviewOutput)
|
|
29360
|
+
lines.push(`- Review: ${r.reviewOutput.slice(0, 500)}`);
|
|
29361
|
+
lines.push("");
|
|
29362
|
+
}
|
|
29363
|
+
if (args.publish && successCount > 0 && failedCount === 0) {
|
|
29364
|
+
lines.push('> All proposals succeeded. Use `evolve_publish({ bump: "patch" })` to publish.');
|
|
29365
|
+
}
|
|
29366
|
+
return lines.join(`
|
|
29367
|
+
`);
|
|
29368
|
+
}
|
|
29369
|
+
});
|
|
29370
|
+
}
|
|
29371
|
+
async function executeProposal(args) {
|
|
29372
|
+
const {
|
|
29373
|
+
ctx,
|
|
29374
|
+
toolCtx,
|
|
29375
|
+
internalSessions,
|
|
29376
|
+
proposal,
|
|
29377
|
+
originalBranch,
|
|
29378
|
+
cwd,
|
|
29379
|
+
projectInfo,
|
|
29380
|
+
skipTests,
|
|
29381
|
+
skipReview,
|
|
29382
|
+
maxIterations,
|
|
29383
|
+
iterationTimeoutMs,
|
|
29384
|
+
progressLabel
|
|
29385
|
+
} = args;
|
|
29386
|
+
const slug = slugify2(proposal.title);
|
|
29387
|
+
const branchName = `evolve/${slug}`;
|
|
29388
|
+
const result = {
|
|
29389
|
+
index: proposal.index,
|
|
29390
|
+
title: proposal.title,
|
|
29391
|
+
status: "failed",
|
|
29392
|
+
branch: branchName
|
|
29393
|
+
};
|
|
29394
|
+
log(`evolve_exe: starting #${proposal.index} "${proposal.title}"`);
|
|
29395
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 branching...` });
|
|
29396
|
+
const branchResult = gitCmd(["checkout", "-b", branchName], cwd);
|
|
29397
|
+
if (!branchResult.ok) {
|
|
29398
|
+
result.error = `Failed to create branch: ${branchResult.stderr}`;
|
|
29399
|
+
log("evolve_exe: branch creation failed", { error: branchResult.stderr });
|
|
29400
|
+
return result;
|
|
29401
|
+
}
|
|
29402
|
+
try {
|
|
29403
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 implementing...` });
|
|
29404
|
+
const implResult = await runImplementation({
|
|
29405
|
+
ctx,
|
|
29406
|
+
internalSessions,
|
|
29407
|
+
proposal,
|
|
29408
|
+
projectInfo,
|
|
29409
|
+
maxIterations,
|
|
29410
|
+
iterationTimeoutMs,
|
|
29411
|
+
toolCtx,
|
|
29412
|
+
progressLabel
|
|
29413
|
+
});
|
|
29414
|
+
if (!implResult.ok) {
|
|
29415
|
+
result.error = `Implementation failed: ${implResult.error}`;
|
|
29416
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29417
|
+
return result;
|
|
29418
|
+
}
|
|
29419
|
+
if (!skipTests) {
|
|
29420
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 testing...` });
|
|
29421
|
+
const testResult = spawnSync2("bun", ["test"], { cwd, encoding: "utf-8", timeout: 120000 });
|
|
29422
|
+
result.testOutput = (testResult.stdout ?? "") + (testResult.stderr ?? "");
|
|
29423
|
+
if (testResult.status !== 0) {
|
|
29424
|
+
result.error = "Tests failed";
|
|
29425
|
+
log("evolve_exe: tests failed", { index: proposal.index });
|
|
29426
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29427
|
+
return result;
|
|
29428
|
+
}
|
|
29429
|
+
}
|
|
29430
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 building...` });
|
|
29431
|
+
const buildResult = spawnSync2("bun", ["run", "build"], { cwd, encoding: "utf-8", timeout: 60000 });
|
|
29432
|
+
result.buildOutput = (buildResult.stdout ?? "") + (buildResult.stderr ?? "");
|
|
29433
|
+
if (buildResult.status !== 0) {
|
|
29434
|
+
result.error = "Build failed";
|
|
29435
|
+
log("evolve_exe: build failed", { index: proposal.index });
|
|
29436
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29437
|
+
return result;
|
|
29438
|
+
}
|
|
29439
|
+
if (!skipReview) {
|
|
29440
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 reviewing...` });
|
|
29441
|
+
const diff = gitCmd(["diff", `${originalBranch}...HEAD`], cwd);
|
|
29442
|
+
const reviewResult = await runReview({
|
|
29443
|
+
ctx,
|
|
29444
|
+
internalSessions,
|
|
29445
|
+
proposal,
|
|
29446
|
+
diff: diff.stdout,
|
|
29447
|
+
testOutput: result.testOutput ?? "(tests skipped)",
|
|
29448
|
+
iterationTimeoutMs
|
|
29449
|
+
});
|
|
29450
|
+
result.reviewOutput = reviewResult.output;
|
|
29451
|
+
if (reviewResult.blocked) {
|
|
29452
|
+
log("evolve_exe: review BLOCKED", { index: proposal.index });
|
|
29453
|
+
}
|
|
29454
|
+
}
|
|
29455
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 merging...` });
|
|
29456
|
+
gitCmd(["checkout", originalBranch], cwd);
|
|
29457
|
+
const mergeResult = gitCmd(["merge", "--no-ff", branchName, "-m", `evolve: ${proposal.title}`], cwd);
|
|
29458
|
+
if (!mergeResult.ok) {
|
|
29459
|
+
result.error = `Merge failed: ${mergeResult.stderr}`;
|
|
29460
|
+
gitCmd(["merge", "--abort"], cwd);
|
|
29461
|
+
gitCmd(["branch", "-D", branchName], cwd);
|
|
29462
|
+
return result;
|
|
29463
|
+
}
|
|
29464
|
+
gitCmd(["branch", "-d", branchName], cwd);
|
|
29465
|
+
result.status = "success";
|
|
29466
|
+
log(`evolve_exe: #${proposal.index} "${proposal.title}" \u2014 SUCCESS`);
|
|
29467
|
+
} catch (err) {
|
|
29468
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
29469
|
+
log("evolve_exe: unexpected error", { error: result.error });
|
|
29470
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29471
|
+
}
|
|
29472
|
+
return result;
|
|
29473
|
+
}
|
|
29474
|
+
async function rollback(cwd, originalBranch, branchName) {
|
|
29475
|
+
gitCmd(["checkout", originalBranch], cwd);
|
|
29476
|
+
gitCmd(["branch", "-D", branchName], cwd);
|
|
29477
|
+
log(`evolve_exe: rolled back branch ${branchName}`);
|
|
29478
|
+
}
|
|
29479
|
+
async function runImplementation(args) {
|
|
29480
|
+
const { ctx, internalSessions, proposal, projectInfo, maxIterations, iterationTimeoutMs, toolCtx, progressLabel } = args;
|
|
29481
|
+
const sessionResp = await ctx.client.session.create({
|
|
29482
|
+
body: {},
|
|
29483
|
+
query: { directory: ctx.directory }
|
|
29484
|
+
});
|
|
29485
|
+
const sessionID = sessionResp.data?.id;
|
|
29486
|
+
if (!sessionID)
|
|
29487
|
+
return { ok: false, error: "Failed to create implementation session" };
|
|
29488
|
+
internalSessions.add(sessionID);
|
|
29489
|
+
const initialPrompt = IMPLEMENT_PROMPT(proposal, projectInfo);
|
|
29490
|
+
try {
|
|
29491
|
+
for (let i = 0;i < maxIterations; i++) {
|
|
29492
|
+
toolCtx.metadata({
|
|
29493
|
+
title: `${progressLabel} #${proposal.index}: implementing [${i + 1}/${maxIterations}]`
|
|
29494
|
+
});
|
|
29495
|
+
const prompt = i === 0 ? initialPrompt : buildContinuationPrompt2(initialPrompt, i + 1);
|
|
29496
|
+
try {
|
|
29497
|
+
await withTimeout3(ctx.client.session.prompt({
|
|
29498
|
+
path: { id: sessionID },
|
|
29499
|
+
body: {
|
|
29500
|
+
parts: [{ type: "text", text: prompt }],
|
|
29501
|
+
agent: "hephaestus"
|
|
29502
|
+
},
|
|
29503
|
+
query: { directory: ctx.directory }
|
|
29504
|
+
}), iterationTimeoutMs, `evolve_exe implementation iteration ${i + 1}`);
|
|
29505
|
+
} catch (iterError) {
|
|
29506
|
+
const msg = iterError instanceof Error ? iterError.message : String(iterError);
|
|
29507
|
+
if (msg.startsWith("Timeout:")) {
|
|
29508
|
+
log(`evolve_exe: implementation iteration ${i + 1} timed out`);
|
|
29509
|
+
return { ok: false, error: `Implementation timed out at iteration ${i + 1}` };
|
|
29510
|
+
}
|
|
29511
|
+
throw iterError;
|
|
29512
|
+
}
|
|
29513
|
+
const messagesResp = await ctx.client.session.messages({
|
|
29514
|
+
path: { id: sessionID },
|
|
29515
|
+
query: { directory: ctx.directory }
|
|
29516
|
+
});
|
|
29517
|
+
const messages = messagesResp.data ?? [];
|
|
29518
|
+
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
29519
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
29520
|
+
`) ?? "";
|
|
29521
|
+
if (rawResult.includes(COMPLETION_MARKER2)) {
|
|
29522
|
+
log(`evolve_exe: implementation completed at iteration ${i + 1}`);
|
|
29523
|
+
return { ok: true };
|
|
29524
|
+
}
|
|
29525
|
+
log(`evolve_exe: implementation iteration ${i + 1}/${maxIterations} \u2014 no completion marker`);
|
|
29526
|
+
}
|
|
29527
|
+
return { ok: false, error: `Max iterations (${maxIterations}) reached without completion` };
|
|
29528
|
+
} finally {
|
|
29529
|
+
internalSessions.delete(sessionID);
|
|
29530
|
+
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
29531
|
+
}
|
|
29532
|
+
}
|
|
29533
|
+
async function runReview(args) {
|
|
29534
|
+
const { ctx, internalSessions, proposal, diff, testOutput, iterationTimeoutMs } = args;
|
|
29535
|
+
const sessionResp = await ctx.client.session.create({
|
|
29536
|
+
body: {},
|
|
29537
|
+
query: { directory: ctx.directory }
|
|
29538
|
+
});
|
|
29539
|
+
const sessionID = sessionResp.data?.id;
|
|
29540
|
+
if (!sessionID)
|
|
29541
|
+
return { output: "(Failed to create review session)", blocked: false };
|
|
29542
|
+
internalSessions.add(sessionID);
|
|
29543
|
+
try {
|
|
29544
|
+
const prompt = REVIEW_PROMPT(proposal, diff, testOutput);
|
|
29545
|
+
await withTimeout3(ctx.client.session.prompt({
|
|
29546
|
+
path: { id: sessionID },
|
|
29547
|
+
body: {
|
|
29548
|
+
parts: [{ type: "text", text: prompt }],
|
|
29549
|
+
agent: "momus"
|
|
29550
|
+
},
|
|
29551
|
+
query: { directory: ctx.directory }
|
|
29552
|
+
}), iterationTimeoutMs, "evolve_exe review");
|
|
29553
|
+
const messagesResp = await ctx.client.session.messages({
|
|
29554
|
+
path: { id: sessionID },
|
|
29555
|
+
query: { directory: ctx.directory }
|
|
29556
|
+
});
|
|
29557
|
+
const messages = messagesResp.data ?? [];
|
|
29558
|
+
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
29559
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
29560
|
+
`) ?? "(No review output)";
|
|
29561
|
+
const output = sanitizeSpawnResult(rawResult);
|
|
29562
|
+
const blocked = /\bBLOCK\b/i.test(output) && !/\bAPPROVE\b/i.test(output);
|
|
29563
|
+
return { output, blocked };
|
|
29564
|
+
} catch (err) {
|
|
29565
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29566
|
+
log("evolve_exe: review error", { error: msg });
|
|
29567
|
+
return { output: `(Review error: ${msg})`, blocked: false };
|
|
29568
|
+
} finally {
|
|
29569
|
+
internalSessions.delete(sessionID);
|
|
29570
|
+
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
29571
|
+
}
|
|
29572
|
+
}
|
|
29573
|
+
|
|
29574
|
+
// src/tools/evolve-publish.ts
|
|
29575
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
29576
|
+
import * as fs10 from "fs";
|
|
29577
|
+
import * as path10 from "path";
|
|
29578
|
+
function run(cmd, args, cwd, timeoutMs = 120000) {
|
|
29579
|
+
const result = spawnSync3(cmd, args, { cwd, encoding: "utf-8", timeout: timeoutMs });
|
|
29580
|
+
return {
|
|
29581
|
+
ok: result.status === 0,
|
|
29582
|
+
stdout: (result.stdout ?? "").trim(),
|
|
29583
|
+
stderr: (result.stderr ?? "").trim()
|
|
29584
|
+
};
|
|
29585
|
+
}
|
|
29586
|
+
function createEvolvePublishTool() {
|
|
29587
|
+
return tool({
|
|
29588
|
+
description: `Publish the plugin: version bump \u2192 test \u2192 build \u2192 npm publish.
|
|
29589
|
+
|
|
29590
|
+
Ensures all tests pass and build succeeds before publishing.
|
|
29591
|
+
Optionally shows SSH deploy instructions for remote hosts.
|
|
29592
|
+
|
|
29593
|
+
Examples:
|
|
29594
|
+
evolve_publish({ bump: "patch" })
|
|
29595
|
+
evolve_publish({ bump: "minor", deploy: "ssh-124" })`,
|
|
29596
|
+
args: {
|
|
29597
|
+
bump: tool.schema.enum(["patch", "minor", "major"]).optional().describe('Version bump type (default: "patch")'),
|
|
29598
|
+
deploy: tool.schema.string().optional().describe("SSH host alias for deploy instructions (e.g. ssh-124)")
|
|
29599
|
+
},
|
|
29600
|
+
execute: async (args) => {
|
|
29601
|
+
const cwd = process.cwd();
|
|
29602
|
+
const bump = args.bump ?? "patch";
|
|
29603
|
+
const lines = [];
|
|
29604
|
+
const gitStatus = run("git", ["status", "--porcelain"], cwd);
|
|
29605
|
+
if (gitStatus.ok && gitStatus.stdout.length > 0) {
|
|
29606
|
+
return `## evolve_publish: Error
|
|
29607
|
+
|
|
29608
|
+
Git working directory is not clean. Commit or stash changes before publishing.
|
|
29609
|
+
|
|
29610
|
+
\`\`\`
|
|
29611
|
+
` + gitStatus.stdout + "\n```";
|
|
29612
|
+
}
|
|
29613
|
+
lines.push("## evolve_publish");
|
|
29614
|
+
lines.push("");
|
|
29615
|
+
const testResult = run("bun", ["test"], cwd);
|
|
29616
|
+
if (!testResult.ok) {
|
|
29617
|
+
lines.push("### Tests FAILED");
|
|
29618
|
+
lines.push("```");
|
|
29619
|
+
lines.push(testResult.stdout.slice(0, 2000));
|
|
29620
|
+
lines.push(testResult.stderr.slice(0, 1000));
|
|
29621
|
+
lines.push("```");
|
|
29622
|
+
lines.push("");
|
|
29623
|
+
lines.push("Fix tests before publishing.");
|
|
29624
|
+
return lines.join(`
|
|
29625
|
+
`);
|
|
29626
|
+
}
|
|
29627
|
+
lines.push("- Tests: PASS");
|
|
29628
|
+
const buildResult = run("bun", ["run", "build"], cwd);
|
|
29629
|
+
if (!buildResult.ok) {
|
|
29630
|
+
lines.push("- Build: FAIL");
|
|
29631
|
+
lines.push("```");
|
|
29632
|
+
lines.push(buildResult.stderr.slice(0, 2000));
|
|
29633
|
+
lines.push("```");
|
|
29634
|
+
lines.push("");
|
|
29635
|
+
lines.push("Fix build errors before publishing.");
|
|
29636
|
+
return lines.join(`
|
|
29637
|
+
`);
|
|
29638
|
+
}
|
|
29639
|
+
lines.push("- Build: PASS");
|
|
29640
|
+
const bumpResult = run("npm", ["version", bump, "--no-git-tag-version"], cwd);
|
|
29641
|
+
if (!bumpResult.ok) {
|
|
29642
|
+
lines.push(`- Version bump (${bump}): FAIL \u2014 ${bumpResult.stderr}`);
|
|
29643
|
+
return lines.join(`
|
|
29644
|
+
`);
|
|
29645
|
+
}
|
|
29646
|
+
const newVersion = bumpResult.stdout.replace(/^v/, "");
|
|
29647
|
+
lines.push(`- Version bump: ${bump} \u2192 ${newVersion}`);
|
|
29648
|
+
const publishResult = run("npm", ["publish"], cwd, 180000);
|
|
29649
|
+
if (!publishResult.ok) {
|
|
29650
|
+
lines.push(`- Publish: FAIL \u2014 ${publishResult.stderr}`);
|
|
29651
|
+
lines.push("");
|
|
29652
|
+
lines.push("Note: Version was bumped locally. You may want to revert package.json if publish failed.");
|
|
29653
|
+
return lines.join(`
|
|
29654
|
+
`);
|
|
29655
|
+
}
|
|
29656
|
+
lines.push("- Publish: SUCCESS");
|
|
29657
|
+
let pkgName = "opencode-ultra";
|
|
29658
|
+
try {
|
|
29659
|
+
const pkg = JSON.parse(fs10.readFileSync(path10.join(cwd, "package.json"), "utf-8"));
|
|
29660
|
+
pkgName = pkg.name ?? pkgName;
|
|
29661
|
+
} catch {}
|
|
29662
|
+
lines.push("");
|
|
29663
|
+
lines.push(`Published **${pkgName}@${newVersion}** to npm.`);
|
|
29664
|
+
if (args.deploy) {
|
|
29665
|
+
lines.push("");
|
|
29666
|
+
lines.push("### Deploy Instructions");
|
|
29667
|
+
lines.push("");
|
|
29668
|
+
lines.push(`Run on \`${args.deploy}\`:`);
|
|
29669
|
+
lines.push("```bash");
|
|
29670
|
+
lines.push(`cd ~/.cache/opencode && bun add ${pkgName}@${newVersion}`);
|
|
29671
|
+
lines.push("```");
|
|
29672
|
+
}
|
|
29673
|
+
log("evolve_publish: success", { version: newVersion, bump });
|
|
29674
|
+
return lines.join(`
|
|
29675
|
+
`);
|
|
29676
|
+
}
|
|
29677
|
+
});
|
|
29678
|
+
}
|
|
29679
|
+
|
|
29005
29680
|
// src/hooks/todo-enforcer.ts
|
|
29006
29681
|
var DEFAULT_MAX_ENFORCEMENTS = 5;
|
|
29007
29682
|
var sessionState = new Map;
|
|
@@ -29084,7 +29759,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
|
|
|
29084
29759
|
}
|
|
29085
29760
|
|
|
29086
29761
|
// src/hooks/comment-checker.ts
|
|
29087
|
-
import * as
|
|
29762
|
+
import * as fs11 from "fs";
|
|
29088
29763
|
var AI_SLOP_PATTERNS = [
|
|
29089
29764
|
/\/\/ .{80,}/,
|
|
29090
29765
|
/\/\/ (This|The|We|Here|Note:)/i,
|
|
@@ -29163,7 +29838,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
|
|
|
29163
29838
|
if (!filePath || !isCodeFile(filePath))
|
|
29164
29839
|
return;
|
|
29165
29840
|
try {
|
|
29166
|
-
const content =
|
|
29841
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
29167
29842
|
const result = checkComments(content, filePath, maxRatio, slopThreshold);
|
|
29168
29843
|
if (result.shouldWarn) {
|
|
29169
29844
|
output.output += `
|
|
@@ -29543,6 +30218,15 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
29543
30218
|
if (!disabledTools.has("evolve_score")) {
|
|
29544
30219
|
toolRegistry.evolve_score = createEvolveScoreTool();
|
|
29545
30220
|
}
|
|
30221
|
+
if (!disabledTools.has("evolve_exe")) {
|
|
30222
|
+
toolRegistry.evolve_exe = createEvolveExeTool(ctx, internalSessions, {
|
|
30223
|
+
agentTimeoutMs: safetyConfig.agentTimeoutMs,
|
|
30224
|
+
evolveExeConfig: pluginConfig.evolve_exe
|
|
30225
|
+
});
|
|
30226
|
+
}
|
|
30227
|
+
if (!disabledTools.has("evolve_publish")) {
|
|
30228
|
+
toolRegistry.evolve_publish = createEvolvePublishTool();
|
|
30229
|
+
}
|
|
29546
30230
|
return {
|
|
29547
30231
|
tool: toolRegistry,
|
|
29548
30232
|
config: async (config3) => {
|
|
@@ -29605,6 +30289,9 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
29605
30289
|
}
|
|
29606
30290
|
pendingKeywords.set(input.sessionID, detected);
|
|
29607
30291
|
const hasEvolve = detected.some((k) => k.type === "evolve");
|
|
30292
|
+
const hasEvolveExe = detected.some((k) => k.type === "evolve-exe");
|
|
30293
|
+
const hasEvolveScan = detected.some((k) => k.type === "evolve-scan");
|
|
30294
|
+
const hasEvolvePublish = detected.some((k) => k.type === "evolve-publish");
|
|
29608
30295
|
if (hasEvolve) {
|
|
29609
30296
|
const anchor = `
|
|
29610
30297
|
|
|
@@ -29617,6 +30304,18 @@ Phase 4: SCORE \u2014 Call evolve_score({ markdown: "## Improvement: ...\\n**Pri
|
|
|
29617
30304
|
Phase 5: SAVE \u2014 Call ledger_save({ name: "evolve-scan-YYYY-MM-DD", content: "..." }).
|
|
29618
30305
|
DO NOT skip tools. Text-only output is FORBIDDEN. Every phase requires tool calls.`;
|
|
29619
30306
|
output.parts.push({ type: "text", text: anchor });
|
|
30307
|
+
} else if (hasEvolveExe) {
|
|
30308
|
+
output.parts.push({ type: "text", text: `
|
|
30309
|
+
|
|
30310
|
+
[EVOLVE EXE \u2014 CALL THE TOOL NOW. Do not explain, do not research. Call evolve_exe immediately.]` });
|
|
30311
|
+
} else if (hasEvolveScan) {
|
|
30312
|
+
output.parts.push({ type: "text", text: `
|
|
30313
|
+
|
|
30314
|
+
[EVOLVE SCAN \u2014 CALL THE TOOL NOW. Call evolve_scan({}) immediately and show results.]` });
|
|
30315
|
+
} else if (hasEvolvePublish) {
|
|
30316
|
+
output.parts.push({ type: "text", text: `
|
|
30317
|
+
|
|
30318
|
+
[EVOLVE PUBLISH \u2014 CALL THE TOOL NOW. Call evolve_publish immediately.]` });
|
|
29620
30319
|
}
|
|
29621
30320
|
if (hasUltrawork) {
|
|
29622
30321
|
for (const part of output.parts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ultra",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Lightweight OpenCode 1.2.x plugin — ultrawork mode, multi-agent orchestration, rules injection",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"@types/bun": "latest",
|
|
36
36
|
"typescript": "^5.8.0"
|
|
37
37
|
},
|
|
38
|
-
"files": [
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
39
41
|
"license": "MIT"
|
|
40
42
|
}
|