@serkanalgur/opencode-nexus 1.7.0 → 1.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.
Files changed (4) hide show
  1. package/README.md +254 -330
  2. package/dist/index.js +1183 -50
  3. package/dist/tui.js +177 -12
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9962,6 +9962,116 @@ class WorktreeManager {
9962
9962
  }
9963
9963
  }
9964
9964
 
9965
+ // src/todo.ts
9966
+ class TodoEnforcer {
9967
+ items = new Map;
9968
+ maxItems;
9969
+ constructor(maxItems = 50) {
9970
+ this.maxItems = maxItems;
9971
+ }
9972
+ add(description, assignedTo) {
9973
+ if (this.items.size >= this.maxItems) {
9974
+ const completedItems = [...this.items.values()].filter((i) => i.status === "completed").sort((a, b) => (a.completedAt?.getTime() || 0) - (b.completedAt?.getTime() || 0));
9975
+ if (completedItems.length > 0) {
9976
+ this.items.delete(completedItems[0].id);
9977
+ } else {
9978
+ const oldestPending = [...this.items.values()].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
9979
+ if (oldestPending.length > 0) {
9980
+ this.items.delete(oldestPending[0].id);
9981
+ }
9982
+ }
9983
+ }
9984
+ const item = {
9985
+ id: `todo-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
9986
+ description,
9987
+ status: "pending",
9988
+ assignedTo,
9989
+ createdAt: new Date
9990
+ };
9991
+ this.items.set(item.id, item);
9992
+ return item;
9993
+ }
9994
+ start(id) {
9995
+ const item = this.items.get(id);
9996
+ if (item)
9997
+ item.status = "in_progress";
9998
+ }
9999
+ complete(id) {
10000
+ const item = this.items.get(id);
10001
+ if (item) {
10002
+ item.status = "completed";
10003
+ item.completedAt = new Date;
10004
+ }
10005
+ }
10006
+ block(id) {
10007
+ const item = this.items.get(id);
10008
+ if (item)
10009
+ item.status = "blocked";
10010
+ }
10011
+ get(id) {
10012
+ return this.items.get(id);
10013
+ }
10014
+ getPending() {
10015
+ return [...this.items.values()].filter((i) => i.status === "pending");
10016
+ }
10017
+ getInProgress() {
10018
+ return [...this.items.values()].filter((i) => i.status === "in_progress");
10019
+ }
10020
+ getCompleted() {
10021
+ return [...this.items.values()].filter((i) => i.status === "completed");
10022
+ }
10023
+ getAll() {
10024
+ return [...this.items.values()];
10025
+ }
10026
+ getStats() {
10027
+ const all = [...this.items.values()];
10028
+ return {
10029
+ total: all.length,
10030
+ pending: all.filter((i) => i.status === "pending").length,
10031
+ inProgress: all.filter((i) => i.status === "in_progress").length,
10032
+ completed: all.filter((i) => i.status === "completed").length,
10033
+ blocked: all.filter((i) => i.status === "blocked").length
10034
+ };
10035
+ }
10036
+ clear() {
10037
+ this.items.clear();
10038
+ }
10039
+ trackTask(taskId, description, role) {
10040
+ return this.add(`[Task: ${taskId}] ${description}`, role);
10041
+ }
10042
+ completeTask(taskId) {
10043
+ for (const item of this.items.values()) {
10044
+ if (item.description.startsWith(`[Task: ${taskId}]`)) {
10045
+ this.complete(item.id);
10046
+ break;
10047
+ }
10048
+ }
10049
+ }
10050
+ formatAll() {
10051
+ const all = this.getAll();
10052
+ if (all.length === 0)
10053
+ return "\uD83D\uDCCB No todo items.";
10054
+ const statusIcon = {
10055
+ pending: "⏳",
10056
+ in_progress: "\uD83D\uDD04",
10057
+ completed: "✅",
10058
+ blocked: "\uD83D\uDEAB"
10059
+ };
10060
+ const lines = all.map((item) => {
10061
+ const icon = statusIcon[item.status] || "❓";
10062
+ const assigned = item.assignedTo ? ` (${item.assignedTo})` : "";
10063
+ const time = item.completedAt ? `completed ${item.completedAt.toISOString()}` : `created ${item.createdAt.toISOString()}`;
10064
+ return ` ${icon} ${item.id}: ${item.description}${assigned} — ${time}`;
10065
+ });
10066
+ const stats = this.getStats();
10067
+ const summary = `\uD83D\uDCCB Todo: ${stats.total} total | ⏳ ${stats.pending} pending | \uD83D\uDD04 ${stats.inProgress} in progress | ✅ ${stats.completed} completed | \uD83D\uDEAB ${stats.blocked} blocked`;
10068
+ return `${summary}
10069
+
10070
+ ${lines.join(`
10071
+ `)}`;
10072
+ }
10073
+ }
10074
+
9965
10075
  // src/orchestrator.ts
9966
10076
  var DEFAULT_ESCALATION = {
9967
10077
  maxRetries: 3,
@@ -10004,6 +10114,7 @@ class NexusOrchestrator {
10004
10114
  customRoles;
10005
10115
  forecaster;
10006
10116
  worktreeManager = null;
10117
+ todoEnforcer = new TodoEnforcer;
10007
10118
  onStateChange = null;
10008
10119
  stateChangeTimer = null;
10009
10120
  costHistory = [];
@@ -10451,6 +10562,7 @@ Please continue from where the previous agent left off.`;
10451
10562
  cost: this.estimateModelCost(agent.model.model)
10452
10563
  };
10453
10564
  this.dag.markComplete(node.id, result);
10565
+ this.todoEnforcer.completeTask(agent.id);
10454
10566
  agent.metrics.tasksCompleted++;
10455
10567
  agent.metrics.totalCost += result.cost;
10456
10568
  this.totalSpent += result.cost;
@@ -10496,15 +10608,28 @@ Please continue from where the previous agent left off.`;
10496
10608
  }
10497
10609
  } catch (error) {
10498
10610
  const duration = Date.now() - startTime;
10611
+ const errorMessage = error.message || "Task failed";
10499
10612
  const result = {
10500
10613
  success: false,
10501
- error: error.message || "Task failed",
10614
+ error: errorMessage,
10502
10615
  duration,
10503
10616
  tokensUsed: 0,
10504
10617
  cost: 0
10505
10618
  };
10506
- this.dag.markFailed(node.id, new Error(result.error));
10619
+ this.dag.markFailed(node.id, new Error(errorMessage));
10620
+ this.todoEnforcer.completeTask(agent.id);
10507
10621
  agent.metrics.tasksFailed++;
10622
+ agent.status = "failed";
10623
+ this.emit("task:failed", {
10624
+ taskId: node.id,
10625
+ taskName: node.task.name,
10626
+ agentId: agent.id,
10627
+ role: node.task.requiredRole,
10628
+ model: agent.model.model,
10629
+ error: errorMessage,
10630
+ duration,
10631
+ sessionID: agent.sessionID
10632
+ });
10508
10633
  this.performanceTracker.record({
10509
10634
  model: agent.model.model,
10510
10635
  role: node.task.requiredRole,
@@ -10524,13 +10649,18 @@ Please continue from where the previous agent left off.`;
10524
10649
  tokensUsed: result.tokensUsed,
10525
10650
  startedAt: new Date(startTime),
10526
10651
  completedAt: new Date,
10527
- error: result.error
10652
+ error: errorMessage
10528
10653
  });
10654
+ if (this.notifications?.isEnabled()) {
10655
+ this.notifications.notify({ title: "Nexus: Task Failed", body: `${node.task.name} failed: ${errorMessage}`, sound: true });
10656
+ }
10529
10657
  if (this.config.selfHealing.enabled) {
10530
- await this.handleFailure(agent, node, new Error(result.error));
10658
+ await this.handleFailure(agent, node, new Error(errorMessage));
10531
10659
  }
10532
10660
  } finally {
10533
- agent.status = "idle";
10661
+ if (agent.status !== "terminated" && agent.status !== "failed") {
10662
+ agent.status = "idle";
10663
+ }
10534
10664
  node.task.status = node.status === "completed" ? "completed" : "failed";
10535
10665
  this.notifyStateChange();
10536
10666
  }
