opencode-ultra 0.7.7 → 0.8.1
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/index.js +884 -3
- package/dist/tools/evolve-filter.d.ts +36 -0
- package/dist/tools/evolve-scan.d.ts +7 -0
- 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;
|
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) {
|
|
@@ -14982,6 +14989,17 @@ ledger_save({
|
|
|
14982
14989
|
})
|
|
14983
14990
|
\`\`\`
|
|
14984
14991
|
|
|
14992
|
+
## EXECUTION MODE
|
|
14993
|
+
|
|
14994
|
+
After proposals are scored and saved, the user can execute them:
|
|
14995
|
+
- \`evolve_exe({ proposals: "1,3" })\` \u2014 Implement proposals #1 and #3 autonomously
|
|
14996
|
+
- \`evolve_exe({ proposals: "all" })\` \u2014 Implement all accepted proposals
|
|
14997
|
+
- \`evolve_exe({ proposals: "1", dryRun: true })\` \u2014 Preview execution plan
|
|
14998
|
+
- \`evolve_exe({ proposals: "1,2", publish: true })\` \u2014 Implement + auto-publish
|
|
14999
|
+
|
|
15000
|
+
Each proposal runs on a git branch. Failed proposals are rolled back automatically.
|
|
15001
|
+
After implementation: \`evolve_publish({ bump: "patch" })\` to publish.
|
|
15002
|
+
|
|
14985
15003
|
## RULES
|
|
14986
15004
|
- Use the capability inventory above as ground truth.
|
|
14987
15005
|
- Do NOT use evolve_apply. Proposals only. Humans decide what to implement.
|
|
@@ -28603,6 +28621,72 @@ function formatResult(result) {
|
|
|
28603
28621
|
}
|
|
28604
28622
|
|
|
28605
28623
|
// src/tools/evolve-filter.ts
|
|
28624
|
+
var EvolveProposalSchema = exports_external.object({
|
|
28625
|
+
title: exports_external.string().min(1),
|
|
28626
|
+
priority: exports_external.enum(["P0", "P1", "P2"]),
|
|
28627
|
+
effort: exports_external.enum(["Low", "Medium", "High"]),
|
|
28628
|
+
description: exports_external.string().optional(),
|
|
28629
|
+
files: exports_external.array(exports_external.string()).optional(),
|
|
28630
|
+
currentState: exports_external.string().optional(),
|
|
28631
|
+
inspiration: exports_external.string().optional(),
|
|
28632
|
+
current_state: exports_external.string().optional(),
|
|
28633
|
+
why: exports_external.string().optional(),
|
|
28634
|
+
how: exports_external.string().optional()
|
|
28635
|
+
}).transform((v) => {
|
|
28636
|
+
const legacyDesc = [v.why, v.how].filter((s) => typeof s === "string" && s.length > 0);
|
|
28637
|
+
return {
|
|
28638
|
+
title: v.title,
|
|
28639
|
+
priority: v.priority,
|
|
28640
|
+
effort: v.effort,
|
|
28641
|
+
description: v.description ?? (legacyDesc.length > 0 ? legacyDesc.join(`
|
|
28642
|
+
`) : ""),
|
|
28643
|
+
files: v.files,
|
|
28644
|
+
currentState: v.currentState ?? v.current_state,
|
|
28645
|
+
inspiration: v.inspiration
|
|
28646
|
+
};
|
|
28647
|
+
});
|
|
28648
|
+
function validateProposalJsonl(line) {
|
|
28649
|
+
const raw = line.trim();
|
|
28650
|
+
if (!raw)
|
|
28651
|
+
return { ok: false, error: "Empty line" };
|
|
28652
|
+
let parsed;
|
|
28653
|
+
try {
|
|
28654
|
+
parsed = JSON.parse(raw);
|
|
28655
|
+
} catch (err) {
|
|
28656
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28657
|
+
return { ok: false, error: `Invalid JSON: ${msg}` };
|
|
28658
|
+
}
|
|
28659
|
+
const result = EvolveProposalSchema.safeParse(parsed);
|
|
28660
|
+
if (!result.success) {
|
|
28661
|
+
const issues = result.error.issues.map((i) => {
|
|
28662
|
+
const path8 = i.path.length > 0 ? i.path.join(".") : "(root)";
|
|
28663
|
+
return `${path8}: ${i.message}`;
|
|
28664
|
+
}).join("; ");
|
|
28665
|
+
return { ok: false, error: `Schema validation failed: ${issues}` };
|
|
28666
|
+
}
|
|
28667
|
+
return { ok: true, proposal: result.data };
|
|
28668
|
+
}
|
|
28669
|
+
function normalizeTitleForDedup(title) {
|
|
28670
|
+
return title.normalize("NFKC").toLowerCase().replace(/[\p{P}\p{S}\s]+/gu, "").trim();
|
|
28671
|
+
}
|
|
28672
|
+
function deduplicateProposals(existing, incoming) {
|
|
28673
|
+
const seen = new Set;
|
|
28674
|
+
const out = [];
|
|
28675
|
+
const add = (p) => {
|
|
28676
|
+
const key = normalizeTitleForDedup(p.title);
|
|
28677
|
+
if (key.length === 0)
|
|
28678
|
+
return;
|
|
28679
|
+
if (seen.has(key))
|
|
28680
|
+
return;
|
|
28681
|
+
seen.add(key);
|
|
28682
|
+
out.push(p);
|
|
28683
|
+
};
|
|
28684
|
+
for (const p of existing)
|
|
28685
|
+
add(p);
|
|
28686
|
+
for (const p of incoming)
|
|
28687
|
+
add(p);
|
|
28688
|
+
return out;
|
|
28689
|
+
}
|
|
28606
28690
|
var PRIORITY_WEIGHT = {
|
|
28607
28691
|
P0: 10,
|
|
28608
28692
|
P1: 5,
|
|
@@ -28756,6 +28840,791 @@ evolve_score({ markdown: "## Improvement: Rate Limiting\\n**Priority**: P0\\n**E
|
|
|
28756
28840
|
});
|
|
28757
28841
|
}
|
|
28758
28842
|
|
|
28843
|
+
// src/tools/evolve-scan.ts
|
|
28844
|
+
import * as fs8 from "fs";
|
|
28845
|
+
import * as path8 from "path";
|
|
28846
|
+
function splitJsonl(text) {
|
|
28847
|
+
return text.split(/\r?\n/);
|
|
28848
|
+
}
|
|
28849
|
+
function parseJsonl(text) {
|
|
28850
|
+
const proposals = [];
|
|
28851
|
+
const errors5 = [];
|
|
28852
|
+
const lines = splitJsonl(text);
|
|
28853
|
+
for (let i = 0;i < lines.length; i++) {
|
|
28854
|
+
const raw = lines[i] ?? "";
|
|
28855
|
+
if (!raw.trim())
|
|
28856
|
+
continue;
|
|
28857
|
+
const v = validateProposalJsonl(raw);
|
|
28858
|
+
if (v.ok === false) {
|
|
28859
|
+
errors5.push({ line: i + 1, raw, error: v.error });
|
|
28860
|
+
continue;
|
|
28861
|
+
}
|
|
28862
|
+
proposals.push(v.proposal);
|
|
28863
|
+
}
|
|
28864
|
+
return { proposals, errors: errors5 };
|
|
28865
|
+
}
|
|
28866
|
+
function proposalToJson(p) {
|
|
28867
|
+
const out = {
|
|
28868
|
+
title: p.title,
|
|
28869
|
+
priority: p.priority,
|
|
28870
|
+
effort: p.effort,
|
|
28871
|
+
description: p.description
|
|
28872
|
+
};
|
|
28873
|
+
if (p.files && p.files.length > 0)
|
|
28874
|
+
out.files = p.files;
|
|
28875
|
+
if (p.currentState)
|
|
28876
|
+
out.currentState = p.currentState;
|
|
28877
|
+
if (p.inspiration)
|
|
28878
|
+
out.inspiration = p.inspiration;
|
|
28879
|
+
return out;
|
|
28880
|
+
}
|
|
28881
|
+
function formatMaybeCode(s) {
|
|
28882
|
+
return s.replace(/\s+/g, " ").trim();
|
|
28883
|
+
}
|
|
28884
|
+
function markdownEscape(s) {
|
|
28885
|
+
return s.replaceAll("|", "\\|").replaceAll(`
|
|
28886
|
+
`, " ");
|
|
28887
|
+
}
|
|
28888
|
+
function buildReport(args) {
|
|
28889
|
+
const lines = [];
|
|
28890
|
+
lines.push("# Evolve Scan Report");
|
|
28891
|
+
lines.push("");
|
|
28892
|
+
lines.push("## Metadata");
|
|
28893
|
+
lines.push(`- Generated: ${args.generatedAt}`);
|
|
28894
|
+
lines.push(`- Search query: ${args.query && args.query.trim() ? args.query.trim() : "(not provided)"}`);
|
|
28895
|
+
lines.push(`- Threshold: minScore=${args.minScore}, maxProposals=${args.maxProposals}`);
|
|
28896
|
+
lines.push(`- Proposals: existing=${args.existingCount}, incoming=${args.incomingCount}, merged=${args.mergedCount}, accepted=${args.acceptedCount}, rejected=${args.rejectedCount}`);
|
|
28897
|
+
lines.push(`- Invalid JSONL lines skipped: ${args.existingErrors.length + args.incomingErrors.length}`);
|
|
28898
|
+
lines.push("");
|
|
28899
|
+
lines.push("## Feature Matrix");
|
|
28900
|
+
lines.push("");
|
|
28901
|
+
if (args.featureMatrix && args.featureMatrix.trim()) {
|
|
28902
|
+
lines.push(args.featureMatrix.trim());
|
|
28903
|
+
} else {
|
|
28904
|
+
lines.push("_(not provided)_");
|
|
28905
|
+
}
|
|
28906
|
+
lines.push("");
|
|
28907
|
+
lines.push("## Scored Proposals");
|
|
28908
|
+
lines.push("");
|
|
28909
|
+
lines.push(`### Accepted (${args.accepted.length})`);
|
|
28910
|
+
lines.push("");
|
|
28911
|
+
if (args.accepted.length === 0) {
|
|
28912
|
+
lines.push("_(none)_");
|
|
28913
|
+
} else {
|
|
28914
|
+
lines.push("| # | Title | Score | Priority | Effort | Description | Inspiration | Current state |");
|
|
28915
|
+
lines.push("|---:|---|---:|---|---|---|---|---|");
|
|
28916
|
+
for (const p of args.accepted) {
|
|
28917
|
+
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) : "")} |`);
|
|
28918
|
+
}
|
|
28919
|
+
}
|
|
28920
|
+
lines.push("");
|
|
28921
|
+
lines.push(`### Rejected (${args.rejected.length})`);
|
|
28922
|
+
lines.push("");
|
|
28923
|
+
if (args.rejected.length === 0) {
|
|
28924
|
+
lines.push("_(none)_");
|
|
28925
|
+
} else {
|
|
28926
|
+
lines.push("| # | Title | Score | Priority | Effort | Reason |");
|
|
28927
|
+
lines.push("|---:|---|---:|---|---|---|");
|
|
28928
|
+
for (const p of args.rejected) {
|
|
28929
|
+
lines.push(`| #${p.index} | ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(p.reason ? formatMaybeCode(p.reason) : "")} |`);
|
|
28930
|
+
}
|
|
28931
|
+
}
|
|
28932
|
+
lines.push("");
|
|
28933
|
+
const allErrors = [
|
|
28934
|
+
...args.existingErrors.map((e) => ({ ...e, source: "existing" })),
|
|
28935
|
+
...args.incomingErrors.map((e) => ({ ...e, source: "incoming" }))
|
|
28936
|
+
];
|
|
28937
|
+
if (allErrors.length > 0) {
|
|
28938
|
+
lines.push("## Skipped JSONL Lines");
|
|
28939
|
+
lines.push("");
|
|
28940
|
+
for (const e of allErrors) {
|
|
28941
|
+
lines.push(`- ${e.source} line ${e.line}: ${e.error}`);
|
|
28942
|
+
}
|
|
28943
|
+
lines.push("");
|
|
28944
|
+
}
|
|
28945
|
+
lines.push("## Score Reference");
|
|
28946
|
+
lines.push("");
|
|
28947
|
+
lines.push("P0+Low=30 | P0+Med=20 | P0+High=10 | P1+Low=15 | P1+Med=10 | P1+High=5 | P2+Low=3 | P2+Med=2 | P2+High=1");
|
|
28948
|
+
lines.push("");
|
|
28949
|
+
return lines.join(`
|
|
28950
|
+
`);
|
|
28951
|
+
}
|
|
28952
|
+
function createEvolveScanTool() {
|
|
28953
|
+
return tool({
|
|
28954
|
+
description: `Scan/validate evolve proposals JSONL, merge with existing proposals, score them, and generate a report.
|
|
28955
|
+
|
|
28956
|
+
Writes outputs to:
|
|
28957
|
+
- .opencode/evolve-proposals.jsonl (validated, deduplicated)
|
|
28958
|
+
- .opencode/evolve-report.md (Feature Matrix + Scored Proposals + metadata)
|
|
28959
|
+
|
|
28960
|
+
This tool only writes into .opencode/ and does not modify source code.`,
|
|
28961
|
+
args: {
|
|
28962
|
+
proposalsJsonl: tool.schema.string().optional().describe("Incoming proposals as raw JSONL text (optional; merged into existing file)"),
|
|
28963
|
+
featureMatrix: tool.schema.string().optional().describe("Feature matrix markdown (Phase 2 output) to embed into the report"),
|
|
28964
|
+
query: tool.schema.string().optional().describe("Search query used during research (for report metadata)"),
|
|
28965
|
+
minScore: tool.schema.number().optional().describe("Minimum score to accept (default: 5)"),
|
|
28966
|
+
maxProposals: tool.schema.number().optional().describe("Max proposals to accept (default: 3)")
|
|
28967
|
+
},
|
|
28968
|
+
execute: async (args) => {
|
|
28969
|
+
const generatedAt = new Date().toISOString();
|
|
28970
|
+
const minScore = args.minScore ?? 5;
|
|
28971
|
+
const maxProposals = args.maxProposals ?? 3;
|
|
28972
|
+
const outDir = path8.join(process.cwd(), ".opencode");
|
|
28973
|
+
const proposalsPath = path8.join(outDir, "evolve-proposals.jsonl");
|
|
28974
|
+
const reportPath = path8.join(outDir, "evolve-report.md");
|
|
28975
|
+
fs8.mkdirSync(outDir, { recursive: true });
|
|
28976
|
+
const existingText = fs8.existsSync(proposalsPath) ? fs8.readFileSync(proposalsPath, "utf-8") : "";
|
|
28977
|
+
const existingParsed = existingText ? parseJsonl(existingText) : { proposals: [], errors: [] };
|
|
28978
|
+
const incomingParsed = args.proposalsJsonl ? parseJsonl(args.proposalsJsonl) : { proposals: [], errors: [] };
|
|
28979
|
+
const existingDeduped = deduplicateProposals([], existingParsed.proposals);
|
|
28980
|
+
const merged = deduplicateProposals(existingDeduped, incomingParsed.proposals);
|
|
28981
|
+
const addedCount = merged.length - existingDeduped.length;
|
|
28982
|
+
const existingDupesRemoved = existingParsed.proposals.length - existingDeduped.length;
|
|
28983
|
+
const incomingDupesSkipped = incomingParsed.proposals.length - addedCount;
|
|
28984
|
+
const filtered = filterProposals(merged, { minScore, maxProposals });
|
|
28985
|
+
const titleToIndex = new Map;
|
|
28986
|
+
for (let i = 0;i < merged.length; i++) {
|
|
28987
|
+
titleToIndex.set(merged[i].title, i + 1);
|
|
28988
|
+
}
|
|
28989
|
+
const accepted = filtered.filter((p) => p.accepted).map((p) => ({ ...p, score: p.score, index: titleToIndex.get(p.title) ?? 0 }));
|
|
28990
|
+
const rejected = filtered.filter((p) => !p.accepted).map((p) => ({ ...p, score: p.score, reason: p.reason, index: titleToIndex.get(p.title) ?? 0 }));
|
|
28991
|
+
const jsonlOut = merged.map((p) => JSON.stringify(proposalToJson(p))).join(`
|
|
28992
|
+
`) + (merged.length > 0 ? `
|
|
28993
|
+
` : "");
|
|
28994
|
+
fs8.writeFileSync(proposalsPath, jsonlOut, "utf-8");
|
|
28995
|
+
const report = buildReport({
|
|
28996
|
+
generatedAt,
|
|
28997
|
+
query: args.query,
|
|
28998
|
+
minScore,
|
|
28999
|
+
maxProposals,
|
|
29000
|
+
featureMatrix: args.featureMatrix,
|
|
29001
|
+
existingCount: existingParsed.proposals.length,
|
|
29002
|
+
incomingCount: incomingParsed.proposals.length,
|
|
29003
|
+
mergedCount: merged.length,
|
|
29004
|
+
acceptedCount: accepted.length,
|
|
29005
|
+
rejectedCount: rejected.length,
|
|
29006
|
+
existingErrors: existingParsed.errors,
|
|
29007
|
+
incomingErrors: incomingParsed.errors,
|
|
29008
|
+
accepted: accepted.map((p) => ({ ...p, score: scoreProposal(p), index: p.index })),
|
|
29009
|
+
rejected: rejected.map((p) => ({ ...p, score: scoreProposal(p), index: p.index }))
|
|
29010
|
+
});
|
|
29011
|
+
fs8.writeFileSync(reportPath, report, "utf-8");
|
|
29012
|
+
const skippedInvalid = existingParsed.errors.length + incomingParsed.errors.length;
|
|
29013
|
+
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]"}`);
|
|
29014
|
+
return [
|
|
29015
|
+
"## evolve_scan complete",
|
|
29016
|
+
`- Wrote: ${proposalsPath}`,
|
|
29017
|
+
`- Wrote: ${reportPath}`,
|
|
29018
|
+
`- Existing proposals: ${existingParsed.proposals.length} (dedup removed ${existingDupesRemoved})`,
|
|
29019
|
+
`- Incoming proposals: ${incomingParsed.proposals.length} (dedup skipped ${incomingDupesSkipped}, invalid skipped ${incomingParsed.errors.length})`,
|
|
29020
|
+
`- Merged proposals: ${merged.length} (accepted ${accepted.length}, rejected ${rejected.length})`,
|
|
29021
|
+
...proposalSummary.length > 0 ? proposalSummary : [],
|
|
29022
|
+
`- Invalid JSONL lines skipped (total): ${skippedInvalid}`
|
|
29023
|
+
].join(`
|
|
29024
|
+
`);
|
|
29025
|
+
}
|
|
29026
|
+
});
|
|
29027
|
+
}
|
|
29028
|
+
|
|
29029
|
+
// src/tools/evolve-exe.ts
|
|
29030
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
29031
|
+
import * as fs9 from "fs";
|
|
29032
|
+
import * as path9 from "path";
|
|
29033
|
+
var COMPLETION_MARKER2 = "<promise>DONE</promise>";
|
|
29034
|
+
var DEFAULT_MAX_ITERATIONS2 = 10;
|
|
29035
|
+
var DEFAULT_ITERATION_TIMEOUT_MS2 = 300000;
|
|
29036
|
+
async function withTimeout3(promise3, ms, label) {
|
|
29037
|
+
let timer;
|
|
29038
|
+
const timeout = new Promise((_, reject) => {
|
|
29039
|
+
timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
29040
|
+
});
|
|
29041
|
+
try {
|
|
29042
|
+
return await Promise.race([promise3, timeout]);
|
|
29043
|
+
} finally {
|
|
29044
|
+
clearTimeout(timer);
|
|
29045
|
+
}
|
|
29046
|
+
}
|
|
29047
|
+
function readProposalsJsonl(cwd) {
|
|
29048
|
+
const jsonlPath = path9.join(cwd, ".opencode", "evolve-proposals.jsonl");
|
|
29049
|
+
if (!fs9.existsSync(jsonlPath)) {
|
|
29050
|
+
return { proposals: [], raw: [] };
|
|
29051
|
+
}
|
|
29052
|
+
const text = fs9.readFileSync(jsonlPath, "utf-8");
|
|
29053
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
|
29054
|
+
const proposals = [];
|
|
29055
|
+
const raw = [];
|
|
29056
|
+
for (const line of lines) {
|
|
29057
|
+
const v = validateProposalJsonl(line);
|
|
29058
|
+
if (v.ok) {
|
|
29059
|
+
proposals.push(v.proposal);
|
|
29060
|
+
raw.push(line);
|
|
29061
|
+
}
|
|
29062
|
+
}
|
|
29063
|
+
return { proposals, raw };
|
|
29064
|
+
}
|
|
29065
|
+
function selectProposals(proposals, selection) {
|
|
29066
|
+
const errors5 = [];
|
|
29067
|
+
if (selection.trim().toLowerCase() === "all") {
|
|
29068
|
+
const filtered = filterProposals(proposals);
|
|
29069
|
+
const accepted = filtered.filter((p) => p.accepted);
|
|
29070
|
+
return {
|
|
29071
|
+
selected: accepted.map((p) => {
|
|
29072
|
+
const idx = proposals.findIndex((orig) => orig.title === p.title);
|
|
29073
|
+
return { ...p, index: idx + 1 };
|
|
29074
|
+
}),
|
|
29075
|
+
errors: errors5
|
|
29076
|
+
};
|
|
29077
|
+
}
|
|
29078
|
+
const indices = selection.split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseInt(s, 10));
|
|
29079
|
+
const selected = [];
|
|
29080
|
+
for (const idx of indices) {
|
|
29081
|
+
if (isNaN(idx) || idx < 1 || idx > proposals.length) {
|
|
29082
|
+
errors5.push(`Invalid index: ${idx} (valid range: 1-${proposals.length})`);
|
|
29083
|
+
continue;
|
|
29084
|
+
}
|
|
29085
|
+
selected.push({ ...proposals[idx - 1], index: idx });
|
|
29086
|
+
}
|
|
29087
|
+
return { selected, errors: errors5 };
|
|
29088
|
+
}
|
|
29089
|
+
function slugify2(title) {
|
|
29090
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
|
29091
|
+
}
|
|
29092
|
+
function gitCmd(args, cwd) {
|
|
29093
|
+
const result = spawnSync2("git", args, { cwd, encoding: "utf-8", timeout: 30000 });
|
|
29094
|
+
return {
|
|
29095
|
+
ok: result.status === 0,
|
|
29096
|
+
stdout: (result.stdout ?? "").trim(),
|
|
29097
|
+
stderr: (result.stderr ?? "").trim()
|
|
29098
|
+
};
|
|
29099
|
+
}
|
|
29100
|
+
function buildProjectInfo(cwd) {
|
|
29101
|
+
try {
|
|
29102
|
+
const pkg = JSON.parse(fs9.readFileSync(path9.join(cwd, "package.json"), "utf-8"));
|
|
29103
|
+
const srcDir = path9.join(cwd, "src");
|
|
29104
|
+
const testDir = path9.join(cwd, "__test__");
|
|
29105
|
+
const srcFiles = fs9.existsSync(srcDir) ? fs9.readdirSync(srcDir, { recursive: true }).filter((f) => f.toString().endsWith(".ts")).map((f) => `src/${f}`) : [];
|
|
29106
|
+
const testFiles = fs9.existsSync(testDir) ? fs9.readdirSync(testDir, { recursive: true }).filter((f) => f.toString().endsWith(".ts")).map((f) => `__test__/${f}`) : [];
|
|
29107
|
+
return [
|
|
29108
|
+
`- **Package**: ${pkg.name}@${pkg.version}`,
|
|
29109
|
+
`- **Type**: ${pkg.type ?? "commonjs"}`,
|
|
29110
|
+
`- **Build**: ${pkg.scripts?.build ?? "(none)"}`,
|
|
29111
|
+
`- **Test**: ${pkg.scripts?.test ?? "(none)"}`,
|
|
29112
|
+
`- **Source files**: ${srcFiles.join(", ")}`,
|
|
29113
|
+
`- **Test files**: ${testFiles.join(", ")}`
|
|
29114
|
+
].join(`
|
|
29115
|
+
`);
|
|
29116
|
+
} catch {
|
|
29117
|
+
return "- (Could not read package.json)";
|
|
29118
|
+
}
|
|
29119
|
+
}
|
|
29120
|
+
var IMPLEMENT_PROMPT = (proposal, projectInfo) => `
|
|
29121
|
+
[EVOLVE IMPLEMENTATION \u2014 AUTONOMOUS MODE]
|
|
29122
|
+
|
|
29123
|
+
## Task
|
|
29124
|
+
Implement the following improvement proposal for this project:
|
|
29125
|
+
|
|
29126
|
+
**Title**: ${proposal.title}
|
|
29127
|
+
**Priority**: ${proposal.priority} | **Effort**: ${proposal.effort}
|
|
29128
|
+
**Description**: ${proposal.description}
|
|
29129
|
+
${proposal.currentState ? `**Current State**: ${proposal.currentState}` : ""}
|
|
29130
|
+
${proposal.files?.length ? `**Target Files**: ${proposal.files.join(", ")}` : ""}
|
|
29131
|
+
${proposal.inspiration ? `**Inspiration**: ${proposal.inspiration}` : ""}
|
|
29132
|
+
|
|
29133
|
+
## Project Context
|
|
29134
|
+
${projectInfo}
|
|
29135
|
+
|
|
29136
|
+
## Requirements
|
|
29137
|
+
1. Implement the feature described above
|
|
29138
|
+
2. Write unit tests in \`__test__/\` directory (same naming pattern as existing tests)
|
|
29139
|
+
3. Ensure all existing tests still pass
|
|
29140
|
+
4. Follow existing code patterns and conventions:
|
|
29141
|
+
- Use \`@opencode-ai/plugin\` tool() API for new tools
|
|
29142
|
+
- Pure functions where possible, side effects isolated
|
|
29143
|
+
- TypeScript strict mode compatible
|
|
29144
|
+
- Use zod for schema validation
|
|
29145
|
+
- Export types from the module
|
|
29146
|
+
5. Do NOT modify unrelated files
|
|
29147
|
+
6. Do NOT add unnecessary dependencies
|
|
29148
|
+
|
|
29149
|
+
## Completion
|
|
29150
|
+
When fully implemented and tested, output: ${COMPLETION_MARKER2}
|
|
29151
|
+
`;
|
|
29152
|
+
var REVIEW_PROMPT = (proposal, diff, testOutput) => `
|
|
29153
|
+
[CODE REVIEW \u2014 EVOLVE IMPLEMENTATION]
|
|
29154
|
+
|
|
29155
|
+
## Proposal Implemented
|
|
29156
|
+
**Title**: ${proposal.title}
|
|
29157
|
+
**Description**: ${proposal.description}
|
|
29158
|
+
|
|
29159
|
+
## Changes (git diff)
|
|
29160
|
+
\`\`\`diff
|
|
29161
|
+
${diff.slice(0, 8000)}
|
|
29162
|
+
\`\`\`
|
|
29163
|
+
|
|
29164
|
+
## Test Results
|
|
29165
|
+
\`\`\`
|
|
29166
|
+
${testOutput.slice(0, 2000)}
|
|
29167
|
+
\`\`\`
|
|
29168
|
+
|
|
29169
|
+
## Review Criteria
|
|
29170
|
+
1. **Correctness**: Does the implementation match the proposal?
|
|
29171
|
+
2. **Tests**: Are there adequate tests? Do they cover edge cases?
|
|
29172
|
+
3. **Code Quality**: Clean, readable, follows existing patterns?
|
|
29173
|
+
4. **Regressions**: Could this break existing functionality?
|
|
29174
|
+
5. **Security**: No injection vulnerabilities, no unsafe operations?
|
|
29175
|
+
|
|
29176
|
+
## Output Format
|
|
29177
|
+
Rate each criterion: PASS / WARN / FAIL
|
|
29178
|
+
Then overall: APPROVE / APPROVE_WITH_WARNINGS / BLOCK
|
|
29179
|
+
|
|
29180
|
+
If BLOCK: explain what must be fixed before merging.
|
|
29181
|
+
`;
|
|
29182
|
+
function buildContinuationPrompt2(original, iteration) {
|
|
29183
|
+
return `[Evolve Exe \u2014 Iteration ${iteration}]
|
|
29184
|
+
|
|
29185
|
+
IMPORTANT:
|
|
29186
|
+
- Review your progress so far
|
|
29187
|
+
- Continue from where you left off
|
|
29188
|
+
- When FULLY complete, output exactly: ${COMPLETION_MARKER2}
|
|
29189
|
+
- Do not stop until the task is truly done
|
|
29190
|
+
|
|
29191
|
+
Original task:
|
|
29192
|
+
${original}`;
|
|
29193
|
+
}
|
|
29194
|
+
function createEvolveExeTool(ctx, internalSessions, deps = {}) {
|
|
29195
|
+
const maxIterations = deps.evolveExeConfig?.maxIterations ?? DEFAULT_MAX_ITERATIONS2;
|
|
29196
|
+
const iterationTimeoutMs = deps.evolveExeConfig?.iterationTimeoutMs ?? deps.agentTimeoutMs ?? DEFAULT_ITERATION_TIMEOUT_MS2;
|
|
29197
|
+
const configSkipReview = deps.evolveExeConfig?.skipReview ?? false;
|
|
29198
|
+
const configSkipTests = deps.evolveExeConfig?.skipTests ?? false;
|
|
29199
|
+
return tool({
|
|
29200
|
+
description: `Execute evolve proposals autonomously. For each proposal:
|
|
29201
|
+
GIT BRANCH \u2192 IMPLEMENT (hephaestus) \u2192 TEST \u2192 BUILD \u2192 REVIEW (momus) \u2192 MERGE/ROLLBACK.
|
|
29202
|
+
|
|
29203
|
+
Proposals are identified by their JSONL line number (1-based) from .opencode/evolve-proposals.jsonl.
|
|
29204
|
+
Use evolve_scan first to see numbered proposals.
|
|
29205
|
+
|
|
29206
|
+
Examples:
|
|
29207
|
+
evolve_exe({ proposals: "1,3" }) \u2014 Execute proposals #1 and #3
|
|
29208
|
+
evolve_exe({ proposals: "all" }) \u2014 Execute all accepted proposals
|
|
29209
|
+
evolve_exe({ proposals: "1", dryRun: true }) \u2014 Preview execution plan only
|
|
29210
|
+
evolve_exe({ proposals: "1,2", publish: true }) \u2014 Execute + auto-publish after`,
|
|
29211
|
+
args: {
|
|
29212
|
+
proposals: tool.schema.string().describe('Proposal numbers: "1,3" or "all" (JSONL line numbers, 1-based)'),
|
|
29213
|
+
dryRun: tool.schema.boolean().optional().describe("Preview plan only, no execution (default: false)"),
|
|
29214
|
+
skipReview: tool.schema.boolean().optional().describe("Skip momus code review (default: false)"),
|
|
29215
|
+
skipTests: tool.schema.boolean().optional().describe("Skip bun test (default: false)"),
|
|
29216
|
+
publish: tool.schema.boolean().optional().describe("Run evolve_publish after all proposals succeed (default: false)")
|
|
29217
|
+
},
|
|
29218
|
+
execute: async (args, toolCtx) => {
|
|
29219
|
+
const cwd = ctx.directory;
|
|
29220
|
+
const dryRun = args.dryRun ?? false;
|
|
29221
|
+
const skipReview = args.skipReview ?? configSkipReview;
|
|
29222
|
+
const skipTests = args.skipTests ?? configSkipTests;
|
|
29223
|
+
toolCtx.metadata({ title: "evolve_exe: reading proposals..." });
|
|
29224
|
+
const { proposals } = readProposalsJsonl(cwd);
|
|
29225
|
+
if (proposals.length === 0) {
|
|
29226
|
+
return "## evolve_exe: No proposals found\n\nRun `evolve_scan` first to generate .opencode/evolve-proposals.jsonl";
|
|
29227
|
+
}
|
|
29228
|
+
const { selected, errors: errors5 } = selectProposals(proposals, args.proposals);
|
|
29229
|
+
if (errors5.length > 0 && selected.length === 0) {
|
|
29230
|
+
return `## evolve_exe: Selection errors
|
|
29231
|
+
|
|
29232
|
+
${errors5.map((e) => `- ${e}`).join(`
|
|
29233
|
+
`)}`;
|
|
29234
|
+
}
|
|
29235
|
+
if (selected.length === 0) {
|
|
29236
|
+
return `## evolve_exe: No proposals selected
|
|
29237
|
+
|
|
29238
|
+
No accepted proposals match the selection.`;
|
|
29239
|
+
}
|
|
29240
|
+
if (dryRun) {
|
|
29241
|
+
const lines2 = [
|
|
29242
|
+
"## evolve_exe: Execution Plan (DRY RUN)",
|
|
29243
|
+
"",
|
|
29244
|
+
`**Selected**: ${selected.length} proposal(s)`,
|
|
29245
|
+
`**Skip tests**: ${skipTests}`,
|
|
29246
|
+
`**Skip review**: ${skipReview}`,
|
|
29247
|
+
`**Auto-publish**: ${args.publish ?? false}`,
|
|
29248
|
+
"",
|
|
29249
|
+
"### Proposals",
|
|
29250
|
+
""
|
|
29251
|
+
];
|
|
29252
|
+
for (const p of selected) {
|
|
29253
|
+
lines2.push(`- **#${p.index}** ${p.title} (${p.priority}/${p.effort}, score=${scoreProposal(p)})`);
|
|
29254
|
+
lines2.push(` Branch: \`evolve/${slugify2(p.title)}\``);
|
|
29255
|
+
if (p.files?.length)
|
|
29256
|
+
lines2.push(` Files: ${p.files.join(", ")}`);
|
|
29257
|
+
}
|
|
29258
|
+
if (errors5.length > 0) {
|
|
29259
|
+
lines2.push("", "### Warnings", "");
|
|
29260
|
+
for (const e of errors5)
|
|
29261
|
+
lines2.push(`- ${e}`);
|
|
29262
|
+
}
|
|
29263
|
+
return lines2.join(`
|
|
29264
|
+
`);
|
|
29265
|
+
}
|
|
29266
|
+
const originalBranch = gitCmd(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
29267
|
+
if (!originalBranch.ok) {
|
|
29268
|
+
return `## evolve_exe: Error
|
|
29269
|
+
|
|
29270
|
+
Failed to determine current git branch. Is this a git repository?`;
|
|
29271
|
+
}
|
|
29272
|
+
const results = [];
|
|
29273
|
+
const projectInfo = buildProjectInfo(cwd);
|
|
29274
|
+
for (let i = 0;i < selected.length; i++) {
|
|
29275
|
+
const proposal = selected[i];
|
|
29276
|
+
const result = await executeProposal({
|
|
29277
|
+
ctx,
|
|
29278
|
+
toolCtx,
|
|
29279
|
+
internalSessions,
|
|
29280
|
+
proposal,
|
|
29281
|
+
originalBranch: originalBranch.stdout,
|
|
29282
|
+
cwd,
|
|
29283
|
+
projectInfo,
|
|
29284
|
+
skipTests,
|
|
29285
|
+
skipReview,
|
|
29286
|
+
maxIterations,
|
|
29287
|
+
iterationTimeoutMs,
|
|
29288
|
+
progressLabel: `[${i + 1}/${selected.length}]`
|
|
29289
|
+
});
|
|
29290
|
+
results.push(result);
|
|
29291
|
+
}
|
|
29292
|
+
const successCount = results.filter((r) => r.status === "success").length;
|
|
29293
|
+
const failedCount = results.filter((r) => r.status === "failed").length;
|
|
29294
|
+
const lines = [
|
|
29295
|
+
"## evolve_exe: Execution Complete",
|
|
29296
|
+
"",
|
|
29297
|
+
`**Results**: ${successCount} succeeded, ${failedCount} failed, ${results.length - successCount - failedCount} skipped`,
|
|
29298
|
+
""
|
|
29299
|
+
];
|
|
29300
|
+
for (const r of results) {
|
|
29301
|
+
const icon = r.status === "success" ? "PASS" : r.status === "failed" ? "FAIL" : "SKIP";
|
|
29302
|
+
lines.push(`### #${r.index} ${r.title} \u2014 ${icon}`);
|
|
29303
|
+
if (r.branch)
|
|
29304
|
+
lines.push(`- Branch: \`${r.branch}\``);
|
|
29305
|
+
if (r.error)
|
|
29306
|
+
lines.push(`- Error: ${r.error}`);
|
|
29307
|
+
if (r.reviewOutput)
|
|
29308
|
+
lines.push(`- Review: ${r.reviewOutput.slice(0, 500)}`);
|
|
29309
|
+
lines.push("");
|
|
29310
|
+
}
|
|
29311
|
+
if (args.publish && successCount > 0 && failedCount === 0) {
|
|
29312
|
+
lines.push('> All proposals succeeded. Use `evolve_publish({ bump: "patch" })` to publish.');
|
|
29313
|
+
}
|
|
29314
|
+
return lines.join(`
|
|
29315
|
+
`);
|
|
29316
|
+
}
|
|
29317
|
+
});
|
|
29318
|
+
}
|
|
29319
|
+
async function executeProposal(args) {
|
|
29320
|
+
const {
|
|
29321
|
+
ctx,
|
|
29322
|
+
toolCtx,
|
|
29323
|
+
internalSessions,
|
|
29324
|
+
proposal,
|
|
29325
|
+
originalBranch,
|
|
29326
|
+
cwd,
|
|
29327
|
+
projectInfo,
|
|
29328
|
+
skipTests,
|
|
29329
|
+
skipReview,
|
|
29330
|
+
maxIterations,
|
|
29331
|
+
iterationTimeoutMs,
|
|
29332
|
+
progressLabel
|
|
29333
|
+
} = args;
|
|
29334
|
+
const slug = slugify2(proposal.title);
|
|
29335
|
+
const branchName = `evolve/${slug}`;
|
|
29336
|
+
const result = {
|
|
29337
|
+
index: proposal.index,
|
|
29338
|
+
title: proposal.title,
|
|
29339
|
+
status: "failed",
|
|
29340
|
+
branch: branchName
|
|
29341
|
+
};
|
|
29342
|
+
log(`evolve_exe: starting #${proposal.index} "${proposal.title}"`);
|
|
29343
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 branching...` });
|
|
29344
|
+
const branchResult = gitCmd(["checkout", "-b", branchName], cwd);
|
|
29345
|
+
if (!branchResult.ok) {
|
|
29346
|
+
result.error = `Failed to create branch: ${branchResult.stderr}`;
|
|
29347
|
+
log("evolve_exe: branch creation failed", { error: branchResult.stderr });
|
|
29348
|
+
return result;
|
|
29349
|
+
}
|
|
29350
|
+
try {
|
|
29351
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 implementing...` });
|
|
29352
|
+
const implResult = await runImplementation({
|
|
29353
|
+
ctx,
|
|
29354
|
+
internalSessions,
|
|
29355
|
+
proposal,
|
|
29356
|
+
projectInfo,
|
|
29357
|
+
maxIterations,
|
|
29358
|
+
iterationTimeoutMs,
|
|
29359
|
+
toolCtx,
|
|
29360
|
+
progressLabel
|
|
29361
|
+
});
|
|
29362
|
+
if (!implResult.ok) {
|
|
29363
|
+
result.error = `Implementation failed: ${implResult.error}`;
|
|
29364
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29365
|
+
return result;
|
|
29366
|
+
}
|
|
29367
|
+
if (!skipTests) {
|
|
29368
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 testing...` });
|
|
29369
|
+
const testResult = spawnSync2("bun", ["test"], { cwd, encoding: "utf-8", timeout: 120000 });
|
|
29370
|
+
result.testOutput = (testResult.stdout ?? "") + (testResult.stderr ?? "");
|
|
29371
|
+
if (testResult.status !== 0) {
|
|
29372
|
+
result.error = "Tests failed";
|
|
29373
|
+
log("evolve_exe: tests failed", { index: proposal.index });
|
|
29374
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29375
|
+
return result;
|
|
29376
|
+
}
|
|
29377
|
+
}
|
|
29378
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 building...` });
|
|
29379
|
+
const buildResult = spawnSync2("bun", ["run", "build"], { cwd, encoding: "utf-8", timeout: 60000 });
|
|
29380
|
+
result.buildOutput = (buildResult.stdout ?? "") + (buildResult.stderr ?? "");
|
|
29381
|
+
if (buildResult.status !== 0) {
|
|
29382
|
+
result.error = "Build failed";
|
|
29383
|
+
log("evolve_exe: build failed", { index: proposal.index });
|
|
29384
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29385
|
+
return result;
|
|
29386
|
+
}
|
|
29387
|
+
if (!skipReview) {
|
|
29388
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 reviewing...` });
|
|
29389
|
+
const diff = gitCmd(["diff", `${originalBranch}...HEAD`], cwd);
|
|
29390
|
+
const reviewResult = await runReview({
|
|
29391
|
+
ctx,
|
|
29392
|
+
internalSessions,
|
|
29393
|
+
proposal,
|
|
29394
|
+
diff: diff.stdout,
|
|
29395
|
+
testOutput: result.testOutput ?? "(tests skipped)",
|
|
29396
|
+
iterationTimeoutMs
|
|
29397
|
+
});
|
|
29398
|
+
result.reviewOutput = reviewResult.output;
|
|
29399
|
+
if (reviewResult.blocked) {
|
|
29400
|
+
log("evolve_exe: review BLOCKED", { index: proposal.index });
|
|
29401
|
+
}
|
|
29402
|
+
}
|
|
29403
|
+
toolCtx.metadata({ title: `${progressLabel} #${proposal.index}: ${proposal.title} \u2014 merging...` });
|
|
29404
|
+
gitCmd(["checkout", originalBranch], cwd);
|
|
29405
|
+
const mergeResult = gitCmd(["merge", "--no-ff", branchName, "-m", `evolve: ${proposal.title}`], cwd);
|
|
29406
|
+
if (!mergeResult.ok) {
|
|
29407
|
+
result.error = `Merge failed: ${mergeResult.stderr}`;
|
|
29408
|
+
gitCmd(["merge", "--abort"], cwd);
|
|
29409
|
+
gitCmd(["branch", "-D", branchName], cwd);
|
|
29410
|
+
return result;
|
|
29411
|
+
}
|
|
29412
|
+
gitCmd(["branch", "-d", branchName], cwd);
|
|
29413
|
+
result.status = "success";
|
|
29414
|
+
log(`evolve_exe: #${proposal.index} "${proposal.title}" \u2014 SUCCESS`);
|
|
29415
|
+
} catch (err) {
|
|
29416
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
29417
|
+
log("evolve_exe: unexpected error", { error: result.error });
|
|
29418
|
+
await rollback(cwd, originalBranch, branchName);
|
|
29419
|
+
}
|
|
29420
|
+
return result;
|
|
29421
|
+
}
|
|
29422
|
+
async function rollback(cwd, originalBranch, branchName) {
|
|
29423
|
+
gitCmd(["checkout", originalBranch], cwd);
|
|
29424
|
+
gitCmd(["branch", "-D", branchName], cwd);
|
|
29425
|
+
log(`evolve_exe: rolled back branch ${branchName}`);
|
|
29426
|
+
}
|
|
29427
|
+
async function runImplementation(args) {
|
|
29428
|
+
const { ctx, internalSessions, proposal, projectInfo, maxIterations, iterationTimeoutMs, toolCtx, progressLabel } = args;
|
|
29429
|
+
const sessionResp = await ctx.client.session.create({
|
|
29430
|
+
body: {},
|
|
29431
|
+
query: { directory: ctx.directory }
|
|
29432
|
+
});
|
|
29433
|
+
const sessionID = sessionResp.data?.id;
|
|
29434
|
+
if (!sessionID)
|
|
29435
|
+
return { ok: false, error: "Failed to create implementation session" };
|
|
29436
|
+
internalSessions.add(sessionID);
|
|
29437
|
+
const initialPrompt = IMPLEMENT_PROMPT(proposal, projectInfo);
|
|
29438
|
+
try {
|
|
29439
|
+
for (let i = 0;i < maxIterations; i++) {
|
|
29440
|
+
toolCtx.metadata({
|
|
29441
|
+
title: `${progressLabel} #${proposal.index}: implementing [${i + 1}/${maxIterations}]`
|
|
29442
|
+
});
|
|
29443
|
+
const prompt = i === 0 ? initialPrompt : buildContinuationPrompt2(initialPrompt, i + 1);
|
|
29444
|
+
try {
|
|
29445
|
+
await withTimeout3(ctx.client.session.prompt({
|
|
29446
|
+
path: { id: sessionID },
|
|
29447
|
+
body: {
|
|
29448
|
+
parts: [{ type: "text", text: prompt }],
|
|
29449
|
+
agent: "hephaestus"
|
|
29450
|
+
},
|
|
29451
|
+
query: { directory: ctx.directory }
|
|
29452
|
+
}), iterationTimeoutMs, `evolve_exe implementation iteration ${i + 1}`);
|
|
29453
|
+
} catch (iterError) {
|
|
29454
|
+
const msg = iterError instanceof Error ? iterError.message : String(iterError);
|
|
29455
|
+
if (msg.startsWith("Timeout:")) {
|
|
29456
|
+
log(`evolve_exe: implementation iteration ${i + 1} timed out`);
|
|
29457
|
+
return { ok: false, error: `Implementation timed out at iteration ${i + 1}` };
|
|
29458
|
+
}
|
|
29459
|
+
throw iterError;
|
|
29460
|
+
}
|
|
29461
|
+
const messagesResp = await ctx.client.session.messages({
|
|
29462
|
+
path: { id: sessionID },
|
|
29463
|
+
query: { directory: ctx.directory }
|
|
29464
|
+
});
|
|
29465
|
+
const messages = messagesResp.data ?? [];
|
|
29466
|
+
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
29467
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
29468
|
+
`) ?? "";
|
|
29469
|
+
if (rawResult.includes(COMPLETION_MARKER2)) {
|
|
29470
|
+
log(`evolve_exe: implementation completed at iteration ${i + 1}`);
|
|
29471
|
+
return { ok: true };
|
|
29472
|
+
}
|
|
29473
|
+
log(`evolve_exe: implementation iteration ${i + 1}/${maxIterations} \u2014 no completion marker`);
|
|
29474
|
+
}
|
|
29475
|
+
return { ok: false, error: `Max iterations (${maxIterations}) reached without completion` };
|
|
29476
|
+
} finally {
|
|
29477
|
+
internalSessions.delete(sessionID);
|
|
29478
|
+
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
29479
|
+
}
|
|
29480
|
+
}
|
|
29481
|
+
async function runReview(args) {
|
|
29482
|
+
const { ctx, internalSessions, proposal, diff, testOutput, iterationTimeoutMs } = args;
|
|
29483
|
+
const sessionResp = await ctx.client.session.create({
|
|
29484
|
+
body: {},
|
|
29485
|
+
query: { directory: ctx.directory }
|
|
29486
|
+
});
|
|
29487
|
+
const sessionID = sessionResp.data?.id;
|
|
29488
|
+
if (!sessionID)
|
|
29489
|
+
return { output: "(Failed to create review session)", blocked: false };
|
|
29490
|
+
internalSessions.add(sessionID);
|
|
29491
|
+
try {
|
|
29492
|
+
const prompt = REVIEW_PROMPT(proposal, diff, testOutput);
|
|
29493
|
+
await withTimeout3(ctx.client.session.prompt({
|
|
29494
|
+
path: { id: sessionID },
|
|
29495
|
+
body: {
|
|
29496
|
+
parts: [{ type: "text", text: prompt }],
|
|
29497
|
+
agent: "momus"
|
|
29498
|
+
},
|
|
29499
|
+
query: { directory: ctx.directory }
|
|
29500
|
+
}), iterationTimeoutMs, "evolve_exe review");
|
|
29501
|
+
const messagesResp = await ctx.client.session.messages({
|
|
29502
|
+
path: { id: sessionID },
|
|
29503
|
+
query: { directory: ctx.directory }
|
|
29504
|
+
});
|
|
29505
|
+
const messages = messagesResp.data ?? [];
|
|
29506
|
+
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
29507
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
29508
|
+
`) ?? "(No review output)";
|
|
29509
|
+
const output = sanitizeSpawnResult(rawResult);
|
|
29510
|
+
const blocked = /\bBLOCK\b/i.test(output) && !/\bAPPROVE\b/i.test(output);
|
|
29511
|
+
return { output, blocked };
|
|
29512
|
+
} catch (err) {
|
|
29513
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29514
|
+
log("evolve_exe: review error", { error: msg });
|
|
29515
|
+
return { output: `(Review error: ${msg})`, blocked: false };
|
|
29516
|
+
} finally {
|
|
29517
|
+
internalSessions.delete(sessionID);
|
|
29518
|
+
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
29519
|
+
}
|
|
29520
|
+
}
|
|
29521
|
+
|
|
29522
|
+
// src/tools/evolve-publish.ts
|
|
29523
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
29524
|
+
import * as fs10 from "fs";
|
|
29525
|
+
import * as path10 from "path";
|
|
29526
|
+
function run(cmd, args, cwd, timeoutMs = 120000) {
|
|
29527
|
+
const result = spawnSync3(cmd, args, { cwd, encoding: "utf-8", timeout: timeoutMs });
|
|
29528
|
+
return {
|
|
29529
|
+
ok: result.status === 0,
|
|
29530
|
+
stdout: (result.stdout ?? "").trim(),
|
|
29531
|
+
stderr: (result.stderr ?? "").trim()
|
|
29532
|
+
};
|
|
29533
|
+
}
|
|
29534
|
+
function createEvolvePublishTool() {
|
|
29535
|
+
return tool({
|
|
29536
|
+
description: `Publish the plugin: version bump \u2192 test \u2192 build \u2192 npm publish.
|
|
29537
|
+
|
|
29538
|
+
Ensures all tests pass and build succeeds before publishing.
|
|
29539
|
+
Optionally shows SSH deploy instructions for remote hosts.
|
|
29540
|
+
|
|
29541
|
+
Examples:
|
|
29542
|
+
evolve_publish({ bump: "patch" })
|
|
29543
|
+
evolve_publish({ bump: "minor", deploy: "ssh-124" })`,
|
|
29544
|
+
args: {
|
|
29545
|
+
bump: tool.schema.enum(["patch", "minor", "major"]).optional().describe('Version bump type (default: "patch")'),
|
|
29546
|
+
deploy: tool.schema.string().optional().describe("SSH host alias for deploy instructions (e.g. ssh-124)")
|
|
29547
|
+
},
|
|
29548
|
+
execute: async (args) => {
|
|
29549
|
+
const cwd = process.cwd();
|
|
29550
|
+
const bump = args.bump ?? "patch";
|
|
29551
|
+
const lines = [];
|
|
29552
|
+
const gitStatus = run("git", ["status", "--porcelain"], cwd);
|
|
29553
|
+
if (gitStatus.ok && gitStatus.stdout.length > 0) {
|
|
29554
|
+
return `## evolve_publish: Error
|
|
29555
|
+
|
|
29556
|
+
Git working directory is not clean. Commit or stash changes before publishing.
|
|
29557
|
+
|
|
29558
|
+
\`\`\`
|
|
29559
|
+
` + gitStatus.stdout + "\n```";
|
|
29560
|
+
}
|
|
29561
|
+
lines.push("## evolve_publish");
|
|
29562
|
+
lines.push("");
|
|
29563
|
+
const testResult = run("bun", ["test"], cwd);
|
|
29564
|
+
if (!testResult.ok) {
|
|
29565
|
+
lines.push("### Tests FAILED");
|
|
29566
|
+
lines.push("```");
|
|
29567
|
+
lines.push(testResult.stdout.slice(0, 2000));
|
|
29568
|
+
lines.push(testResult.stderr.slice(0, 1000));
|
|
29569
|
+
lines.push("```");
|
|
29570
|
+
lines.push("");
|
|
29571
|
+
lines.push("Fix tests before publishing.");
|
|
29572
|
+
return lines.join(`
|
|
29573
|
+
`);
|
|
29574
|
+
}
|
|
29575
|
+
lines.push("- Tests: PASS");
|
|
29576
|
+
const buildResult = run("bun", ["run", "build"], cwd);
|
|
29577
|
+
if (!buildResult.ok) {
|
|
29578
|
+
lines.push("- Build: FAIL");
|
|
29579
|
+
lines.push("```");
|
|
29580
|
+
lines.push(buildResult.stderr.slice(0, 2000));
|
|
29581
|
+
lines.push("```");
|
|
29582
|
+
lines.push("");
|
|
29583
|
+
lines.push("Fix build errors before publishing.");
|
|
29584
|
+
return lines.join(`
|
|
29585
|
+
`);
|
|
29586
|
+
}
|
|
29587
|
+
lines.push("- Build: PASS");
|
|
29588
|
+
const bumpResult = run("npm", ["version", bump, "--no-git-tag-version"], cwd);
|
|
29589
|
+
if (!bumpResult.ok) {
|
|
29590
|
+
lines.push(`- Version bump (${bump}): FAIL \u2014 ${bumpResult.stderr}`);
|
|
29591
|
+
return lines.join(`
|
|
29592
|
+
`);
|
|
29593
|
+
}
|
|
29594
|
+
const newVersion = bumpResult.stdout.replace(/^v/, "");
|
|
29595
|
+
lines.push(`- Version bump: ${bump} \u2192 ${newVersion}`);
|
|
29596
|
+
const publishResult = run("npm", ["publish"], cwd, 180000);
|
|
29597
|
+
if (!publishResult.ok) {
|
|
29598
|
+
lines.push(`- Publish: FAIL \u2014 ${publishResult.stderr}`);
|
|
29599
|
+
lines.push("");
|
|
29600
|
+
lines.push("Note: Version was bumped locally. You may want to revert package.json if publish failed.");
|
|
29601
|
+
return lines.join(`
|
|
29602
|
+
`);
|
|
29603
|
+
}
|
|
29604
|
+
lines.push("- Publish: SUCCESS");
|
|
29605
|
+
let pkgName = "opencode-ultra";
|
|
29606
|
+
try {
|
|
29607
|
+
const pkg = JSON.parse(fs10.readFileSync(path10.join(cwd, "package.json"), "utf-8"));
|
|
29608
|
+
pkgName = pkg.name ?? pkgName;
|
|
29609
|
+
} catch {}
|
|
29610
|
+
lines.push("");
|
|
29611
|
+
lines.push(`Published **${pkgName}@${newVersion}** to npm.`);
|
|
29612
|
+
if (args.deploy) {
|
|
29613
|
+
lines.push("");
|
|
29614
|
+
lines.push("### Deploy Instructions");
|
|
29615
|
+
lines.push("");
|
|
29616
|
+
lines.push(`Run on \`${args.deploy}\`:`);
|
|
29617
|
+
lines.push("```bash");
|
|
29618
|
+
lines.push(`cd ~/.cache/opencode && bun add ${pkgName}@${newVersion}`);
|
|
29619
|
+
lines.push("```");
|
|
29620
|
+
}
|
|
29621
|
+
log("evolve_publish: success", { version: newVersion, bump });
|
|
29622
|
+
return lines.join(`
|
|
29623
|
+
`);
|
|
29624
|
+
}
|
|
29625
|
+
});
|
|
29626
|
+
}
|
|
29627
|
+
|
|
28759
29628
|
// src/hooks/todo-enforcer.ts
|
|
28760
29629
|
var DEFAULT_MAX_ENFORCEMENTS = 5;
|
|
28761
29630
|
var sessionState = new Map;
|
|
@@ -28838,7 +29707,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
|
|
|
28838
29707
|
}
|
|
28839
29708
|
|
|
28840
29709
|
// src/hooks/comment-checker.ts
|
|
28841
|
-
import * as
|
|
29710
|
+
import * as fs11 from "fs";
|
|
28842
29711
|
var AI_SLOP_PATTERNS = [
|
|
28843
29712
|
/\/\/ .{80,}/,
|
|
28844
29713
|
/\/\/ (This|The|We|Here|Note:)/i,
|
|
@@ -28917,7 +29786,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
|
|
|
28917
29786
|
if (!filePath || !isCodeFile(filePath))
|
|
28918
29787
|
return;
|
|
28919
29788
|
try {
|
|
28920
|
-
const content =
|
|
29789
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
28921
29790
|
const result = checkComments(content, filePath, maxRatio, slopThreshold);
|
|
28922
29791
|
if (result.shouldWarn) {
|
|
28923
29792
|
output.output += `
|
|
@@ -29291,9 +30160,21 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
29291
30160
|
if (!disabledTools.has("evolve_apply")) {
|
|
29292
30161
|
toolRegistry.evolve_apply = evolveApply;
|
|
29293
30162
|
}
|
|
30163
|
+
if (!disabledTools.has("evolve_scan")) {
|
|
30164
|
+
toolRegistry.evolve_scan = createEvolveScanTool();
|
|
30165
|
+
}
|
|
29294
30166
|
if (!disabledTools.has("evolve_score")) {
|
|
29295
30167
|
toolRegistry.evolve_score = createEvolveScoreTool();
|
|
29296
30168
|
}
|
|
30169
|
+
if (!disabledTools.has("evolve_exe")) {
|
|
30170
|
+
toolRegistry.evolve_exe = createEvolveExeTool(ctx, internalSessions, {
|
|
30171
|
+
agentTimeoutMs: safetyConfig.agentTimeoutMs,
|
|
30172
|
+
evolveExeConfig: pluginConfig.evolve_exe
|
|
30173
|
+
});
|
|
30174
|
+
}
|
|
30175
|
+
if (!disabledTools.has("evolve_publish")) {
|
|
30176
|
+
toolRegistry.evolve_publish = createEvolvePublishTool();
|
|
30177
|
+
}
|
|
29297
30178
|
return {
|
|
29298
30179
|
tool: toolRegistry,
|
|
29299
30180
|
config: async (config3) => {
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* evolve-filter — Pure scoring and filtering logic for evolve proposals.
|
|
3
3
|
*
|
|
4
4
|
* No side effects. All functions are deterministic.
|
|
5
|
+
* Also exports createEvolveScoreTool() for LLM-facing tool registration.
|
|
5
6
|
*/
|
|
7
|
+
import { tool } from "@opencode-ai/plugin";
|
|
8
|
+
import { z } from "zod";
|
|
6
9
|
export interface EvolveProposal {
|
|
7
10
|
title: string;
|
|
8
11
|
priority: "P0" | "P1" | "P2";
|
|
@@ -12,6 +15,32 @@ export interface EvolveProposal {
|
|
|
12
15
|
currentState?: string;
|
|
13
16
|
inspiration?: string;
|
|
14
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* JSONL schema for evolve proposals.
|
|
20
|
+
*
|
|
21
|
+
* Accepts both canonical keys (description/currentState) and legacy keys
|
|
22
|
+
* (why/how/current_state) used by older evolve prompts.
|
|
23
|
+
*/
|
|
24
|
+
export declare const EvolveProposalSchema: z.ZodType<EvolveProposal>;
|
|
25
|
+
export type ProposalJsonlValidation = {
|
|
26
|
+
ok: true;
|
|
27
|
+
proposal: EvolveProposal;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
error: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Parse and validate a single JSONL line.
|
|
34
|
+
*
|
|
35
|
+
* Returns {ok:false,error} for invalid lines so callers can skip with reason.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateProposalJsonl(line: string): ProposalJsonlValidation;
|
|
38
|
+
/**
|
|
39
|
+
* Merge existing + incoming proposals, skipping duplicates by normalized title.
|
|
40
|
+
*
|
|
41
|
+
* Normalization: lowercase + punctuation/symbol/whitespace removed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function deduplicateProposals(existing: EvolveProposal[], incoming: EvolveProposal[]): EvolveProposal[];
|
|
15
44
|
export interface FilteredProposal extends EvolveProposal {
|
|
16
45
|
score: number;
|
|
17
46
|
accepted: boolean;
|
|
@@ -38,3 +67,10 @@ export declare function filterProposals(proposals: EvolveProposal[], config?: Fi
|
|
|
38
67
|
* ```
|
|
39
68
|
*/
|
|
40
69
|
export declare function parseProposalsFromMarkdown(markdown: string): EvolveProposal[];
|
|
70
|
+
/**
|
|
71
|
+
* Tool: evolve_score
|
|
72
|
+
*
|
|
73
|
+
* Takes evolve proposal markdown, parses it, scores each proposal,
|
|
74
|
+
* and returns a prioritized list. Pure computation, no agent spawning.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createEvolveScoreTool(): ReturnType<typeof tool>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ultra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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
|
}
|