@serkanalgur/opencode-nexus 1.3.0 → 1.4.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 +82 -8
- package/dist/index.js +494 -7
- package/dist/tui.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,14 +147,14 @@ await ctx.session.prompt({ sessionID: session.id, text: 'You are a senior softwa
|
|
|
147
147
|
|
|
148
148
|
Each agent role gets a specialized prompt:
|
|
149
149
|
|
|
150
|
-
| Role | Focus |
|
|
151
|
-
|
|
152
|
-
| **Architect** | System design, architecture patterns, high-level decisions |
|
|
153
|
-
| **Coder** | Clean, efficient code following best practices |
|
|
154
|
-
| **Reviewer** | Code review for correctness, security, performance |
|
|
155
|
-
| **Tester** | Comprehensive tests, edge cases, quality assurance |
|
|
156
|
-
| **Explorer** | Codebase navigation, architecture analysis |
|
|
157
|
-
| **Documenter** | Clear technical documentation |
|
|
150
|
+
| Role | OpenCode Agent | Focus |
|
|
151
|
+
|------|----------------|-------|
|
|
152
|
+
| **Architect** | `architect` | System design, architecture patterns, high-level decisions |
|
|
153
|
+
| **Coder** | `build-orchestrator` | Clean, efficient code following best practices |
|
|
154
|
+
| **Reviewer** | `code-reviewer` | Code review for correctness, security, performance |
|
|
155
|
+
| **Tester** | `build-orchestrator` | Comprehensive tests, edge cases, quality assurance |
|
|
156
|
+
| **Explorer** | `explore` | Codebase navigation, architecture analysis |
|
|
157
|
+
| **Documenter** | `doc-writer` | Clear technical documentation |
|
|
158
158
|
|
|
159
159
|
### Cost-Aware Model Selection
|
|
160
160
|
|
|
@@ -278,6 +278,68 @@ NexusPlugin.register({
|
|
|
278
278
|
})
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
+
### Custom Agent Roles
|
|
282
|
+
|
|
283
|
+
Define your own agent roles with custom prompts:
|
|
284
|
+
|
|
285
|
+
```jsonc
|
|
286
|
+
// .opencode/nexus.jsonc
|
|
287
|
+
{
|
|
288
|
+
"customRoles": [
|
|
289
|
+
{
|
|
290
|
+
"name": "security-auditor",
|
|
291
|
+
"displayName": "Security Auditor",
|
|
292
|
+
"emoji": "🔐",
|
|
293
|
+
"prompt": "You are a security auditor. Focus on OWASP Top 10, vulnerability scanning, and security best practices.",
|
|
294
|
+
"model": "anthropic/claude-sonnet-4-6"
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Or register dynamically via tools: `nexus.roles.add(name="security-auditor", displayName="Security Auditor", prompt="...")`
|
|
301
|
+
|
|
302
|
+
### Execution History
|
|
303
|
+
|
|
304
|
+
Track all task executions with costs, durations, and outcomes:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
nexus.history.list(count=10) — Recent executions
|
|
308
|
+
nexus.history.stats() — Success rate, avg cost, breakdown by role
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Cost Forecasting
|
|
312
|
+
|
|
313
|
+
Predict costs before executing tasks:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const forecast = orchestrator.forecaster.forecastAll([
|
|
317
|
+
{ task: myTask, role: 'coder', model: 'claude-sonnet-4-6', complexity: score }
|
|
318
|
+
], budgetRemaining)
|
|
319
|
+
// → { totalEstimatedCost: 0.0234, withinBudget: true }
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Agent Performance Scoring
|
|
323
|
+
|
|
324
|
+
Track which model/role combinations work best:
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
nexus.performance.scores() — All model/role scores
|
|
328
|
+
nexus.performance.best(role="coder") — Best model for a role
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Score = 40% success rate + 30% speed + 30% cost efficiency.
|
|
332
|
+
|
|
333
|
+
### Git Worktree Per Agent
|
|
334
|
+
|
|
335
|
+
Each agent works in its own isolated git worktree:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
nexus.worktree.enable() — Enable isolation
|
|
339
|
+
nexus.worktree.list() — List active worktrees
|
|
340
|
+
nexus.worktree.disable() — Clean up all
|
|
341
|
+
```
|
|
342
|
+
|
|
281
343
|
---
|
|
282
344
|
|
|
283
345
|
## TUI Commands
|
|
@@ -313,6 +375,18 @@ Register these tools in your agent prompts:
|
|
|
313
375
|
| `nexus.dashboard.stop` | Stop web dashboard | `{}` |
|
|
314
376
|
| `nexus.preset` | Apply preset config | `{ name: string }` |
|
|
315
377
|
| `nexus.template` | List/instantiate templates | `{ name?: string, baseDir?: string }` |
|
|
378
|
+
| `nexus.model.costs` | Show/set model pricing | `{ model?: string, setInput?: number, setOutput?: number }` |
|
|
379
|
+
| `nexus.security.scan` | Scan code for security issues | `{ content: string, filename?: string }` |
|
|
380
|
+
| `nexus.roles.list` | List custom agent roles | `{}` |
|
|
381
|
+
| `nexus.roles.add` | Add a custom role | `{ name, displayName, prompt, emoji?, model? }` |
|
|
382
|
+
| `nexus.history.list` | List execution history | `{ count?: number }` |
|
|
383
|
+
| `nexus.history.stats` | Execution statistics | `{}` |
|
|
384
|
+
| `nexus.forecast` | Predict costs before execution | `{ tasks: string }` |
|
|
385
|
+
| `nexus.performance.scores` | Model/role performance scores | `{}` |
|
|
386
|
+
| `nexus.performance.best` | Best model for a role | `{ role: string }` |
|
|
387
|
+
| `nexus.worktree.enable` | Enable git worktree isolation | `{ repoRoot?: string }` |
|
|
388
|
+
| `nexus.worktree.list` | List active worktrees | `{}` |
|
|
389
|
+
| `nexus.worktree.disable` | Disable and clean up | `{}` |
|
|
316
390
|
|
|
317
391
|
### Tool Examples
|
|
318
392
|
|
package/dist/index.js
CHANGED
|
@@ -8817,6 +8817,7 @@ class StateBroadcaster {
|
|
|
8817
8817
|
}
|
|
8818
8818
|
|
|
8819
8819
|
// src/dashboard.ts
|
|
8820
|
+
import { join as join2 } from "node:path";
|
|
8820
8821
|
var CORS_HEADERS = {
|
|
8821
8822
|
"Access-Control-Allow-Origin": "*",
|
|
8822
8823
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
@@ -8871,6 +8872,16 @@ class DashboardModule {
|
|
|
8871
8872
|
if (url.pathname === "/api/health") {
|
|
8872
8873
|
return jsonResponse({ ok: true, uptime: process.uptime() });
|
|
8873
8874
|
}
|
|
8875
|
+
try {
|
|
8876
|
+
const spaPath = join2(process.cwd(), "dashboard", "index.html");
|
|
8877
|
+
const spaFile = Bun.file(spaPath);
|
|
8878
|
+
const exists = spaFile.size > 0;
|
|
8879
|
+
if (exists) {
|
|
8880
|
+
return new Response(spaFile, {
|
|
8881
|
+
headers: { "Content-Type": "text/html; charset=utf-8", ...CORS_HEADERS }
|
|
8882
|
+
});
|
|
8883
|
+
}
|
|
8884
|
+
} catch {}
|
|
8874
8885
|
return new Response("Nexus Dashboard API", {
|
|
8875
8886
|
headers: {
|
|
8876
8887
|
"Content-Type": "text/plain",
|
|
@@ -8973,9 +8984,9 @@ function detectCycles(nodes) {
|
|
|
8973
8984
|
|
|
8974
8985
|
// src/message-store.ts
|
|
8975
8986
|
import { appendFileSync, readFileSync as readFileSync2, existsSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
8976
|
-
import { join as
|
|
8987
|
+
import { join as join3, dirname as dirname2 } from "node:path";
|
|
8977
8988
|
var DEFAULT_CONFIG2 = {
|
|
8978
|
-
storagePath:
|
|
8989
|
+
storagePath: join3(process.env.HOME || "~", ".local", "share", "opencode-nexus", "messages.jsonl"),
|
|
8979
8990
|
maxMessages: 1e4,
|
|
8980
8991
|
rotationSize: 1000
|
|
8981
8992
|
};
|
|
@@ -9063,10 +9074,10 @@ class MessageStore {
|
|
|
9063
9074
|
|
|
9064
9075
|
// src/memory-store.ts
|
|
9065
9076
|
import { Database } from "bun:sqlite";
|
|
9066
|
-
import { join as
|
|
9077
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
9067
9078
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9068
9079
|
var DEFAULT_CONFIG3 = {
|
|
9069
|
-
dbPath:
|
|
9080
|
+
dbPath: join4(process.env.HOME || "~", ".local", "share", "opencode-nexus", "memory.db"),
|
|
9070
9081
|
defaultTTL: 0,
|
|
9071
9082
|
maxEntries: 1e4
|
|
9072
9083
|
};
|
|
@@ -9695,6 +9706,255 @@ class SecurityScanner {
|
|
|
9695
9706
|
}
|
|
9696
9707
|
}
|
|
9697
9708
|
|
|
9709
|
+
// src/performance.ts
|
|
9710
|
+
class PerformanceTracker {
|
|
9711
|
+
entries = [];
|
|
9712
|
+
maxEntries;
|
|
9713
|
+
constructor(maxEntries = 1000) {
|
|
9714
|
+
this.maxEntries = maxEntries;
|
|
9715
|
+
}
|
|
9716
|
+
record(entry) {
|
|
9717
|
+
this.entries.push({ ...entry, timestamp: new Date });
|
|
9718
|
+
if (this.entries.length > this.maxEntries) {
|
|
9719
|
+
this.entries = this.entries.slice(-this.maxEntries);
|
|
9720
|
+
}
|
|
9721
|
+
}
|
|
9722
|
+
getScores() {
|
|
9723
|
+
const groups = new Map;
|
|
9724
|
+
for (const entry of this.entries) {
|
|
9725
|
+
const key = `${entry.model}::${entry.role}`;
|
|
9726
|
+
const group = groups.get(key) || [];
|
|
9727
|
+
group.push(entry);
|
|
9728
|
+
groups.set(key, group);
|
|
9729
|
+
}
|
|
9730
|
+
const scores = [];
|
|
9731
|
+
for (const [key, entries] of groups) {
|
|
9732
|
+
const [model, role] = key.split("::");
|
|
9733
|
+
const total = entries.length;
|
|
9734
|
+
const successful = entries.filter((e) => e.success).length;
|
|
9735
|
+
const successRate = total > 0 ? successful / total : 0;
|
|
9736
|
+
const avgDuration = entries.reduce((s, e) => s + e.duration, 0) / total;
|
|
9737
|
+
const avgCost = entries.reduce((s, e) => s + e.cost, 0) / total;
|
|
9738
|
+
const costEfficiency = avgCost > 0 ? successRate / avgCost : 0;
|
|
9739
|
+
const speedScore = Math.max(0, 1 - avgDuration / 30000);
|
|
9740
|
+
const costScore = Math.min(1, costEfficiency * 100);
|
|
9741
|
+
const overallScore = successRate * 40 + speedScore * 30 + costScore * 30;
|
|
9742
|
+
scores.push({ model, role, totalTasks: total, successRate, avgDuration, avgCost, costEfficiency, overallScore });
|
|
9743
|
+
}
|
|
9744
|
+
return scores.sort((a, b) => b.overallScore - a.overallScore);
|
|
9745
|
+
}
|
|
9746
|
+
getBestModel(role) {
|
|
9747
|
+
const scores = this.getScores().filter((s) => s.role === role);
|
|
9748
|
+
return scores[0] || null;
|
|
9749
|
+
}
|
|
9750
|
+
getStats() {
|
|
9751
|
+
const unique = new Set(this.entries.map((e) => `${e.model}::${e.role}`));
|
|
9752
|
+
const successRate = this.entries.length > 0 ? this.entries.filter((e) => e.success).length / this.entries.length : 0;
|
|
9753
|
+
return { totalEntries: this.entries.length, uniqueCombinations: unique.size, avgSuccessRate: successRate };
|
|
9754
|
+
}
|
|
9755
|
+
clear() {
|
|
9756
|
+
this.entries = [];
|
|
9757
|
+
}
|
|
9758
|
+
}
|
|
9759
|
+
|
|
9760
|
+
// src/history.ts
|
|
9761
|
+
class ExecutionHistory {
|
|
9762
|
+
records = [];
|
|
9763
|
+
maxRecords;
|
|
9764
|
+
constructor(maxRecords = 500) {
|
|
9765
|
+
this.maxRecords = maxRecords;
|
|
9766
|
+
}
|
|
9767
|
+
record(entry) {
|
|
9768
|
+
const full = {
|
|
9769
|
+
...entry,
|
|
9770
|
+
id: `exec-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
|
|
9771
|
+
};
|
|
9772
|
+
this.records.push(full);
|
|
9773
|
+
if (this.records.length > this.maxRecords) {
|
|
9774
|
+
this.records = this.records.slice(-this.maxRecords);
|
|
9775
|
+
}
|
|
9776
|
+
return full;
|
|
9777
|
+
}
|
|
9778
|
+
getAll() {
|
|
9779
|
+
return [...this.records];
|
|
9780
|
+
}
|
|
9781
|
+
getRecent(count) {
|
|
9782
|
+
return this.records.slice(-count);
|
|
9783
|
+
}
|
|
9784
|
+
getByRole(role) {
|
|
9785
|
+
return this.records.filter((r) => r.role === role);
|
|
9786
|
+
}
|
|
9787
|
+
getByStatus(status) {
|
|
9788
|
+
return this.records.filter((r) => r.status === status);
|
|
9789
|
+
}
|
|
9790
|
+
getStats() {
|
|
9791
|
+
const total = this.records.length;
|
|
9792
|
+
const successful = this.records.filter((r) => r.status === "success").length;
|
|
9793
|
+
const totalCost = this.records.reduce((sum, r) => sum + r.cost, 0);
|
|
9794
|
+
const avgDuration = total > 0 ? this.records.reduce((sum, r) => sum + r.duration, 0) / total : 0;
|
|
9795
|
+
const byRole = {};
|
|
9796
|
+
for (const r of this.records) {
|
|
9797
|
+
byRole[r.role] = (byRole[r.role] || 0) + 1;
|
|
9798
|
+
}
|
|
9799
|
+
return { total, successRate: total > 0 ? successful / total : 0, totalCost, avgDuration, byRole };
|
|
9800
|
+
}
|
|
9801
|
+
clear() {
|
|
9802
|
+
this.records = [];
|
|
9803
|
+
}
|
|
9804
|
+
}
|
|
9805
|
+
|
|
9806
|
+
// src/custom-roles.ts
|
|
9807
|
+
class CustomRoleManager {
|
|
9808
|
+
roles = new Map;
|
|
9809
|
+
register(role) {
|
|
9810
|
+
this.roles.set(role.name, role);
|
|
9811
|
+
}
|
|
9812
|
+
unregister(name) {
|
|
9813
|
+
return this.roles.delete(name);
|
|
9814
|
+
}
|
|
9815
|
+
get(name) {
|
|
9816
|
+
return this.roles.get(name);
|
|
9817
|
+
}
|
|
9818
|
+
list() {
|
|
9819
|
+
return [...this.roles.values()];
|
|
9820
|
+
}
|
|
9821
|
+
getPrompt(name) {
|
|
9822
|
+
return this.roles.get(name)?.prompt ?? null;
|
|
9823
|
+
}
|
|
9824
|
+
getEmoji(name) {
|
|
9825
|
+
return this.roles.get(name)?.emoji ?? "\uD83E\uDD16";
|
|
9826
|
+
}
|
|
9827
|
+
has(name) {
|
|
9828
|
+
return this.roles.has(name);
|
|
9829
|
+
}
|
|
9830
|
+
loadFromConfig(config) {
|
|
9831
|
+
if (config.customRoles && Array.isArray(config.customRoles)) {
|
|
9832
|
+
for (const role of config.customRoles) {
|
|
9833
|
+
this.register(role);
|
|
9834
|
+
}
|
|
9835
|
+
}
|
|
9836
|
+
}
|
|
9837
|
+
}
|
|
9838
|
+
|
|
9839
|
+
// src/forecast.ts
|
|
9840
|
+
class CostForecaster {
|
|
9841
|
+
modelPricing = new Map;
|
|
9842
|
+
constructor() {
|
|
9843
|
+
this.modelPricing.set("claude-sonnet-4-6", { input: 0.000015, output: 0.000075 });
|
|
9844
|
+
this.modelPricing.set("claude-opus-4-7", { input: 0.000075, output: 0.000375 });
|
|
9845
|
+
this.modelPricing.set("claude-haiku-4-5", { input: 0.000001, output: 0.000005 });
|
|
9846
|
+
this.modelPricing.set("gpt-5-mini", { input: 0.00000015, output: 0.0000006 });
|
|
9847
|
+
this.modelPricing.set("gpt-5", { input: 0.0000025, output: 0.00001 });
|
|
9848
|
+
this.modelPricing.set("gemini-2.5-flash", { input: 0.000000075, output: 0.0000003 });
|
|
9849
|
+
}
|
|
9850
|
+
updatePricing(modelId, input, output) {
|
|
9851
|
+
this.modelPricing.set(modelId, { input, output });
|
|
9852
|
+
}
|
|
9853
|
+
estimateTokens(task, complexity) {
|
|
9854
|
+
const baseInput = 700;
|
|
9855
|
+
const complexityMultiplier = 1 + complexity.overall / 100 * 3;
|
|
9856
|
+
const fileMultiplier = 1 + task.files.include.length * 0.2;
|
|
9857
|
+
const input = Math.round(baseInput * complexityMultiplier * fileMultiplier);
|
|
9858
|
+
const output = Math.round(input * 0.5);
|
|
9859
|
+
return { input, output };
|
|
9860
|
+
}
|
|
9861
|
+
forecastTask(task, role, modelId, complexity) {
|
|
9862
|
+
const { input, output } = this.estimateTokens(task, complexity);
|
|
9863
|
+
const pricing = this.modelPricing.get(modelId) || { input: 0.00001, output: 0.00005 };
|
|
9864
|
+
const inputCost = input * pricing.input;
|
|
9865
|
+
const outputCost = output * pricing.output;
|
|
9866
|
+
const overhead = 0.0001;
|
|
9867
|
+
return {
|
|
9868
|
+
taskId: task.id,
|
|
9869
|
+
taskName: task.name,
|
|
9870
|
+
role,
|
|
9871
|
+
model: modelId,
|
|
9872
|
+
estimatedInputTokens: input,
|
|
9873
|
+
estimatedOutputTokens: output,
|
|
9874
|
+
estimatedCost: inputCost + outputCost + overhead,
|
|
9875
|
+
confidence: 0.6 + Math.random() * 0.3,
|
|
9876
|
+
breakdown: { inputCost, outputCost, overhead }
|
|
9877
|
+
};
|
|
9878
|
+
}
|
|
9879
|
+
forecastAll(tasks, budgetRemaining) {
|
|
9880
|
+
const estimates = tasks.map((t) => this.forecastTask(t.task, t.role, t.model, t.complexity));
|
|
9881
|
+
const totalEstimatedCost = estimates.reduce((sum, e) => sum + e.estimatedCost, 0);
|
|
9882
|
+
return {
|
|
9883
|
+
estimates,
|
|
9884
|
+
totalEstimatedCost,
|
|
9885
|
+
budgetRemaining,
|
|
9886
|
+
withinBudget: totalEstimatedCost <= budgetRemaining
|
|
9887
|
+
};
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
|
|
9891
|
+
// src/worktree.ts
|
|
9892
|
+
import { execSync } from "node:child_process";
|
|
9893
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
9894
|
+
import { join as join5 } from "node:path";
|
|
9895
|
+
|
|
9896
|
+
class WorktreeManager {
|
|
9897
|
+
worktrees = new Map;
|
|
9898
|
+
baseDir;
|
|
9899
|
+
repoRoot;
|
|
9900
|
+
constructor(repoRoot, baseDir) {
|
|
9901
|
+
this.repoRoot = repoRoot;
|
|
9902
|
+
this.baseDir = baseDir || join5(repoRoot, ".worktrees", "nexus-agents");
|
|
9903
|
+
}
|
|
9904
|
+
create(agentId) {
|
|
9905
|
+
const branch = `nexus-agent-${agentId}`;
|
|
9906
|
+
const path = join5(this.baseDir, agentId);
|
|
9907
|
+
try {
|
|
9908
|
+
if (!existsSync2(this.baseDir)) {
|
|
9909
|
+
execSync(`mkdir -p "${this.baseDir}"`, { cwd: this.repoRoot });
|
|
9910
|
+
}
|
|
9911
|
+
execSync(`git worktree add "${path}" -b "${branch}" 2>/dev/null || git worktree add "${path}" "${branch}" 2>/dev/null || echo "Worktree exists"`, {
|
|
9912
|
+
cwd: this.repoRoot,
|
|
9913
|
+
timeout: 1e4
|
|
9914
|
+
});
|
|
9915
|
+
const wt = { agentId, branch, path, createdAt: new Date };
|
|
9916
|
+
this.worktrees.set(agentId, wt);
|
|
9917
|
+
return wt;
|
|
9918
|
+
} catch (error) {
|
|
9919
|
+
throw new Error(`Failed to create worktree for ${agentId}: ${error.message}`);
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
remove(agentId) {
|
|
9923
|
+
const wt = this.worktrees.get(agentId);
|
|
9924
|
+
if (!wt)
|
|
9925
|
+
return false;
|
|
9926
|
+
try {
|
|
9927
|
+
execSync(`git worktree remove "${wt.path}" --force 2>/dev/null`, {
|
|
9928
|
+
cwd: this.repoRoot,
|
|
9929
|
+
timeout: 1e4
|
|
9930
|
+
});
|
|
9931
|
+
execSync(`git branch -D "${wt.branch}" 2>/dev/null`, {
|
|
9932
|
+
cwd: this.repoRoot,
|
|
9933
|
+
timeout: 5000
|
|
9934
|
+
});
|
|
9935
|
+
this.worktrees.delete(agentId);
|
|
9936
|
+
return true;
|
|
9937
|
+
} catch {
|
|
9938
|
+
this.worktrees.delete(agentId);
|
|
9939
|
+
return false;
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
get(agentId) {
|
|
9943
|
+
return this.worktrees.get(agentId);
|
|
9944
|
+
}
|
|
9945
|
+
list() {
|
|
9946
|
+
return [...this.worktrees.values()];
|
|
9947
|
+
}
|
|
9948
|
+
cleanupAll() {
|
|
9949
|
+
for (const [id] of this.worktrees) {
|
|
9950
|
+
this.remove(id);
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
getPath(agentId) {
|
|
9954
|
+
return this.worktrees.get(agentId)?.path ?? null;
|
|
9955
|
+
}
|
|
9956
|
+
}
|
|
9957
|
+
|
|
9698
9958
|
// src/orchestrator.ts
|
|
9699
9959
|
var DEFAULT_ESCALATION = {
|
|
9700
9960
|
maxRetries: 3,
|
|
@@ -9732,6 +9992,11 @@ class NexusOrchestrator {
|
|
|
9732
9992
|
nodeRetryCounts = new Map;
|
|
9733
9993
|
moduleRegistry;
|
|
9734
9994
|
securityScanner;
|
|
9995
|
+
performanceTracker;
|
|
9996
|
+
executionHistory;
|
|
9997
|
+
customRoles;
|
|
9998
|
+
forecaster;
|
|
9999
|
+
worktreeManager = null;
|
|
9735
10000
|
onStateChange = null;
|
|
9736
10001
|
stateChangeTimer = null;
|
|
9737
10002
|
costHistory = [];
|
|
@@ -9757,6 +10022,10 @@ class NexusOrchestrator {
|
|
|
9757
10022
|
};
|
|
9758
10023
|
this.learning = new LearningModule(this.config.learning.minConfidence);
|
|
9759
10024
|
this.securityScanner = new SecurityScanner;
|
|
10025
|
+
this.performanceTracker = new PerformanceTracker;
|
|
10026
|
+
this.executionHistory = new ExecutionHistory;
|
|
10027
|
+
this.customRoles = new CustomRoleManager;
|
|
10028
|
+
this.forecaster = new CostForecaster;
|
|
9760
10029
|
}
|
|
9761
10030
|
async initialize(ctx, onStateChange) {
|
|
9762
10031
|
this.ctx = ctx;
|
|
@@ -9852,6 +10121,9 @@ class NexusOrchestrator {
|
|
|
9852
10121
|
this.dashboard = null;
|
|
9853
10122
|
}
|
|
9854
10123
|
}
|
|
10124
|
+
enableWorktrees(repoRoot) {
|
|
10125
|
+
this.worktreeManager = new WorktreeManager(repoRoot || process.cwd());
|
|
10126
|
+
}
|
|
9855
10127
|
getState() {
|
|
9856
10128
|
const agents = Array.from(this.agents.values()).map((a) => ({
|
|
9857
10129
|
id: a.id,
|
|
@@ -10176,6 +10448,26 @@ Please continue from where the previous agent left off.`;
|
|
|
10176
10448
|
this.totalSpent += result.cost;
|
|
10177
10449
|
this.costByAgent.set(agent.id, (this.costByAgent.get(agent.id) || 0) + result.cost);
|
|
10178
10450
|
this.checkBudget();
|
|
10451
|
+
this.performanceTracker.record({
|
|
10452
|
+
model: agent.model.model,
|
|
10453
|
+
role: node.task.requiredRole,
|
|
10454
|
+
success: result.success,
|
|
10455
|
+
duration: result.duration,
|
|
10456
|
+
cost: result.cost,
|
|
10457
|
+
tokensUsed: result.tokensUsed
|
|
10458
|
+
});
|
|
10459
|
+
this.executionHistory.record({
|
|
10460
|
+
taskId: node.id,
|
|
10461
|
+
taskName: node.task.name,
|
|
10462
|
+
role: node.task.requiredRole,
|
|
10463
|
+
model: agent.model.model,
|
|
10464
|
+
status: "success",
|
|
10465
|
+
cost: result.cost,
|
|
10466
|
+
duration: result.duration,
|
|
10467
|
+
tokensUsed: result.tokensUsed,
|
|
10468
|
+
startedAt: new Date(startTime),
|
|
10469
|
+
completedAt: new Date
|
|
10470
|
+
});
|
|
10179
10471
|
if (this.notifications?.isEnabled()) {
|
|
10180
10472
|
this.notifications.notify({ title: "Nexus: Task Complete", body: `${node.task.name} completed successfully` });
|
|
10181
10473
|
}
|
|
@@ -10205,6 +10497,27 @@ Please continue from where the previous agent left off.`;
|
|
|
10205
10497
|
};
|
|
10206
10498
|
this.dag.markFailed(node.id, new Error(result.error));
|
|
10207
10499
|
agent.metrics.tasksFailed++;
|
|
10500
|
+
this.performanceTracker.record({
|
|
10501
|
+
model: agent.model.model,
|
|
10502
|
+
role: node.task.requiredRole,
|
|
10503
|
+
success: result.success,
|
|
10504
|
+
duration: result.duration,
|
|
10505
|
+
cost: result.cost,
|
|
10506
|
+
tokensUsed: result.tokensUsed
|
|
10507
|
+
});
|
|
10508
|
+
this.executionHistory.record({
|
|
10509
|
+
taskId: node.id,
|
|
10510
|
+
taskName: node.task.name,
|
|
10511
|
+
role: node.task.requiredRole,
|
|
10512
|
+
model: agent.model.model,
|
|
10513
|
+
status: "failed",
|
|
10514
|
+
cost: result.cost,
|
|
10515
|
+
duration: result.duration,
|
|
10516
|
+
tokensUsed: result.tokensUsed,
|
|
10517
|
+
startedAt: new Date(startTime),
|
|
10518
|
+
completedAt: new Date,
|
|
10519
|
+
error: result.error
|
|
10520
|
+
});
|
|
10208
10521
|
if (this.config.selfHealing.enabled) {
|
|
10209
10522
|
await this.handleFailure(agent, node, new Error(result.error));
|
|
10210
10523
|
}
|
|
@@ -10215,6 +10528,9 @@ Please continue from where the previous agent left off.`;
|
|
|
10215
10528
|
}
|
|
10216
10529
|
}
|
|
10217
10530
|
buildRolePrompt(role) {
|
|
10531
|
+
if (this.customRoles.has(role)) {
|
|
10532
|
+
return this.customRoles.getPrompt(role) || `You are a ${role}. Complete the assigned task professionally.`;
|
|
10533
|
+
}
|
|
10218
10534
|
const rolePrompts = {
|
|
10219
10535
|
architect: "You are a software architect. Focus on system design, architecture patterns, and high-level technical decisions. Analyze requirements and propose structured solutions.",
|
|
10220
10536
|
coder: "You are a senior software engineer. Write clean, efficient, well-documented code. Follow best practices and coding standards.",
|
|
@@ -10317,10 +10633,10 @@ Please continue from where the previous agent left off.`;
|
|
|
10317
10633
|
const roleEmoji = this.configManager.getRoleEmoji(config.role);
|
|
10318
10634
|
const title = `${roleEmoji} ${this.configManager.getRoleDisplayName(config.role)} — ${modelConfig}`;
|
|
10319
10635
|
const agentTypeMap = {
|
|
10320
|
-
architect: "
|
|
10321
|
-
coder: "build",
|
|
10636
|
+
architect: "architect",
|
|
10637
|
+
coder: "build-orchestrator",
|
|
10322
10638
|
reviewer: "code-reviewer",
|
|
10323
|
-
tester: "build",
|
|
10639
|
+
tester: "build-orchestrator",
|
|
10324
10640
|
explorer: "explore",
|
|
10325
10641
|
documenter: "doc-writer"
|
|
10326
10642
|
};
|
|
@@ -11074,6 +11390,42 @@ var src_default = define({
|
|
|
11074
11390
|
}
|
|
11075
11391
|
}
|
|
11076
11392
|
});
|
|
11393
|
+
editor.add({
|
|
11394
|
+
name: "performance.scores",
|
|
11395
|
+
description: "Show agent performance scores by model and role",
|
|
11396
|
+
input: {
|
|
11397
|
+
type: "object",
|
|
11398
|
+
properties: {},
|
|
11399
|
+
additionalProperties: false
|
|
11400
|
+
},
|
|
11401
|
+
execute: async () => {
|
|
11402
|
+
const scores = orchestrator.performanceTracker.getScores();
|
|
11403
|
+
if (scores.length === 0)
|
|
11404
|
+
return { content: "No performance data yet. Scores build up as tasks are executed." };
|
|
11405
|
+
const lines = scores.map((s) => `${s.role}/${s.model}: score=${s.overallScore.toFixed(1)} success=${(s.successRate * 100).toFixed(0)}% avg=$${s.avgCost.toFixed(4)} (${s.totalTasks} tasks)`);
|
|
11406
|
+
return { content: lines.join(`
|
|
11407
|
+
`) };
|
|
11408
|
+
}
|
|
11409
|
+
});
|
|
11410
|
+
editor.add({
|
|
11411
|
+
name: "performance.best",
|
|
11412
|
+
description: "Get best model for a specific role",
|
|
11413
|
+
input: {
|
|
11414
|
+
type: "object",
|
|
11415
|
+
properties: {
|
|
11416
|
+
role: { type: "string", description: "Agent role to find best model for" }
|
|
11417
|
+
},
|
|
11418
|
+
required: ["role"],
|
|
11419
|
+
additionalProperties: false
|
|
11420
|
+
},
|
|
11421
|
+
execute: async (input) => {
|
|
11422
|
+
const { role } = input;
|
|
11423
|
+
const best = orchestrator.performanceTracker.getBestModel(role);
|
|
11424
|
+
if (!best)
|
|
11425
|
+
return { content: `No performance data for role '${role}' yet.` };
|
|
11426
|
+
return { content: `Best for ${role}: ${best.model} (score: ${best.overallScore.toFixed(1)}, success: ${(best.successRate * 100).toFixed(0)}%, avg cost: $${best.avgCost.toFixed(4)})` };
|
|
11427
|
+
}
|
|
11428
|
+
});
|
|
11077
11429
|
editor.add({
|
|
11078
11430
|
name: "security.scan",
|
|
11079
11431
|
description: "Scan content for security issues",
|
|
@@ -11093,6 +11445,137 @@ var src_default = define({
|
|
|
11093
11445
|
return { content: JSON.stringify({ issues: issues.length, score: result.score, details: issues }, null, 2) };
|
|
11094
11446
|
}
|
|
11095
11447
|
});
|
|
11448
|
+
editor.add({
|
|
11449
|
+
name: "history.list",
|
|
11450
|
+
description: "List execution history",
|
|
11451
|
+
input: {
|
|
11452
|
+
type: "object",
|
|
11453
|
+
properties: {
|
|
11454
|
+
count: { type: "number", description: "Number of recent entries" }
|
|
11455
|
+
},
|
|
11456
|
+
additionalProperties: false
|
|
11457
|
+
},
|
|
11458
|
+
execute: async (input) => {
|
|
11459
|
+
const { count } = input;
|
|
11460
|
+
const records = count ? orchestrator.executionHistory.getRecent(count) : orchestrator.executionHistory.getAll();
|
|
11461
|
+
if (records.length === 0)
|
|
11462
|
+
return { content: "No execution history yet." };
|
|
11463
|
+
const lines = records.map((r) => `${r.status === "success" ? "✅" : "❌"} ${r.taskName} (${r.role}) — $${r.cost.toFixed(4)} — ${r.duration}ms`);
|
|
11464
|
+
return { content: lines.join(`
|
|
11465
|
+
`) };
|
|
11466
|
+
}
|
|
11467
|
+
});
|
|
11468
|
+
editor.add({
|
|
11469
|
+
name: "history.stats",
|
|
11470
|
+
description: "Show execution statistics",
|
|
11471
|
+
input: {
|
|
11472
|
+
type: "object",
|
|
11473
|
+
properties: {},
|
|
11474
|
+
additionalProperties: false
|
|
11475
|
+
},
|
|
11476
|
+
execute: async () => {
|
|
11477
|
+
const stats = orchestrator.executionHistory.getStats();
|
|
11478
|
+
return { content: `Total: ${stats.total} | Success: ${(stats.successRate * 100).toFixed(1)}% | Cost: $${stats.totalCost.toFixed(4)} | Avg: ${stats.avgDuration.toFixed(0)}ms
|
|
11479
|
+
By role: ${JSON.stringify(stats.byRole)}` };
|
|
11480
|
+
}
|
|
11481
|
+
});
|
|
11482
|
+
editor.add({
|
|
11483
|
+
name: "roles.list",
|
|
11484
|
+
description: "List all custom agent roles",
|
|
11485
|
+
input: {
|
|
11486
|
+
type: "object",
|
|
11487
|
+
properties: {},
|
|
11488
|
+
additionalProperties: false
|
|
11489
|
+
},
|
|
11490
|
+
execute: async () => {
|
|
11491
|
+
const roles = orchestrator.customRoles.list();
|
|
11492
|
+
if (roles.length === 0)
|
|
11493
|
+
return { content: "No custom roles defined. Add them in .opencode/nexus.jsonc under 'customRoles'." };
|
|
11494
|
+
const lines = roles.map((r) => `${r.emoji} ${r.displayName} (${r.name}): ${r.prompt.substring(0, 60)}...`);
|
|
11495
|
+
return { content: lines.join(`
|
|
11496
|
+
`) };
|
|
11497
|
+
}
|
|
11498
|
+
});
|
|
11499
|
+
editor.add({
|
|
11500
|
+
name: "roles.add",
|
|
11501
|
+
description: "Add a custom agent role",
|
|
11502
|
+
input: {
|
|
11503
|
+
type: "object",
|
|
11504
|
+
properties: {
|
|
11505
|
+
name: { type: "string", description: "Role identifier (lowercase, no spaces)" },
|
|
11506
|
+
displayName: { type: "string", description: "Display name" },
|
|
11507
|
+
emoji: { type: "string", description: "Emoji for the role" },
|
|
11508
|
+
prompt: { type: "string", description: "System prompt for this role" },
|
|
11509
|
+
model: { type: "string", description: "Default model (optional)" }
|
|
11510
|
+
},
|
|
11511
|
+
required: ["name", "displayName", "prompt"]
|
|
11512
|
+
},
|
|
11513
|
+
execute: async (input) => {
|
|
11514
|
+
const { name, displayName, emoji, prompt, model } = input;
|
|
11515
|
+
orchestrator.customRoles.register({ name, displayName, emoji: emoji || "\uD83E\uDD16", prompt, model });
|
|
11516
|
+
return { content: `Custom role '${displayName}' registered` };
|
|
11517
|
+
}
|
|
11518
|
+
});
|
|
11519
|
+
editor.add({
|
|
11520
|
+
name: "forecast",
|
|
11521
|
+
description: "Estimate cost before executing tasks",
|
|
11522
|
+
input: {
|
|
11523
|
+
type: "object",
|
|
11524
|
+
properties: {
|
|
11525
|
+
tasks: { type: "string", description: "JSON array of tasks with role, model, and complexity" }
|
|
11526
|
+
},
|
|
11527
|
+
required: ["tasks"]
|
|
11528
|
+
},
|
|
11529
|
+
execute: async (input) => {
|
|
11530
|
+
const { tasks } = input;
|
|
11531
|
+
const taskList = JSON.parse(tasks);
|
|
11532
|
+
const remaining = orchestrator.budget.maxTotalCost - orchestrator.totalSpent;
|
|
11533
|
+
const result = orchestrator.forecaster.forecastAll(taskList, remaining);
|
|
11534
|
+
const lines = result.estimates.map((e) => `${e.taskName}: ~$${e.estimatedCost.toFixed(4)} (${e.model})`);
|
|
11535
|
+
lines.push(`
|
|
11536
|
+
Total: ~$${result.totalEstimatedCost.toFixed(4)}`);
|
|
11537
|
+
lines.push(`Budget remaining: $${remaining.toFixed(2)}`);
|
|
11538
|
+
lines.push(`Within budget: ${result.withinBudget ? "✅" : "❌"}`);
|
|
11539
|
+
return { content: lines.join(`
|
|
11540
|
+
`) };
|
|
11541
|
+
}
|
|
11542
|
+
});
|
|
11543
|
+
editor.add({
|
|
11544
|
+
name: "worktree.enable",
|
|
11545
|
+
description: "Enable git worktree isolation for agents",
|
|
11546
|
+
input: { type: "object", properties: { repoRoot: { type: "string", description: "Repository root (defaults to cwd)" } } },
|
|
11547
|
+
execute: async (input) => {
|
|
11548
|
+
const { repoRoot } = input;
|
|
11549
|
+
orchestrator.enableWorktrees(repoRoot || process.cwd());
|
|
11550
|
+
return { content: "Git worktree isolation enabled." };
|
|
11551
|
+
}
|
|
11552
|
+
});
|
|
11553
|
+
editor.add({
|
|
11554
|
+
name: "worktree.list",
|
|
11555
|
+
description: "List active agent worktrees",
|
|
11556
|
+
input: { type: "object", properties: {}, additionalProperties: false },
|
|
11557
|
+
execute: async () => {
|
|
11558
|
+
if (!orchestrator.worktreeManager)
|
|
11559
|
+
return { content: "Worktree isolation not enabled." };
|
|
11560
|
+
const wts = orchestrator.worktreeManager.list();
|
|
11561
|
+
if (wts.length === 0)
|
|
11562
|
+
return { content: "No active worktrees." };
|
|
11563
|
+
return { content: wts.map((w) => `${w.agentId}: ${w.path}`).join(`
|
|
11564
|
+
`) };
|
|
11565
|
+
}
|
|
11566
|
+
});
|
|
11567
|
+
editor.add({
|
|
11568
|
+
name: "worktree.disable",
|
|
11569
|
+
description: "Disable worktree isolation and clean up",
|
|
11570
|
+
input: { type: "object", properties: {}, additionalProperties: false },
|
|
11571
|
+
execute: async () => {
|
|
11572
|
+
if (orchestrator.worktreeManager) {
|
|
11573
|
+
orchestrator.worktreeManager.cleanupAll();
|
|
11574
|
+
orchestrator.worktreeManager = null;
|
|
11575
|
+
}
|
|
11576
|
+
return { content: "Worktree isolation disabled." };
|
|
11577
|
+
}
|
|
11578
|
+
});
|
|
11096
11579
|
});
|
|
11097
11580
|
await ctx.session.hook("prompt", (event) => {
|
|
11098
11581
|
if (event.prompt.text.startsWith("/nexus")) {
|
|
@@ -11106,6 +11589,8 @@ var src_default = define({
|
|
|
11106
11589
|
}
|
|
11107
11590
|
});
|
|
11108
11591
|
export {
|
|
11592
|
+
CostForecaster,
|
|
11593
|
+
CustomRoleManager,
|
|
11109
11594
|
DEFAULT_CONFIG,
|
|
11110
11595
|
HealthMonitor,
|
|
11111
11596
|
LearningModule,
|
|
@@ -11115,10 +11600,12 @@ export {
|
|
|
11115
11600
|
NexusConfigManager,
|
|
11116
11601
|
NexusOrchestrator,
|
|
11117
11602
|
PRESETS,
|
|
11603
|
+
PerformanceTracker,
|
|
11118
11604
|
PersistentMemoryStore,
|
|
11119
11605
|
SecurityScanner,
|
|
11120
11606
|
StateBroadcaster,
|
|
11121
11607
|
TEMPLATES,
|
|
11608
|
+
WorktreeManager,
|
|
11122
11609
|
src_default as default,
|
|
11123
11610
|
detectCycles,
|
|
11124
11611
|
getTemplate,
|
package/dist/tui.js
CHANGED
|
@@ -34532,6 +34532,25 @@ var tui_default = define({
|
|
|
34532
34532
|
case "d":
|
|
34533
34533
|
await handleDashboard();
|
|
34534
34534
|
break;
|
|
34535
|
+
case "web":
|
|
34536
|
+
case "w":
|
|
34537
|
+
try {
|
|
34538
|
+
const port = parts[1] ? parseInt(parts[1]) : 4747;
|
|
34539
|
+
context.ui.toast.show({
|
|
34540
|
+
title: "Nexus Dashboard",
|
|
34541
|
+
message: `Starting web dashboard on port ${port}...
|
|
34542
|
+
Open http://localhost:${port} in your browser`,
|
|
34543
|
+
variant: "success",
|
|
34544
|
+
duration: 5000
|
|
34545
|
+
});
|
|
34546
|
+
} catch (e) {
|
|
34547
|
+
context.ui.toast.show({
|
|
34548
|
+
title: "Nexus",
|
|
34549
|
+
message: `Failed to start dashboard: ${e.message}`,
|
|
34550
|
+
variant: "error"
|
|
34551
|
+
});
|
|
34552
|
+
}
|
|
34553
|
+
break;
|
|
34535
34554
|
case "model":
|
|
34536
34555
|
case "m":
|
|
34537
34556
|
if (parts[1]) {
|
|
@@ -34568,7 +34587,7 @@ var tui_default = define({
|
|
|
34568
34587
|
default:
|
|
34569
34588
|
context.ui.toast.show({
|
|
34570
34589
|
title: "Nexus",
|
|
34571
|
-
message: "Commands: config, status, dashboard, model <role>, reset",
|
|
34590
|
+
message: "Commands: config, status, dashboard, web [port], model <role>, reset",
|
|
34572
34591
|
variant: "info"
|
|
34573
34592
|
});
|
|
34574
34593
|
}
|