@@ -10653,7 +10783,13 @@ Please continue from where the previous agent left off.`;
10653
10783
  title,
10654
10784
  agent: agentType,
10655
10785
  model: modelName ? { providerID: provider, id: modelName } : undefined,
10656
- parentID: this.parentSessionID || undefined
10786
+ parentID: this.parentSessionID || undefined,
10787
+ metadata: {
10788
+ nexusRole: config.role,
10789
+ nexusTask: config.task?.name || "direct-spawn",
10790
+ nexusAgentId: agentId,
10791
+ nexusModel: modelConfig
10792
+ }
10657
10793
  });
10658
10794
  const agent = {
10659
10795
  id: agentId,
@@ -10680,6 +10816,8 @@ Please continue from where the previous agent left off.`;
10680
10816
  sessionID: session.id
10681
10817
  };
10682
10818
  this.agents.set(agentId, agent);
10819
+ const taskDesc = config.task?.name || `Agent ${config.role} task`;
10820
+ this.todoEnforcer.trackTask(agentId, taskDesc, config.role);
10683
10821
  this.emit("agent:spawned", agent);
10684
10822
  this.notifyStateChange();
10685
10823
  this.enforceAgentLimit();
@@ -11102,6 +11240,203 @@ function getTemplate(name) {
11102
11240
  return TEMPLATES[name];
11103
11241
  }
11104
11242
 
11243
+ // src/goal.ts
11244
+ var goalCounter = 0;
11245
+
11246
+ class GoalManager {
11247
+ goals = new Map;
11248
+ activeGoalId = null;
11249
+ set(description, autoContinue = true) {
11250
+ if (this.activeGoalId) {
11251
+ const prev = this.goals.get(this.activeGoalId);
11252
+ if (prev && prev.status === "active") {
11253
+ prev.status = "completed";
11254
+ prev.completedAt = new Date;
11255
+ }
11256
+ }
11257
+ const goal = {
11258
+ id: `goal-${Date.now()}-${++goalCounter}`,
11259
+ description,
11260
+ status: "active",
11261
+ tasks: [],
11262
+ createdAt: new Date,
11263
+ autoContinue
11264
+ };
11265
+ this.goals.set(goal.id, goal);
11266
+ this.activeGoalId = goal.id;
11267
+ return goal;
11268
+ }
11269
+ addTask(goalId, taskId) {
11270
+ const goal = this.goals.get(goalId);
11271
+ if (goal)
11272
+ goal.tasks.push(taskId);
11273
+ }
11274
+ complete(goalId) {
11275
+ const goal = this.goals.get(goalId);
11276
+ if (goal) {
11277
+ goal.status = "completed";
11278
+ goal.completedAt = new Date;
11279
+ if (this.activeGoalId === goalId) {
11280
+ this.activeGoalId = null;
11281
+ }
11282
+ }
11283
+ }
11284
+ pause(goalId) {
11285
+ const goal = this.goals.get(goalId);
11286
+ if (goal)
11287
+ goal.status = "paused";
11288
+ }
11289
+ resume(goalId) {
11290
+ const goal = this.goals.get(goalId);
11291
+ if (goal)
11292
+ goal.status = "active";
11293
+ }
11294
+ getActive() {
11295
+ if (!this.activeGoalId)
11296
+ return null;
11297
+ const goal = this.goals.get(this.activeGoalId);
11298
+ if (!goal || goal.status !== "active")
11299
+ return null;
11300
+ return goal;
11301
+ }
11302
+ getAll() {
11303
+ return [...this.goals.values()];
11304
+ }
11305
+ shouldContinue() {
11306
+ const active = this.getActive();
11307
+ return active?.autoContinue === true && active.status === "active";
11308
+ }
11309
+ get(goalId) {
11310
+ return this.goals.get(goalId);
11311
+ }
11312
+ }
11313
+
11314
+ // src/team.ts
11315
+ class TeamManager {
11316
+ teams = new Map;
11317
+ teamCounter = 0;
11318
+ memberCounter = 0;
11319
+ create(name, leadRole) {
11320
+ this.teamCounter++;
11321
+ const team = {
11322
+ id: `team-${Date.now()}-${this.teamCounter}`,
11323
+ name,
11324
+ lead: leadRole,
11325
+ members: [],
11326
+ status: "forming",
11327
+ createdAt: new Date
11328
+ };
11329
+ this.teams.set(team.id, team);
11330
+ return team;
11331
+ }
11332
+ addMember(teamId, role, model) {
11333
+ const team = this.teams.get(teamId);
11334
+ if (!team)
11335
+ return null;
11336
+ this.memberCounter++;
11337
+ const member = {
11338
+ id: `member-${Date.now()}-${this.memberCounter}`,
11339
+ role,
11340
+ status: "idle",
11341
+ model
11342
+ };
11343
+ team.members.push(member);
11344
+ return member;
11345
+ }
11346
+ activate(teamId) {
11347
+ const team = this.teams.get(teamId);
11348
+ if (team)
11349
+ team.status = "active";
11350
+ }
11351
+ complete(teamId) {
11352
+ const team = this.teams.get(teamId);
11353
+ if (team)
11354
+ team.status = "completed";
11355
+ }
11356
+ get(teamId) {
11357
+ return this.teams.get(teamId);
11358
+ }
11359
+ getAll() {
11360
+ return [...this.teams.values()];
11361
+ }
11362
+ getActive() {
11363
+ return [...this.teams.values()].find((t) => t.status === "active") || null;
11364
+ }
11365
+ }
11366
+
11367
+ // src/astgrep.ts
11368
+ import { execSync as execSync2 } from "node:child_process";
11369
+
11370
+ class AstGrep {
11371
+ _available = null;
11372
+ search(pattern, language, directory) {
11373
+ if (!this.isAvailable()) {
11374
+ return [];
11375
+ }
11376
+ try {
11377
+ const cmd = `sg run --pattern "${pattern}" --lang ${language} --json ${directory}`;
11378
+ const output = execSync2(cmd, { encoding: "utf-8", timeout: 30000 });
11379
+ const results = JSON.parse(output);
11380
+ if (!Array.isArray(results))
11381
+ return [];
11382
+ return results.map((r) => ({
11383
+ file: r.file?.path || r.file || "",
11384
+ line: r.range?.start?.line || 0,
11385
+ column: r.range?.start?.column || 0,
11386
+ match: r.text || r.source || ""
11387
+ }));
11388
+ } catch {
11389
+ return [];
11390
+ }
11391
+ }
11392
+ rewrite(pattern, rewrite, language, directory) {
11393
+ if (!this.isAvailable()) {
11394
+ return { count: 0, results: [] };
11395
+ }
11396
+ try {
11397
+ const cmd = `sg run --pattern "${pattern}" --rewrite "${rewrite}" --lang ${language} --json ${directory}`;
11398
+ const output = execSync2(cmd, { encoding: "utf-8", timeout: 30000 });
11399
+ const results = JSON.parse(output);
11400
+ if (!Array.isArray(results)) {
11401
+ return { count: 0, results: [] };
11402
+ }
11403
+ const mapped = results.map((r) => ({
11404
+ file: r.file?.path || r.file || "",
11405
+ line: r.range?.start?.line || 0,
11406
+ column: r.range?.start?.column || 0,
11407
+ match: r.text || r.source || "",
11408
+ replacement: r.replacement || undefined
11409
+ }));
11410
+ return { count: mapped.length, results: mapped };
11411
+ } catch {
11412
+ return { count: 0, results: [] };
11413
+ }
11414
+ }
11415
+ isAvailable() {
11416
+ if (this._available !== null)
11417
+ return this._available;
11418
+ try {
11419
+ execSync2("sg --version", { encoding: "utf-8", timeout: 5000 });
11420
+ this._available = true;
11421
+ } catch {
11422
+ this._available = false;
11423
+ }
11424
+ return this._available;
11425
+ }
11426
+ status() {
11427
+ const available = this.isAvailable();
11428
+ if (available) {
11429
+ try {
11430
+ const version = execSync2("sg --version", { encoding: "utf-8", timeout: 5000 }).trim();
11431
+ return `✅ ast-grep is installed (${version})`;
11432
+ } catch {
11433
+ return "✅ ast-grep is installed";
11434
+ }
11435
+ }
11436
+ return "❌ ast-grep is not installed. Install with: brew install ast-grep";
11437
+ }
11438
+ }
11439
+
11105
11440
  // src/index.ts
11106
11441
  import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3 } from "node:fs";
11107
11442
  import { join as join6 } from "node:path";
@@ -11113,18 +11448,28 @@ mode: primary
11113
11448
 
11114
11449
  # Nexus Orchestrator Agent
11115
11450
 
