@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 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 join2, dirname as dirname2 } from "node:path";
8987
+ import { join as join3, dirname as dirname2 } from "node:path";
8977
8988
  var DEFAULT_CONFIG2 = {
8978
- storagePath: join2(process.env.HOME || "~", ".local", "share", "opencode-nexus", "messages.jsonl"),
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 join3, dirname as dirname3 } from "node:path";
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: join3(process.env.HOME || "~", ".local", "share", "opencode-nexus", "memory.db"),
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: "build",
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",