opencode-ultra 0.8.0 → 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 +645 -13
- 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.
|
|
@@ -28893,10 +28911,10 @@ function buildReport(args) {
|
|
|
28893
28911
|
if (args.accepted.length === 0) {
|
|
28894
28912
|
lines.push("_(none)_");
|
|
28895
28913
|
} else {
|
|
28896
|
-
lines.push("| Title | Score | Priority | Effort | Description | Inspiration | Current state |");
|
|
28897
|
-
lines.push("
|
|
28914
|
+
lines.push("| # | Title | Score | Priority | Effort | Description | Inspiration | Current state |");
|
|
28915
|
+
lines.push("|---:|---|---:|---|---|---|---|---|");
|
|
28898
28916
|
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) : "")} |`);
|
|
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) : "")} |`);
|
|
28900
28918
|
}
|
|
28901
28919
|
}
|
|
28902
28920
|
lines.push("");
|
|
@@ -28905,10 +28923,10 @@ function buildReport(args) {
|
|
|
28905
28923
|
if (args.rejected.length === 0) {
|
|
28906
28924
|
lines.push("_(none)_");
|
|
28907
28925
|
} else {
|
|
28908
|
-
lines.push("| Title | Score | Priority | Effort | Reason |");
|
|
28909
|
-
lines.push("
|
|
28926
|
+
lines.push("| # | Title | Score | Priority | Effort | Reason |");
|
|
28927
|
+
lines.push("|---:|---|---:|---|---|---|");
|
|
28910
28928
|
for (const p of args.rejected) {
|
|
28911
|
-
lines.push(`| ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(p.reason ? formatMaybeCode(p.reason) : "")} |`);
|
|
28929
|
+
lines.push(`| #${p.index} | ${markdownEscape(p.title)} | ${p.score} | ${p.priority} | ${p.effort} | ${markdownEscape(p.reason ? formatMaybeCode(p.reason) : "")} |`);
|
|
28912
28930
|
}
|
|
28913
28931
|
}
|
|
28914
28932
|
lines.push("");
|
|
@@ -28964,8 +28982,12 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
28964
28982
|
const existingDupesRemoved = existingParsed.proposals.length - existingDeduped.length;
|
|
28965
28983
|
const incomingDupesSkipped = incomingParsed.proposals.length - addedCount;
|
|
28966
28984
|
const filtered = filterProposals(merged, { minScore, maxProposals });
|
|
28967
|
-
const
|
|
28968
|
-
|
|
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 }));
|
|
28969
28991
|
const jsonlOut = merged.map((p) => JSON.stringify(proposalToJson(p))).join(`
|
|
28970
28992
|
`) + (merged.length > 0 ? `
|
|
28971
28993
|
` : "");
|
|
@@ -28983,11 +29005,12 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
28983
29005
|
rejectedCount: rejected.length,
|
|
28984
29006
|
existingErrors: existingParsed.errors,
|
|
28985
29007
|
incomingErrors: incomingParsed.errors,
|
|
28986
|
-
accepted: accepted.map((p) => ({ ...p, score: scoreProposal(p) })),
|
|
28987
|
-
rejected: rejected.map((p) => ({ ...p, score: scoreProposal(p) }))
|
|
29008
|
+
accepted: accepted.map((p) => ({ ...p, score: scoreProposal(p), index: p.index })),
|
|
29009
|
+
rejected: rejected.map((p) => ({ ...p, score: scoreProposal(p), index: p.index }))
|
|
28988
29010
|
});
|
|
28989
29011
|
fs8.writeFileSync(reportPath, report, "utf-8");
|
|
28990
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]"}`);
|
|
28991
29014
|
return [
|
|
28992
29015
|
"## evolve_scan complete",
|
|
28993
29016
|
`- Wrote: ${proposalsPath}`,
|
|
@@ -28995,6 +29018,7 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
28995
29018
|
`- Existing proposals: ${existingParsed.proposals.length} (dedup removed ${existingDupesRemoved})`,
|
|
28996
29019
|
`- Incoming proposals: ${incomingParsed.proposals.length} (dedup skipped ${incomingDupesSkipped}, invalid skipped ${incomingParsed.errors.length})`,
|
|
28997
29020
|
`- Merged proposals: ${merged.length} (accepted ${accepted.length}, rejected ${rejected.length})`,
|
|
29021
|
+
...proposalSummary.length > 0 ? proposalSummary : [],
|
|
28998
29022
|
`- Invalid JSONL lines skipped (total): ${skippedInvalid}`
|
|
28999
29023
|
].join(`
|
|
29000
29024
|
`);
|
|
@@ -29002,6 +29026,605 @@ This tool only writes into .opencode/ and does not modify source code.`,
|
|
|
29002
29026
|
});
|
|
29003
29027
|
}
|
|
29004
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
|
+
|
|
29005
29628
|
// src/hooks/todo-enforcer.ts
|
|
29006
29629
|
var DEFAULT_MAX_ENFORCEMENTS = 5;
|
|
29007
29630
|
var sessionState = new Map;
|
|
@@ -29084,7 +29707,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
|
|
|
29084
29707
|
}
|
|
29085
29708
|
|
|
29086
29709
|
// src/hooks/comment-checker.ts
|
|
29087
|
-
import * as
|
|
29710
|
+
import * as fs11 from "fs";
|
|
29088
29711
|
var AI_SLOP_PATTERNS = [
|
|
29089
29712
|
/\/\/ .{80,}/,
|
|
29090
29713
|
/\/\/ (This|The|We|Here|Note:)/i,
|
|
@@ -29163,7 +29786,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
|
|
|
29163
29786
|
if (!filePath || !isCodeFile(filePath))
|
|
29164
29787
|
return;
|
|
29165
29788
|
try {
|
|
29166
|
-
const content =
|
|
29789
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
29167
29790
|
const result = checkComments(content, filePath, maxRatio, slopThreshold);
|
|
29168
29791
|
if (result.shouldWarn) {
|
|
29169
29792
|
output.output += `
|
|
@@ -29543,6 +30166,15 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
29543
30166
|
if (!disabledTools.has("evolve_score")) {
|
|
29544
30167
|
toolRegistry.evolve_score = createEvolveScoreTool();
|
|
29545
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
|
+
}
|
|
29546
30178
|
return {
|
|
29547
30179
|
tool: toolRegistry,
|
|
29548
30180
|
config: async (config3) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ultra",
|
|
3
|
-
"version": "0.8.
|
|
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
|
}
|