11116
- You are the Nexus Orchestrator — an intelligent multi-agent coordinator. You manage parallel sub-agents with cost-aware model selection, self-healing on failures, and DAG-based task execution.
11451
+ You are the Nexus Orchestrator — an intelligent multi-agent coordinator with cost awareness, performance tracking, and security scanning. You manage parallel sub-agents through a disciplined 5-phase execution model, routing tasks to optimal models, reviewing every delivery, and integrating with conflict recovery.
11452
+
11453
+ The currency of the whole orchestration is the **validated commit**: a delivery exists only as an immutable commit SHA that passed review and tests. Never treat a working tree as a delivery.
11117
11454
 
11118
- ## Core Capabilities
11455
+ Execute the five phases in order. Never skip the quality gate.
11119
11456
 
11120
- - **Spawn specialized sub-agents** for each task (architect, coder, reviewer, tester, explorer, documenter)
11121
- - **Wait for completion** — get results back from agents
11122
- - **Background execution** — run agents in background while you continue
11123
- - **Cost-aware routing** — automatically selects optimal models based on task complexity and budget
11124
- - **Self-healing** — retries failed tasks, transfers context to new agents, escalates if needed
11125
- - **Performance tracking** — learns which model/role combinations work best
11457
+ ## Model configuration
11126
11458
 
11127
- ## How to Use Nexus Tools
11459
+ Read your model pools from the first file that exists: \`.opencode/nexus.jsonc\` in the project, then \`~/.config/opencode/nexus.jsonc\`. The configuration maps complexity levels to model pools:
11460
+
11461
+ \`\`\`json
11462
+ {
11463
+ "models": {
11464
+ "complex": "provider/model-a, provider/model-b",
11465
+ "normal": "provider/model-c"
11466
+ }
11467
+ }
11468
+ ### Delegating Tasks (Recommended — spawn + wait + result)
11469
+ \`\`\`
11470
+ nexus.delegate(role="coder", task="Implement JWT auth")
11471
+ nexus.delegate(role="reviewer", task="Review the implementation", timeout=180000)
11472
+ \`\`\`
11128
11473
 
11129
11474
  ### Spawning Agents (Wait for Result)
11130
11475
  \`\`\`
@@ -11132,57 +11477,306 @@ nexus.spawn(role="coder", task="Implement JWT auth", wait=true)
11132
11477
  nexus.spawn(role="reviewer", task="Review implementation", wait=true, timeout=60000)
11133
11478
  \`\`\`
11134
11479
 
11135
- ### Spawning Agents (Parallel, No Wait)
11136
- \`\`\`
11137
- nexus.spawn(role="coder", task="Task A")
11138
- nexus.spawn(role="coder", task="Task B")
11139
- # Both run in parallel
11140
- nexus.sessions() # Check progress
11141
- nexus.background() # Move to background
11142
- \`\`\`
11480
+ - A model reference is \`provider/model\`, optionally suffixed with a variant: \`provider/model#max\`, \`provider/model#xhigh\`.
11481
+ - Each level maps to a pool of models (comma-separated).
11482
+ - \`complex\` and \`normal\` are task complexity levels for implementation, research, and verification tasks.
11483
+ - The \`reviewer\` and \`simplifier\` sub-agents always run with your own session model — no configuration needed.
11143
11484
 
11144
- ### Getting Results
11145
- \`\`\`
11146
- nexus.result(sessionID="ses_xxx") # Get output from completed agent
11147
- \`\`\`
11485
+ **Rotation rule:** when a pool contains several models, assign them round-robin across tasks. Comparison tasks are the exception: they use the whole pool at once.
11148
11486
 
11487
+ **Fallback rules:**
11488
+ - Missing \`complex\` → use \`normal\`.
11489
+ - Missing \`normal\`, or no config file → use the session default model everywhere (do not pass an explicit \`model\`).
11149
11490
  ### Other Tools
11150
11491
  \`\`\`
11151
- nexus.status(detailed=true) # Full state
11152
- nexus.costs() # Cost report
11153
- nexus.forecast(tasks='[...]') # Predict costs
11154
- nexus.performance.scores() # Performance data
11155
- nexus.history.list(count=10) # Execution history
11156
- nexus.security.scan(content="...", filename="app.ts") # Security scan
11492
+ nexus.status(detailed=true) # Full state
11493
+ nexus.costs() # Cost report
11494
+ nexus.forecast(tasks='[...]') # Predict costs
11495
+ nexus.performance.scores() # Performance data
11496
+ nexus.history.list(count=10) # Execution history
11497
+ nexus.security.scan(content="..", filename="app.ts") # Security scan
11498
+ nexus.dashboard.start(port=4747) # Start web dashboard
11499
+ nexus.dashboard.stop() # Stop web dashboard
11157
11500
  \`\`\`
11158
11501
 
11159
- ## Task Decomposition Strategy
11502
+ **Cost-aware selection:** before dispatching, use \`nexus.forecast()\` to estimate task costs. If a complex model exceeds budget thresholds, downgrade to normal. Use \`nexus.performance.best(role)\` to prefer models with proven success rates.
11160
11503
 
11504
+ ## Phase 1 — Decompose
11161
11505
  When given a development request:
11162
11506
  1. **Analyze** — Break into discrete tasks
11163
11507
  2. **Plan** — Determine parallel vs sequential
11164
11508
  3. **Estimate** — Use nexus.forecast for costs
11165
- 4. **Spawn** — Parallel tasks: spawn without wait
11166
- 5. **Spawn** — Sequential tasks: spawn with wait=true
11509
+ 4. **Delegate** — Sequential tasks: use nexus.delegate (recommended)
11510
+ 5. **Spawn** — Parallel tasks: spawn without wait
11167
11511
  6. **Monitor** — nexus.sessions() to track progress
11168
- 7. **Review** — nexus.spawn(role="reviewer", wait=true)
11512
+ 7. **Review** — nexus.delegate(role="reviewer")
11169
11513
  8. **Report** — Summarize outcomes
11170
11514
 
11515
+ 1. Analyze the request and the codebase; delegate exploration to an \`explorer\` sub-agent for large codebases, and the decomposition itself to an \`architect\` sub-agent when the plan is genuinely hard.
11516
+ 2. Each task must define:
11517
+ - \`id\`: short kebab-case slug
11518
+ - \`kind\`: \`implementation\` | \`research\` | \`verification\`
11519
+ - \`objective\`: what to build, find, or verify, with acceptance criteria
11520
+ - \`scope\`: files or directories it may touch (empty for research and verification)
11521
+ - \`complexity\`: \`complex\` or \`normal\`
11522
+ - \`compare\`: optional, \`true\` to run the task on every model of the pool and keep the best delivery
11523
+ - \`depends_on\`: ids of tasks that must be validated before this one starts
11524
+ 3. **Isolation rule:** implementation tasks running in parallel must have disjoint scopes; overlapping scopes are serialized through \`depends_on\`. Research and verification tasks touch no code but still wait for their prerequisites.
11525
+ 4. If the request is ambiguous, ask the user before decomposing. Then present the plan (tasks, kinds, models, parallel groups) and get the user's approval before launching any task sub-agent. Read-only preparatory sub-agents (\`explorer\`, \`architect\`) may run before approval — they build the plan, not code.
11526
+ ## Web Dashboard
11527
+
11528
+ Start the web dashboard to monitor agents, costs, and history in real-time:
11529
+ \`\`\`
11530
+ nexus.dashboard.start(port=4747, host="127.0.0.1")
11531
+ \`\`\`
11532
+ Then open http://127.0.0.1:4747 in your browser.
11533
+
11534
+ Stop it when done:
11535
+ \`\`\`
11536
+ nexus.dashboard.stop()
11537
+ \`\`\`
11538
+
11539
+ ## Git Workflow & Environment Detection
11540
+
11541
+ Before starting any work, detect the git environment and follow proper workflow:
11542
+
11543
+ ### Detection
11544
+ - Run \`git status\` to verify it's a git repo
11545
+ - Check \`.github/\` or \`.gitlab-ci.yml\` for CI/CD
11546
+ - Check \`git remote -v\` for GitHub/GitLab URLs
11547
+
11548
+ ### Rules
11549
+ 1. **NEVER push directly to main** — always create a feature branch
11550
+ 2. **Use conventional commits**: \`feat:\`, \`fix:\`, \`docs:\`, \`chore:\`
11551
+ 3. **Create PR** for review before merging
11552
+ 4. **Ask the user** about branching strategy if unclear
11553
+
11554
+ ### Memory
11555
+ Store git workflow preferences in memory for future reference.
11556
+
11171
11557
  ## Quality Gates
11172
11558
 
11173
- - Every code change → nexus.spawn(role="reviewer", wait=true)
11559
+ ## Phase 2 — Worktrees and snapshots
11560
+
11561
+ 1. Enable worktree isolation: \`nexus.worktree.enable()\`. Verify the repository is clean; if not, stop and report. Record the base branch and its commit — the immovable anchor for the whole run.
11562
+ 2. Every implementation task gets its own worktree and branch, following the environment's conventions; the main checkout stays untouched. Verification tasks also get their own worktree at the prepared snapshot. Comparison candidates use separate worktrees.
11563
+ 3. A task's worktree is created only once all its prerequisites are validated, from this snapshot rule (transitive implementation ancestors count, even through research or verification prerequisites):
11564
+ - No implementation ancestor → start from the base commit
11565
+ - Exactly one implementation ancestor → fast-path: branch directly from that ancestor's validated commit
11566
+ - Several implementation ancestors → start from the base commit and merge each ancestor's validated commit in \`depends_on\` order
11567
+ 4. Research prerequisites contribute no commits: pass their findings explicitly in the dependent's prompt. Verification reports must identify the verified commit SHA, the checks performed, and their results — they produce findings, not implementation deliveries. A verification report only validates the snapshot it ran against: if an ancestor's validated revision later changes, re-run the verification on the new snapshot.
11568
+ 5. Comparison candidates must all start from the same snapshot.
11569
+ 6. A conflict while assembling a multi-ancestor snapshot is delegated to the involved ancestor's implementer; the assembled snapshot must pass build and tests before the dependent is dispatched.
11570
+
11571
+ ## Phase 3 — Dispatch
11572
+
11573
+ 1. Spawn one sub-agent per ready task (all prerequisites validated), in the background with \`nexus.spawn(role=..., task=..., wait=false)\`, with the \`model\` picked from the pool matching its complexity.
11574
+ 2. Each prompt must include:
11575
+ - the kind, objective, and acceptance criteria
11576
+ - the task scope, with the instruction to never touch files outside it
11577
+ - findings from research and verification prerequisites, when any
11578
+ - for implementation tasks: work inside your worktree (move your session there so every read, edit, and command targets it) and run the project build and tests before reporting — the final delivery commit is produced during the quality gate
11579
+ - for verification tasks: work inside your snapshot worktree (move your session there) and report the verified commit SHA, the checks performed, and their results
11580
+ 3. **Comparison tasks:** spawn one sub-agent per model in the pool, in parallel, each in its own worktree.
11581
+ 4. Monitor progress: use \`nexus.sessions()\` to track active agents. When a task completes, use \`nexus.result(sessionID=...)\` to retrieve its output.
11582
+ 5. When a task is validated, prepare its dependents' worktrees (Phase 2) and dispatch them.
11583
+
11584
+ ## Phase 4 — Quality gate (every implementation delivery)
11585
+
11586
+ Research and verification reports are assessed directly against their acceptance criteria.
11587
+
11588
+ 1. **Review:** spawn a \`reviewer\` sub-agent with \`nexus.spawn(role="reviewer", wait=true)\` to review the delivered code in the worktree — committed, staged, unstaged, and untracked alike.
11589
+ 2. **Security scan:** run \`nexus.security.scan(content=..., filename=...)\` on changed files. Flag any security findings as blocking issues.
11590
+ 3. **Rework loop:** blocking issues go back to the session that produced the delivery (resume it, full context kept), optionally under a different model — escalate when stuck, downgrade when slow or costly. Each round repeats review until no blocking issues remain.
11591
+ 4. **Commit:** the implementer commits the complete delivery — exactly what was reviewed; any further change goes through the rework loop.
11592
+ 5. **Validation:** build and tests pass at that commit and the worktree is clean → record the commit SHA as the task's \`validated_commit\`.
11593
+ 6. **Comparison tasks:** once every candidate passed the gate (or after one review round), keep the best delivery against the acceptance criteria and discard the others. Use \`nexus.performance.scores()\` to inform which model delivered the best result.
11594
+ 7. **Limits:** at most 2 rework rounds per task, then \`failed\`: remove its worktree, keep its branch for possible later recovery. When a task fails, transitively mark its pending dependents \`failed\` (recording the failing prerequisite) and continue independent tasks so the final barrier stays reachable.
11595
+ 8. **Cost tracking:** after each quality gate round, call \`nexus.costs()\` to check budget status. If budget is exhausted, halt remaining tasks and report.
11596
+
11597
+ ## Phase 5 — Final integration and simplification
11598
+
11599
+ 1. Wait until every task is validated or marked failed. Never merge mid-flight.
11600
+ 2. Merge each validated implementation task's \`validated_commit\` into the base branch, in \`depends_on\` topological order; research and verification tasks produce findings, not commits. Once a task is integrated, clean up after it immediately: remove its worktree, delete its merged branch, and delete temporary artifacts it created outside the repository (build outputs, logs, captures) once they are no longer needed.
11601
+ 3. **Conflict recovery** — never force:
11602
+ - Abort the conflicting merge in the base checkout first; never leave an unfinished merge behind.
11603
+ - Delegate to the implementer: merge the current integration commit into its task branch in its worktree and resolve.
11604
+ - The resolution changes the delivery: commit it, re-run the quality gate, record the replacement \`validated_commit\`, then retry.
11605
+ - When a prerequisite's validated revision changes, revalidate its affected dependents — bounded to one cascade per integration; further churn marks the task \`failed\`.
11606
+ - If a task ultimately fails here, exclude its unmerged descendants — even previously validated ones — and confirm the base checkout is clean before continuing.
11607
+ 4. **Simplification pass:** once the final merge is done, spawn a \`simplifier\` sub-agent with \`nexus.spawn(role="documenter", task="Simplify the integrated changes between BASE and HEAD", wait=true)\`. Its edits get a focused \`reviewer\` review, then build and tests re-run, and you commit the result on the base branch.
11608
+ 5. Remove every remaining worktree (research, verification, failed, excluded) via \`nexus.worktree.disable()\` and delete stray temporary artifacts.
11609
+ 6. **Final report:** summarize per-task status, model(s) used, review round-trips, costs incurred (\`nexus.costs()\`), performance insights (\`nexus.performance.scores()\`), and the overall outcome with follow-ups.
11610
+
11611
+ ## Rules
11612
+
11613
+ - Never modify code directly; all code changes go through sub-agents. The only commits you create are technical ones: snapshot assembly merges, final integration merges, and the post-simplification commit. Validated delivery commits always come from sub-agents.
11614
+ - Never force a merge, rewrite history, or discard uncommitted user work.
11615
+ - Rework always resumes the session that produced the delivery, possibly under a different model.
11616
+ - Never leave the base checkout in an unfinished merge state.
11617
+ - Always identify a validated task by its immutable \`validated_commit\`: use recorded SHAs, never branch names or working trees, when creating dependents and integrating deliveries.
11618
+ - Report progress after each phase. Keep reports concise.
11619
+
11620
+ ## Nexus-Specific Advantages
11621
+
11622
+ These capabilities differentiate Nexus from a plain orchestrator:
11623
+
11624
+ ### Cost Intelligence
11625
+ - \`nexus.forecast(tasks=...)\` — estimate costs before dispatching, avoid budget surprises
11626
+ - \`nexus.costs()\` — real-time budget status, halt if exhausted
11627
+ - \`nexus.model.costs(model=...)\` — inspect per-model pricing to make informed routing decisions
11628
+
11629
+ ### Performance Learning
11630
+ - \`nexus.performance.scores()\` — see which model/role combos succeed most
11631
+ - \`nexus.performance.best(role=...)\` — pick the proven winner for a role
11632
+ - Scores improve routing over time: complex tasks go to high-performers, simple tasks use cheaper models
11633
+
11634
+ ### Security Scanning
11635
+ - \`nexus.security.scan(content=..., filename=...)\` — automated security review at the quality gate
11636
+ - Findings become blocking issues in the rework loop
11637
+ - No code ships without a security pass
11638
+
11639
+ ### Observability
11640
+ - \`nexus.dashboard.start()\` — live web dashboard for monitoring orchestrator state
11641
+ - \`nexus.status(detailed=true)\` — full metrics on demand
11642
+ - \`nexus.history.list(count=N)\` — execution history for post-mortem analysis
11643
+ - \`nexus.history.stats()\` — aggregate success rates and cost trends
11644
+
11645
+ ### Extensibility
11646
+ - \`nexus.roles.add(...)\` — define custom agent roles with specialized prompts
11647
+ - \`nexus.template(name=...)\` — reusable task templates for common workflows
11648
+ - \`nexus.worktree.enable()\` — git worktree isolation for parallel safety
11649
+ - Every code change → nexus.delegate(role="reviewer")
11174
11650
  - Security check → nexus.security.scan()
11175
- - Testing → nexus.spawn(role="tester", wait=true)
11651
+ - Testing → nexus.delegate(role="tester")
11176
11652
  `;
11177
11653
  var src_default = define({
11178
11654
  id: "nexus",
11179
11655
  async setup(ctx) {
11180
11656
  try {
11181
11657
  const agentDir = join6(homedir2(), ".config", "opencode", "agents");
11182
- const agentFile = join6(agentDir, "nexus-orchestrator.md");
11183
- if (!existsSync3(agentFile)) {
11184
- mkdirSync4(agentDir, { recursive: true });
11185
- writeFileSync2(agentFile, NEXUS_AGENT_CONTENT, "utf-8");
11658
+ mkdirSync4(agentDir, { recursive: true });
11659
+ const orchestratorFile = join6(agentDir, "nexus-orchestrator.md");
11660
+ if (!existsSync3(orchestratorFile)) {
11661
+ writeFileSync2(orchestratorFile, NEXUS_AGENT_CONTENT, "utf-8");
11662
+ }
11663
+ const subagents = {
11664
+ "nexus-architect.md": `---
11665
+ description: Nexus Architect agent — designs system architecture with cost-aware model selection
11666
+ mode: subagent
11667
+ permissions:
11668
+ - action: edit
11669
+ resource: "*"
11670
+ - action: shell
11671
+ resource: "*"
11672
+ ---
11673
+
11674
+ # Nexus Architect Agent
11675
+
11676
+ You are a Nexus Architect sub-agent. Design system architecture.
11677
+
11678
+ - System design and architecture patterns
11679
+ - High-level technical decisions
11680
+ - API design and interface definitions
11681
+ - Consider cost implications
11682
+ - Document decisions and trade-offs`,
11683
+ "nexus-coder.md": `---
11684
+ description: Nexus Coder agent — implements code with cost-aware model selection
11685
+ mode: subagent
11686
+ permissions:
11687
+ - action: edit
11688
+ resource: "*"
11689
+ - action: shell
11690
+ resource: "*"
11691
+ ---
11692
+
11693
+ # Nexus Coder Agent
11694
+
11695
+ You are a Nexus Coder sub-agent. Implement code tasks.
11696
+
11697
+ - Write clean, efficient TypeScript/JavaScript code
11698
+ - Follow existing code patterns
11699
+ - Add tests for functionality
11700
+ - Use model from nexus config for your role
11701
+ - Commit with conventional commit messages`,
11702
+ "nexus-explorer.md": `---
11703
+ description: Nexus Explorer agent — explores codebases and provides architecture analysis
11704
+ mode: subagent
11705
+ permissions:
11706
+ - action: edit
11707
+ resource: "*"
11708
+ effect: deny
11709
+ ---
11710
+
11711
+ # Nexus Explorer Agent
11712
+
11713
+ You are a Nexus Explorer sub-agent. Explore codebases.
11714
+
11715
+ - Read-only exploration
11716
+ - Find files by patterns
11717
+ - Analyze dependencies
11718
+ - Report architecture findings`,
11719
+ "nexus-tester.md": `---
11720
+ description: Nexus Tester agent — writes and runs tests for quality assurance
11721
+ mode: subagent
11722
+ permissions:
11723
+ - action: edit
11724
+ resource: "*"
11725
+ - action: shell
11726
+ resource: "*"
11727
+ ---
11728
+
11729
+ # Nexus Tester Agent
11730
+
11731
+ You are a Nexus Tester sub-agent. Write and run tests.
11732
+
11733
+ - Unit tests for new functions
11734
+ - Integration tests for features
11735
+ - Test edge cases and errors
11736
+ - Follow existing test patterns`,
11737
+ "nexus-reviewer.md": `---
11738
+ description: Nexus Reviewer agent — reviews code for quality, security, and correctness
11739
+ mode: subagent
11740
+ permissions:
11741
+ - action: edit
11742
+ resource: "*"
11743
+ effect: deny
11744
+ ---
11745
+
11746
+ # Nexus Reviewer Agent
11747
+
11748
+ You are a Nexus Reviewer sub-agent. Review code changes.
11749
+
11750
+ - Code correctness and logic
11751
+ - Security vulnerabilities
11752
+ - Performance implications
11753
+ - Test coverage
11754
+ - Provide APPROVED / CHANGES REQUESTED assessment`,
11755
+ "nexus-documenter.md": `---
11756
+ description: Nexus Documenter agent — writes clear, comprehensive technical documentation
11757
+ mode: subagent
11758
+ permissions:
11759
+ - action: edit
11760
+ resource: "*"
11761
+ - action: shell
11762
+ resource: "*"
11763
+ ---
11764
+
11765
+ # Nexus Documenter Agent
11766
+
11767
+ You are a Nexus Documenter sub-agent. Write documentation.
11768
+
11769
+ - API documentation
11770
+ - README files
11771
+ - Architecture docs
11772
+ - Include code examples
11773
+ - Keep docs close to code`
11774
+ };
11775
+ for (const [filename, content] of Object.entries(subagents)) {
11776
+ const filepath = join6(agentDir, filename);
11777
+ if (!existsSync3(filepath)) {
11778
+ writeFileSync2(filepath, content, "utf-8");
11779
+ }
11186
11780
  }
11187
11781
  } catch {}
11188
11782
  try {
@@ -11197,12 +11791,49 @@ var src_default = define({
11197
11791
  }
11198
11792
  }
11199
11793
  } catch {}
11794
+ try {
11795
+ const nexusConfigPath = join6(process.cwd(), ".opencode", "nexus.jsonc");
11796
+ if (existsSync3(nexusConfigPath)) {
11797
+ const nexusContent = readFileSync3(nexusConfigPath, "utf-8");
11798
+ const stripped = nexusContent.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
11799
+ const nexusConfig = JSON.parse(stripped);
11800
+ if (nexusConfig.models) {
11801
+ const configPath = join6(homedir2(), ".config", "opencode", "opencode.jsonc");
11802
+ if (existsSync3(configPath)) {
11803
+ let configContent = readFileSync3(configPath, "utf-8");
11804
+ const configObj = JSON.parse(configContent);
11805
+ if (!configObj.agents)
11806
+ configObj.agents = {};
11807
+ if (!configObj.agents["nexus-orchestrator"])
11808
+ configObj.agents["nexus-orchestrator"] = {};
11809
+ if (!configObj.agents["nexus-orchestrator"].model) {
11810
+ configObj.agents["nexus-orchestrator"].model = nexusConfig.models.coder || "opencode-go/mimo-v2.5";
11811
+ writeFileSync2(configPath, JSON.stringify(configObj, null, 2) + `
11812
+ `, "utf-8");
11813
+ }
11814
+ }
11815
+ }
11816
+ }
11817
+ } catch {}
11200
11818
  const orchestrator = new NexusOrchestrator;
11819
+ const goalManager = new GoalManager;
11201
11820
  await orchestrator.initialize(ctx, () => {
11202
11821
  const state = orchestrator.getState();
11203
11822
  ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state))).catch(() => {});
11823
+ const sidebarState = {
11824
+ agents: state.agents,
11825
+ totalCost: state.totalSpent,
11826
+ budgetRemaining: state.budgetRemaining
11827
+ };
11828
+ ctx.storage.set("nexus-sidebar-state", JSON.parse(JSON.stringify(sidebarState))).catch(() => {});
11204
11829
  });
11205
11830
  await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
11831
+ const initialState = orchestrator.getState();
11832
+ await ctx.storage.set("nexus-sidebar-state", JSON.parse(JSON.stringify({
11833
+ agents: initialState.agents,
11834
+ totalCost: initialState.totalSpent,
11835
+ budgetRemaining: initialState.budgetRemaining
11836
+ })));
11206
11837
  await ctx.tool.transform((editor) => {
11207
11838
  editor.namespace({
11208
11839
  name: "nexus",
@@ -11422,7 +12053,7 @@ var src_default = define({
11422
12053
  properties: {
11423
12054
  role: { type: "string", description: "Agent role (architect, coder, reviewer, tester, explorer, documenter)" },
11424
12055
  task: { type: "string", description: "Task description" },
11425
- model: { type: "string", description: "Model override (optional)" },
12056
+ model: { type: "string", description: "Model override (optional, e.g. 'anthropic/claude-sonnet-4-6')" },
11426
12057
  wait: { type: "boolean", description: "Wait for completion (default: false)" },
11427
12058
  timeout: { type: "number", description: "Timeout in ms when waiting (default: 120000)" }
11428
12059
  },
@@ -11432,17 +12063,62 @@ var src_default = define({
11432
12063
  execute: async (input) => {
11433
12064
  const { role, task, model, wait, timeout } = input;
11434
12065
  try {
11435
- orchestrator.parentSessionID = null;
12066
+ if (!orchestrator.parentSessionID) {
12067
+ try {
12068
+ const currentSession = ctx.session?.current?.();
12069
+ if (currentSession?.id) {
12070
+ orchestrator.parentSessionID = currentSession.id;
12071
+ }
12072
+ } catch {}
12073
+ }
11436
12074
  const agent = await orchestrator.spawnAgent({ role, model });
11437
12075
  await orchestrator.ctx.session.prompt({
11438
12076
  sessionID: agent.sessionID,
11439
12077
  text: task
11440
12078
  });
12079
+ agent.status = "working";
12080
+ orchestrator.notifyStateChange();
11441
12081
  if (wait) {
11442
12082
  const waitTimeout = timeout || 120000;
12083
+ const waitPromise = orchestrator.ctx.session.wait({ sessionID: agent.sessionID });
12084
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${waitTimeout}ms`)), waitTimeout));
11443
12085
  try {
11444
- await orchestrator.ctx.session.wait({ sessionID: agent.sessionID });
11445
- } catch {}
12086
+ await Promise.race([waitPromise, timeoutPromise]);
12087
+ } catch (waitError) {
12088
+ agent.status = "working";
12089
+ orchestrator.notifyStateChange();
12090
+ try {
12091
+ const messages = await orchestrator.ctx.session.context({ sessionID: agent.sessionID });
12092
+ const lastMsg = messages.filter((m) => m.role === "assistant").pop();
12093
+ if (lastMsg) {
12094
+ agent.status = "completed";
12095
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
12096
+ const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
12097
+ return {
12098
+ content: [
12099
+ `${agent.name}`,
12100
+ `\uD83D\uDCCB Task: ${taskPreview}`,
12101
+ `⏱️ Status: ${waitError.message || "timeout"}`,
12102
+ `\uD83D\uDCCE Session: ${agent.sessionID}`,
12103
+ `
12104
+ --- Partial Result ---`,
12105
+ typeof lastMsg.content === "string" ? lastMsg.content : JSON.stringify(lastMsg.content)
12106
+ ].join(`
12107
+ `)
12108
+ };
12109
+ }
12110
+ } catch {}
12111
+ return {
12112
+ content: [
12113
+ `${agent.name}`,
12114
+ `\uD83D\uDCCB Task: ${task.length > 80 ? task.substring(0, 77) + "..." : task}`,
12115
+ `⏱️ Status: ${waitError.message || "timeout"}`,
12116
+ `\uD83D\uDCCE Session: ${agent.sessionID}`,
12117
+ `\uD83D\uDCA1 Use nexus.result(sessionID="${agent.sessionID}") to check later`
12118
+ ].join(`
12119
+ `)
12120
+ };
12121
+ }
11446
12122
  try {
11447
12123
  const messages = await orchestrator.ctx.session.context({ sessionID: agent.sessionID });
11448
12124
  const lastMsg = messages.filter((m) => m.role === "assistant").pop();
@@ -11458,11 +12134,23 @@ var src_default = define({
11458
12134
  `\uD83D\uDCCE Session: ${agent.sessionID}`,
11459
12135
  `
11460
12136
  --- Result ---`,
11461
- result
12137
+ typeof result === "string" ? result : JSON.stringify(result)
11462
12138
  ].join(`
11463
12139
  `)
11464
12140
  };
11465
- } catch {}
12141
+ } catch {
12142
+ agent.status = "completed";
12143
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
12144
+ return {
12145
+ content: [
12146
+ `${agent.name}`,
12147
+ `✅ Status: completed`,
12148
+ `\uD83D\uDCCE Session: ${agent.sessionID}`,
12149
+ `⚠️ Could not read result output`
12150
+ ].join(`
12151
+ `)
12152
+ };
12153
+ }
11466
12154
  }
11467
12155
  const complexity = orchestrator.analyzeComplexity({
11468
12156
  id: `spawn-${Date.now()}`,
@@ -11485,7 +12173,7 @@ var src_default = define({
11485
12173
  `\uD83D\uDCCA Complexity: ${complexity.overall}/100 (${complexity.factors.riskLevel} risk)`,
11486
12174
  `\uD83E\uDD16 Model reasoning: ${modelSelection.reasoning}`,
11487
12175
  `\uD83D\uDCCE Session: ${agent.sessionID}`,
11488
- `\uD83D\uDCA1 Use wait=true to wait for completion`
12176
+ `\uD83D\uDCA1 Use wait=true to wait for completion, or nexus.result() to fetch later`
11489
12177
  ].join(`
11490
12178
  `);
11491
12179
  return { content: output };
@@ -11494,6 +12182,72 @@ var src_default = define({
11494
12182
  }
11495
12183
  }
11496
12184
  });
12185
+ editor.add({
12186
+ name: "delegate",
12187
+ description: "Delegate a task to a sub-agent and wait for result (convenience wrapper around spawn+wait)",
12188
+ input: {
12189
+ type: "object",
12190
+ properties: {
12191
+ role: { type: "string", description: "Agent role (architect, coder, reviewer, tester, explorer, documenter)" },
12192
+ task: { type: "string", description: "Task description" },
12193
+ model: { type: "string", description: "Model override (optional)" },
12194
+ timeout: { type: "number", description: "Timeout in ms (default: 120000)" }
12195
+ },
12196
+ required: ["role", "task"],
12197
+ additionalProperties: false
12198
+ },
12199
+ execute: async (input) => {
12200
+ const { role, task, model, timeout } = input;
12201
+ try {
12202
+ const agent = await orchestrator.spawnAgent({ role, model });
12203
+ await orchestrator.ctx.session.prompt({
12204
+ sessionID: agent.sessionID,
12205
+ text: task
12206
+ });
12207
+ agent.status = "working";
12208
+ orchestrator.notifyStateChange();
12209
+ const waitTimeout = timeout || 120000;
12210
+ const waitPromise = orchestrator.ctx.session.wait({ sessionID: agent.sessionID });
12211
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("timeout"), waitTimeout));
12212
+ const outcome = await Promise.race([waitPromise.then(() => "completed"), timeoutPromise]);
12213
+ try {
12214
+ const messages = await orchestrator.ctx.session.context({ sessionID: agent.sessionID });
12215
+ const lastMsg = messages.filter((m) => m.role === "assistant").pop();
12216
+ const resultContent = lastMsg?.content || (outcome === "timeout" ? "Timed out — agent may still be running" : "Completed with no output");
12217
+ agent.status = outcome === "timeout" ? "working" : "completed";
12218
+ orchestrator.notifyStateChange();
12219
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
12220
+ const statusIcon = outcome === "timeout" ? "⏱️" : "✅";
12221
+ return {
12222
+ content: [
12223
+ `${agent.name}`,
12224
+ `\uD83D\uDCCB Task: ${task.length > 80 ? task.substring(0, 77) + "..." : task}`,
12225
+ `${statusIcon} Status: ${outcome}`,
12226
+ `\uD83D\uDCCE Session: ${agent.sessionID}`,
12227
+ `
12228
+ --- Result ---`,
12229
+ typeof resultContent === "string" ? resultContent : JSON.stringify(resultContent)
12230
+ ].join(`
12231
+ `)
12232
+ };
12233
+ } catch {
12234
+ agent.status = outcome === "timeout" ? "working" : "completed";
12235
+ orchestrator.notifyStateChange();
12236
+ return {
12237
+ content: [
12238
+ `${agent.name}`,
12239
+ `${outcome === "timeout" ? "⏱️" : "✅"} Status: ${outcome}`,
12240
+ `\uD83D\uDCCE Session: ${agent.sessionID}`,
12241
+ `⚠️ Could not read result output`
12242
+ ].join(`
12243
+ `)
12244
+ };
12245
+ }
12246
+ } catch (error) {
12247
+ return { content: `Delegate failed: ${error.message}` };
12248
+ }
12249
+ }
12250
+ });
11497
12251
  editor.add({
11498
12252
  name: "template",
11499
12253
  description: "List or instantiate task templates",
@@ -11764,6 +12518,381 @@ ${lastMsg.content}` };
11764
12518
  }
11765
12519
  }
11766
12520
  });
12521
+ editor.add({
12522
+ name: "clarify",
12523
+ description: "Ask clarifying question before proceeding with ambiguous task",
12524
+ input: {
12525
+ type: "object",
12526
+ properties: {
12527
+ question: { type: "string", description: "The clarifying question to ask" },
12528
+ options: { type: "string", description: "Comma-separated options to present" },
12529
+ assumption: { type: "string", description: "Default assumption if no response" }
12530
+ },
12531
+ required: ["question"],
12532
+ additionalProperties: false
12533
+ },
12534
+ execute: async (input) => {
12535
+ const { question, options, assumption } = input;
12536
+ const optionList = options ? options.split(",").map((o) => o.trim()) : [];
12537
+ let response = `❓ ${question}`;
12538
+ if (optionList.length > 0) {
12539
+ response += `
12540
+ Options: ${optionList.map((o, i) => `${i + 1}. ${o}`).join(", ")}`;
12541
+ }
12542
+ if (assumption) {
12543
+ response += `
12544
+ \uD83D\uDCA1 Default: ${assumption}`;
12545
+ }
12546
+ return { content: response };
12547
+ }
12548
+ });
12549
+ editor.add({
12550
+ name: "todo.add",
12551
+ description: "Add a todo item to track work",
12552
+ input: {
12553
+ type: "object",
12554
+ properties: {
12555
+ description: { type: "string", description: "Todo description" },
12556
+ assignedTo: { type: "string", description: "Agent or role to assign (optional)" }
12557
+ },
12558
+ required: ["description"],
12559
+ additionalProperties: false
12560
+ },
12561
+ execute: async (input) => {
12562
+ const { description, assignedTo } = input;
12563
+ const item = orchestrator.todoEnforcer.add(description, assignedTo);
12564
+ return { content: `\uD83D\uDCDD Todo added: ${item.id}: ${item.description} (${item.status})` };
12565
+ }
12566
+ });
12567
+ editor.add({
12568
+ name: "todo.list",
12569
+ description: "List all todo items",
12570
+ input: {
12571
+ type: "object",
12572
+ properties: {},
12573
+ additionalProperties: false
12574
+ },
12575
+ execute: async () => {
12576
+ return { content: orchestrator.todoEnforcer.formatAll() };
12577
+ }
12578
+ });
12579
+ editor.add({
12580
+ name: "todo.complete",
12581
+ description: "Mark a todo item as completed",
12582
+ input: {
12583
+ type: "object",
12584
+ properties: {
12585
+ id: { type: "string", description: "Todo item ID" }
12586
+ },
12587
+ required: ["id"],
12588
+ additionalProperties: false
12589
+ },
12590
+ execute: async (input) => {
12591
+ const { id } = input;
12592
+ const item = orchestrator.todoEnforcer.get(id);
12593
+ if (!item)
12594
+ return { content: `❌ Todo ${id} not found.` };
12595
+ orchestrator.todoEnforcer.complete(id);
12596
+ return { content: `✅ Todo completed: ${id}: ${item.description}` };
12597
+ }
12598
+ });
12599
+ editor.add({
12600
+ name: "todo.stats",
12601
+ description: "Get todo statistics",
12602
+ input: {
12603
+ type: "object",
12604
+ properties: {},
12605
+ additionalProperties: false
12606
+ },
12607
+ execute: async () => {
12608
+ const stats = orchestrator.todoEnforcer.getStats();
12609
+ return {
12610
+ content: `\uD83D\uDCCA Todo Statistics:
12611
+ Total: ${stats.total}
12612
+ ⏳ Pending: ${stats.pending}
12613
+ \uD83D\uDD04 In Progress: ${stats.inProgress}
12614
+ ✅ Completed: ${stats.completed}
12615
+ \uD83D\uDEAB Blocked: ${stats.blocked}`
12616
+ };
12617
+ }
12618
+ });
12619
+ editor.add({
12620
+ name: "goal.set",
12621
+ description: "Set a new persistent objective",
12622
+ input: {
12623
+ type: "object",
12624
+ properties: {
12625
+ description: { type: "string", description: "Goal description" },
12626
+ autoContinue: { type: "boolean", description: "Auto-continue (default: true)" }
12627
+ },
12628
+ required: ["description"],
12629
+ additionalProperties: false
12630
+ },
12631
+ execute: async (input) => {
12632
+ const { description, autoContinue } = input;
12633
+ const goal = goalManager.set(description, autoContinue ?? true);
12634
+ return { content: `\uD83C\uDFAF Goal set: ${goal.description}` };
12635
+ }
12636
+ });
12637
+ editor.add({
12638
+ name: "goal.status",
12639
+ description: "Show current goal status",
12640
+ input: { type: "object", properties: {}, additionalProperties: false },
12641
+ execute: async () => {
12642
+ const goal = goalManager.getActive();
12643
+ if (!goal)
12644
+ return { content: "No active goal. Use nexus.goal.set() to create one." };
12645
+ return { content: `\uD83C\uDFAF ${goal.description}
12646
+ Status: ${goal.status}
12647
+ Tasks: ${goal.tasks.length}` };
12648
+ }
12649
+ });
12650
+ editor.add({
12651
+ name: "goal.complete",
12652
+ description: "Complete current goal",
12653
+ input: { type: "object", properties: {}, additionalProperties: false },
12654
+ execute: async () => {
12655
+ const goal = goalManager.getActive();
12656
+ if (!goal)
12657
+ return { content: "No active goal." };
12658
+ goalManager.complete(goal.id);
12659
+ return { content: `✅ Goal completed: ${goal.description}` };
12660
+ }
12661
+ });
12662
+ editor.add({
12663
+ name: "goal.list",
12664
+ description: "List all goals",
12665
+ input: { type: "object", properties: {}, additionalProperties: false },
12666
+ execute: async () => {
12667
+ const goals = goalManager.getAll();
12668
+ if (goals.length === 0)
12669
+ return { content: "No goals yet." };
12670
+ const lines = goals.map((g) => `${g.status === "active" ? "\uD83C\uDFAF" : "✅"} ${g.description}`);
12671
+ return { content: lines.join(`
12672
+ `) };
12673
+ }
12674
+ });
12675
+ editor.add({
12676
+ name: "goal.status",
12677
+ description: "Show current active goal status",
12678
+ input: {
12679
+ type: "object",
12680
+ properties: {},
12681
+ additionalProperties: false
12682
+ },
12683
+ execute: async () => {
12684
+ const active = goalManager.getActive();
12685
+ if (!active)
12686
+ return { content: "No active goal. Use nexus.goal.set() to set one." };
12687
+ return {
12688
+ content: [
12689
+ `\uD83C\uDFAF Active Goal: ${active.description}`,
12690
+ `\uD83D\uDCCB ID: ${active.id}`,
12691
+ `\uD83D\uDD04 Auto-continue: ${active.autoContinue ? "enabled" : "disabled"}`,
12692
+ `\uD83D\uDCCA Status: ${active.status}`,
12693
+ `\uD83D\uDCCE Tasks: ${active.tasks.length > 0 ? active.tasks.join(", ") : "none yet"}`,
12694
+ `\uD83D\uDCC5 Created: ${active.createdAt.toISOString()}`,
12695
+ `⏱️ Should continue: ${goalManager.shouldContinue() ? "yes" : "no"}`
12696
+ ].join(`
12697
+ `)
12698
+ };
12699
+ }
12700
+ });
12701
+ editor.add({
12702
+ name: "goal.complete",
12703
+ description: "Mark current active goal as completed",
12704
+ input: {
12705
+ type: "object",
12706
+ properties: {},
12707
+ additionalProperties: false
12708
+ },
12709
+ execute: async () => {
12710
+ const active = goalManager.getActive();
12711
+ if (!active)
12712
+ return { content: "No active goal to complete." };
12713
+ goalManager.complete(active.id);
12714
+ await ctx.storage.set("nexus-goal", JSON.parse(JSON.stringify(goalManager.getAll())));
12715
+ return {
12716
+ content: [
12717
+ `✅ Goal completed: ${active.description}`,
12718
+ `\uD83D\uDCCB ID: ${active.id}`,
12719
+ `\uD83D\uDCCE Tasks tracked: ${active.tasks.length}`
12720
+ ].join(`
12721
+ `)
12722
+ };
12723
+ }
12724
+ });
12725
+ editor.add({
12726
+ name: "goal.list",
12727
+ description: "List all goals",
12728
+ input: {
12729
+ type: "object",
12730
+ properties: {},
12731
+ additionalProperties: false
12732
+ },
12733
+ execute: async () => {
12734
+ const goals = goalManager.getAll();
12735
+ if (goals.length === 0)
12736
+ return { content: "No goals set yet. Use nexus.goal.set() to create one." };
12737
+ const lines = goals.map((g) => {
12738
+ const icon = g.status === "active" ? "\uD83C\uDFAF" : g.status === "completed" ? "✅" : g.status === "paused" ? "⏸️" : "❌";
12739
+ return `${icon} [${g.status}] ${g.description} (tasks: ${g.tasks.length})`;
12740
+ });
12741
+ return { content: `Goals (${goals.length}):
12742
+ ${lines.join(`
12743
+ `)}` };
12744
+ }
12745
+ });
12746
+ const teamManager = new TeamManager;
12747
+ const astGrep = new AstGrep;
12748
+ editor.add({
12749
+ name: "team.create",
12750
+ description: "Create a new team with a lead role",
12751
+ input: {
12752
+ type: "object",
12753
+ properties: {
12754
+ name: { type: "string", description: "Team name" },
12755
+ leadRole: { type: "string", description: "Role of the team lead" }
12756
+ },
12757
+ required: ["name", "leadRole"],
12758
+ additionalProperties: false
12759
+ },
12760
+ execute: async (input) => {
12761
+ const { name, leadRole } = input;
12762
+ const team = teamManager.create(name, leadRole);
12763
+ return { content: `Team '${team.name}' created with ID: ${team.id}
12764
+ Lead: ${leadRole}
12765
+ Status: ${team.status}
12766
+
12767
+ Add members with nexus.team.addMember(teamId="${team.id}", role="...", model="...")` };
12768
+ }
12769
+ });
12770
+ editor.add({
12771
+ name: "team.addMember",
12772
+ description: "Add a member to a team",
12773
+ input: {
12774
+ type: "object",
12775
+ properties: {
12776
+ teamId: { type: "string", description: "Team ID" },
12777
+ role: { type: "string", description: "Role for this member" },
12778
+ model: { type: "string", description: "Model for this member" }
12779
+ },
12780
+ required: ["teamId", "role", "model"],
12781
+ additionalProperties: false
12782
+ },
12783
+ execute: async (input) => {
12784
+ const { teamId, role, model } = input;
12785
+ const member = teamManager.addMember(teamId, role, model);
12786
+ if (!member) {
12787
+ return { content: `Team ${teamId} not found.` };
12788
+ }
12789
+ const team = teamManager.get(teamId);
12790
+ return { content: `Member added to team '${team?.name}':
12791
+ ID: ${member.id}
12792
+ Role: ${member.role}
12793
+ Model: ${member.model}
12794
+ Status: ${member.status}
12795
+
12796
+ Total members: ${team?.members.length || 0}` };
12797
+ }
12798
+ });
12799
+ editor.add({
12800
+ name: "team.status",
12801
+ description: "Show team status",
12802
+ input: {
12803
+ type: "object",
12804
+ properties: {
12805
+ teamId: { type: "string", description: "Team ID (optional, shows all if omitted)" }
12806
+ },
12807
+ additionalProperties: false
12808
+ },
12809
+ execute: async (input) => {
12810
+ const { teamId } = input;
12811
+ if (teamId) {
12812
+ const team = teamManager.get(teamId);
12813
+ if (!team) {
12814
+ return { content: `Team ${teamId} not found.` };
12815
+ }
12816
+ const memberLines = team.members.map((m) => ` - ${m.role} (${m.model}): ${m.status}`).join(`
12817
+ `);
12818
+ return { content: `Team: ${team.name} (${team.id})
12819
+ Lead: ${team.lead}
12820
+ Status: ${team.status}
12821
+ Created: ${team.createdAt.toISOString()}
12822
+ Members (${team.members.length}):
12823
+ ${memberLines || " No members yet"}` };
12824
+ }
12825
+ const teams = teamManager.getAll();
12826
+ if (teams.length === 0) {
12827
+ return { content: "No teams created yet." };
12828
+ }
12829
+ const lines = teams.map((t) => `${t.status === "active" ? "\uD83D\uDFE2" : t.status === "completed" ? "✅" : "\uD83D\uDD35"} ${t.name} (${t.id}) - Lead: ${t.lead} - Members: ${t.members.length}`);
12830
+ return { content: `Teams (${teams.length}):
12831
+ ${lines.join(`
12832
+ `)}` };
12833
+ }
12834
+ });
12835
+ editor.add({
12836
+ name: "team.activate",
12837
+ description: "Activate a team to start execution",
12838
+ input: {
12839
+ type: "object",
12840
+ properties: {
12841
+ teamId: { type: "string", description: "Team ID" }
12842
+ },
12843
+ required: ["teamId"],
12844
+ additionalProperties: false
12845
+ },
12846
+ execute: async (input) => {
12847
+ const { teamId } = input;
12848
+ const team = teamManager.get(teamId);
12849
+ if (!team) {
12850
+ return { content: `Team ${teamId} not found.` };
12851
+ }
12852
+ if (team.members.length === 0) {
12853
+ return { content: `Team '${team.name}' has no members. Add members before activating.` };
12854
+ }
12855
+ teamManager.activate(teamId);
12856
+ return { content: `Team '${team.name}' activated!
12857
+
12858
+ Team is now ready for parallel execution with ${team.members.length} members:
12859
+ ${team.members.map((m) => ` - ${m.role}: ${m.model}`).join(`
12860
+ `)}` };
12861
+ }
12862
+ });
12863
+ editor.add({
12864
+ name: "astgrep.search",
12865
+ description: "Search for AST patterns in codebase",
12866
+ input: {
12867
+ type: "object",
12868
+ properties: {
12869
+ pattern: { type: "string", description: "AST pattern to search for" },
12870
+ language: { type: "string", description: "Programming language (typescript, python, etc.)" },
12871
+ directory: { type: "string", description: "Directory to search in" }
12872
+ },
12873
+ required: ["pattern", "language", "directory"],
12874
+ additionalProperties: false
12875
+ },
12876
+ execute: async (input) => {
12877
+ const { pattern, language, directory } = input;
12878
+ const results = astGrep.search(pattern, language, directory);
12879
+ if (results.length === 0)
12880
+ return { content: `No matches found for "${pattern}" in ${language}` };
12881
+ const lines = results.map((r) => `${r.file}:${r.line} — ${r.match}`);
12882
+ return { content: `Found ${results.length} matches:
12883
+ ${lines.join(`
12884
+ `)}` };
12885
+ }
12886
+ });
12887
+ editor.add({
12888
+ name: "astgrep.status",
12889
+ description: "Check if ast-grep is installed",
12890
+ input: { type: "object", properties: {}, additionalProperties: false },
12891
+ execute: async () => {
12892
+ const available = astGrep.isAvailable();
12893
+ return { content: available ? "✅ ast-grep is installed" : "❌ ast-grep is not installed. Install with: cargo install ast-grep" };
12894
+ }
12895
+ });
11767
12896
  });
11768
12897
  await ctx.session.hook("prompt", (event) => {
11769
12898
  if (event.prompt.text.startsWith("/nexus")) {
@@ -11777,9 +12906,11 @@ ${lastMsg.content}` };
11777
12906
  }
11778
12907
  });
11779
12908
  export {
12909
+ AstGrep,
11780
12910
  CostForecaster,
11781
12911
  CustomRoleManager,
11782
12912
  DEFAULT_CONFIG,
12913
+ GoalManager,
11783
12914
  HealthMonitor,
11784
12915
  LearningModule,
11785
12916
  MessageRouter,
@@ -11793,6 +12924,8 @@ export {
11793
12924
  SecurityScanner,
11794
12925
  StateBroadcaster,
11795
12926
  TEMPLATES,
12927
+ TeamManager,
12928
+ TodoEnforcer,
11796
12929
  WorktreeManager,
11797
12930
  src_default as default,
11798
12931
  detectCycles,