@serkanalgur/opencode-nexus 1.0.5 → 1.2.0

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 +296 -248
  2. package/dist/index.js +2294 -432
  3. package/dist/tui.js +328 -20
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
1
4
  // node_modules/@opencode/plugin/dist/promise/plugin.js
2
5
  function define(plugin) {
3
6
  return plugin;
@@ -3457,7 +3460,7 @@ function hasCheck(checks, id) {
3457
3460
  }
3458
3461
  var number2 = /* @__PURE__ */ new Number4;
3459
3462
 
3460
- class Boolean extends Base2 {
3463
+ class Boolean2 extends Base2 {
3461
3464
  _tag = "Boolean";
3462
3465
  getParser() {
3463
3466
  return fromRefinement(this, isBoolean);
@@ -3466,7 +3469,7 @@ class Boolean extends Base2 {
3466
3469
  return "boolean";
3467
3470
  }
3468
3471
  }
3469
- var boolean = /* @__PURE__ */ new Boolean;
3472
+ var boolean = /* @__PURE__ */ new Boolean2;
3470
3473
  class Arrays extends Base2 {
3471
3474
  _tag = "Arrays";
3472
3475
  isMutable;
@@ -5372,7 +5375,7 @@ var Null2 = /* @__PURE__ */ make9(null_);
5372
5375
  var Undefined2 = /* @__PURE__ */ make9(undefined_2);
5373
5376
  var String5 = /* @__PURE__ */ make9(string2);
5374
5377
  var Number5 = /* @__PURE__ */ make9(number2);
5375
- var Boolean2 = /* @__PURE__ */ make9(boolean);
5378
+ var Boolean3 = /* @__PURE__ */ make9(boolean);
5376
5379
  function makeStruct(ast, fields) {
5377
5380
  return make9(ast, {
5378
5381
  fields,
@@ -6406,14 +6409,14 @@ var Option = Struct({
6406
6409
  var When = Struct({
6407
6410
  key: String5,
6408
6411
  op: Literals(["eq", "neq"]),
6409
- value: Union2([String5, Number5, Boolean2])
6412
+ value: Union2([String5, Number5, Boolean3])
6410
6413
  }).annotate({ identifier: "Form.When" });
6411
6414
  var FieldBase = {
6412
6415
  key: String5,
6413
6416
  title: String5.pipe(optional3),
6414
6417
  description: String5.pipe(optional3),
6415
- required: Boolean2.pipe(optional3),
6416
- hidden: Boolean2.pipe(optional3).annotate({
6418
+ required: Boolean3.pipe(optional3),
6419
+ hidden: Boolean3.pipe(optional3).annotate({
6417
6420
  description: "Skip the interactive authentication prompt and use the default unless an answer is supplied"
6418
6421
  }),
6419
6422
  when: ArraySchema(When).pipe(optional3)
@@ -6428,7 +6431,7 @@ var StringField = Struct({
6428
6431
  placeholder: String5.pipe(optional3),
6429
6432
  default: String5.pipe(optional3),
6430
6433
  options: ArraySchema(Option).pipe(optional3),
6431
- custom: Boolean2.pipe(optional3)
6434
+ custom: Boolean3.pipe(optional3)
6432
6435
  }).annotate({ identifier: "Form.StringField" });
6433
6436
  var NumberField = Struct({
6434
6437
  ...FieldBase,
@@ -6447,7 +6450,7 @@ var IntegerField = Struct({
6447
6450
  var BooleanField = Struct({
6448
6451
  ...FieldBase,
6449
6452
  type: Literal2("boolean"),
6450
- default: Boolean2.pipe(optional3)
6453
+ default: Boolean3.pipe(optional3)
6451
6454
  }).annotate({ identifier: "Form.BooleanField" });
6452
6455
  var MultiselectField = Struct({
6453
6456
  ...FieldBase,
@@ -6455,7 +6458,7 @@ var MultiselectField = Struct({
6455
6458
  options: ArraySchema(Option),
6456
6459
  minItems: NonNegativeInt.pipe(optional3),
6457
6460
  maxItems: NonNegativeInt.pipe(optional3),
6458
- custom: Boolean2.pipe(optional3),
6461
+ custom: Boolean3.pipe(optional3),
6459
6462
  default: ArraySchema(String5).pipe(optional3)
6460
6463
  }).annotate({ identifier: "Form.MultiselectField" });
6461
6464
  var ExternalField = Struct({
@@ -6484,7 +6487,7 @@ var Info2 = Struct({
6484
6487
  ...InfoBase,
6485
6488
  fields: Fields
6486
6489
  }).annotate({ identifier: "Form.Info" });
6487
- var Value = Union2([String5, Number5, Boolean2, ArraySchema(String5)]).annotate({
6490
+ var Value = Union2([String5, Number5, Boolean3, ArraySchema(String5)]).annotate({
6488
6491
  identifier: "Form.Value"
6489
6492
  });
6490
6493
  var Answer = Record(String5, Value).annotate({ identifier: "Form.Answer" });
@@ -6720,14 +6723,14 @@ var Overlays2 = {
6720
6723
  };
6721
6724
  var Compatibility = Struct({
6722
6725
  reasoningField: ReasoningField.pipe(optional3),
6723
- requireReasoning: Boolean2.pipe(optional3),
6726
+ requireReasoning: Boolean3.pipe(optional3),
6724
6727
  maxTokensField: MaxTokensField.pipe(optional3),
6725
- requireFinishReason: Boolean2.pipe(optional3),
6726
- requireAssistantAfterTool: Boolean2.pipe(optional3),
6727
- supportsPromptCacheKey: Boolean2.pipe(optional3)
6728
+ requireFinishReason: Boolean3.pipe(optional3),
6729
+ requireAssistantAfterTool: Boolean3.pipe(optional3),
6730
+ supportsPromptCacheKey: Boolean3.pipe(optional3)
6728
6731
  }).annotate({ identifier: "Model.Compatibility" });
6729
6732
  var Capabilities = Struct({
6730
- tools: Boolean2,
6733
+ tools: Boolean3,
6731
6734
  input: ArraySchema(String5),
6732
6735
  output: ArraySchema(String5)
6733
6736
  }).annotate({ identifier: "Model.Capabilities" }).pipe(statics(() => ({
@@ -6766,7 +6769,7 @@ var Info6 = Struct({
6766
6769
  }),
6767
6770
  cost: ArraySchema(Cost),
6768
6771
  status: Literals(["alpha", "beta", "deprecated", "active"]),
6769
- enabled: Boolean2,
6772
+ enabled: Boolean3,
6770
6773
  limit: Struct({
6771
6774
  context: Int,
6772
6775
  input: Int.pipe(optional3),
@@ -6850,7 +6853,7 @@ var Info7 = Struct({
6850
6853
  system: String5.pipe(optional3),
6851
6854
  description: String5.pipe(optional3),
6852
6855
  mode: Literals(["subagent", "primary", "all"]),
6853
- hidden: Boolean2,
6856
+ hidden: Boolean3,
6854
6857
  color: Color.pipe(optional3),
6855
6858
  steps: PositiveInt.pipe(optional3),
6856
6859
  permissions: Ruleset
@@ -6909,8 +6912,8 @@ class LocalConfig extends Class3("Mcp.LocalConfig")({
6909
6912
  description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory."
6910
6913
  }),
6911
6914
  environment: Record(String5, String5).pipe(optional3),
6912
- disabled: Boolean2.pipe(optional3),
6913
- codemode: Boolean2.pipe(optional3).annotate({
6915
+ disabled: Boolean3.pipe(optional3),
6916
+ codemode: Boolean3.pipe(optional3).annotate({
6914
6917
  description: "Expose this server's tools through Code Mode. Defaults to true."
6915
6918
  }),
6916
6919
  timeout: TimeoutConfig.pipe(optional3),
@@ -6935,8 +6938,8 @@ class RemoteConfig extends Class3("Mcp.RemoteConfig")({
6935
6938
  url: String5,
6936
6939
  headers: Record(String5, String5).pipe(optional3),
6937
6940
  oauth: Union2([OAuthConfig, Literal2(false)]).pipe(optional3),
6938
- disabled: Boolean2.pipe(optional3),
6939
- codemode: Boolean2.pipe(optional3).annotate({
6941
+ disabled: Boolean3.pipe(optional3),
6942
+ codemode: Boolean3.pipe(optional3).annotate({
6940
6943
  description: "Expose this server's tools through Code Mode. Defaults to true."
6941
6944
  }),
6942
6945
  timeout: TimeoutConfig.pipe(optional3),
@@ -7118,7 +7121,7 @@ var Info11 = Struct({
7118
7121
  id: ID11,
7119
7122
  name: Name2,
7120
7123
  description: String5.pipe(optional3),
7121
- autoinvoke: Boolean2.pipe(optional3),
7124
+ autoinvoke: Boolean3.pipe(optional3),
7122
7125
  path: AbsolutePath,
7123
7126
  content: String5
7124
7127
  }).annotate({ identifier: "Skill.Info" });
@@ -7261,7 +7264,7 @@ var Output = Struct({
7261
7264
  output: String5,
7262
7265
  cursor: NonNegativeInt,
7263
7266
  size: NonNegativeInt,
7264
- truncated: Boolean2
7267
+ truncated: Boolean3
7265
7268
  });
7266
7269
  // node_modules/@opencode/schema/dist/session-error.js
7267
7270
  var Error5 = Struct({
@@ -7386,7 +7389,7 @@ var AssistantTool = Struct({
7386
7389
  type: tag("tool"),
7387
7390
  id: String5,
7388
7391
  name: String5,
7389
- executed: Boolean2.pipe(optional3),
7392
+ executed: Boolean3.pipe(optional3),
7390
7393
  providerState: ProviderState.pipe(optional3),
7391
7394
  providerResultState: ProviderState.pipe(optional3),
7392
7395
  state: ToolState,
@@ -7583,7 +7586,7 @@ var Info17 = Struct({
7583
7586
  }).annotate({ identifier: "InstructionEntry.Info" });
7584
7587
  var Snapshot = ArraySchema(Struct({
7585
7588
  ...Info17.fields,
7586
- removed: Boolean2
7589
+ removed: Boolean3
7587
7590
  })).annotate({ identifier: "InstructionEntry.Snapshot" });
7588
7591
  var MaxValueBytes = 256 * 1024;
7589
7592
 
@@ -8028,7 +8031,7 @@ var Tool;
8028
8031
  schema: {
8029
8032
  ...ToolBase,
8030
8033
  input: Record(String5, Unknown2),
8031
- executed: Boolean2,
8034
+ executed: Boolean3,
8032
8035
  state: ProviderState.pipe(optional3)
8033
8036
  }
8034
8037
  });
@@ -8049,7 +8052,7 @@ var Tool;
8049
8052
  ...ToolBase,
8050
8053
  content: NonEmptyArray(Content),
8051
8054
  metadata: Record(String5, Json2).pipe(optional3),
8052
- executed: Boolean2,
8055
+ executed: Boolean3,
8053
8056
  resultState: ProviderState.pipe(optional3)
8054
8057
  }
8055
8058
  });
@@ -8064,7 +8067,7 @@ var Tool;
8064
8067
  error: Error5,
8065
8068
  content: NonEmptyArray(Content).pipe(optional3),
8066
8069
  metadata: Record(String5, Json2).pipe(optional3),
8067
- executed: Boolean2,
8070
+ executed: Boolean3,
8068
8071
  resultState: ProviderState.pipe(optional3)
8069
8072
  }
8070
8073
  });
@@ -8241,21 +8244,21 @@ var LocalSource = Struct({
8241
8244
  type: Literal2("local"),
8242
8245
  path: AbsolutePath,
8243
8246
  description: String5.pipe(optional3),
8244
- hidden: Boolean2.pipe(optional3)
8247
+ hidden: Boolean3.pipe(optional3)
8245
8248
  }).annotate({ identifier: "Reference.LocalSource" });
8246
8249
  var GitSource = Struct({
8247
8250
  type: Literal2("git"),
8248
8251
  repository: String5,
8249
8252
  branch: String5.pipe(optional3),
8250
8253
  description: String5.pipe(optional3),
8251
- hidden: Boolean2.pipe(optional3)
8254
+ hidden: Boolean3.pipe(optional3)
8252
8255
  }).annotate({ identifier: "Reference.GitSource" });
8253
8256
  var Source4 = Union2([LocalSource, GitSource]).pipe(toTaggedUnion("type")).annotate({ identifier: "Reference.Source" });
8254
8257
  var Info21 = Struct({
8255
8258
  name: String5,
8256
8259
  path: AbsolutePath,
8257
8260
  description: String5.pipe(optional3),
8258
- hidden: Boolean2.pipe(optional3),
8261
+ hidden: Boolean3.pipe(optional3),
8259
8262
  source: Source4
8260
8263
  }).annotate({ identifier: "Reference.Info" });
8261
8264
  // node_modules/@opencode/schema/dist/vcs.js
@@ -8301,7 +8304,7 @@ var Result2 = Struct({
8301
8304
  })
8302
8305
  }).annotate({ identifier: "WebSearch.Result" });
8303
8306
 
8304
- class Response extends Class3("WebSearch.Response")({
8307
+ class Response2 extends Class3("WebSearch.Response")({
8305
8308
  providerID: ID16,
8306
8309
  results: ArraySchema(Result2)
8307
8310
  }) {
@@ -8325,7 +8328,7 @@ var CreateInput4 = Struct({
8325
8328
  var RemoveInput = Struct({
8326
8329
  projectID: ID10,
8327
8330
  directory: AbsolutePath,
8328
- force: Boolean2
8331
+ force: Boolean3
8329
8332
  }).annotate({ identifier: "Worktree.RemoveInput" });
8330
8333
  var Info23 = Struct({
8331
8334
  directory: AbsolutePath
@@ -8341,7 +8344,7 @@ var ListEntry = Struct({
8341
8344
 
8342
8345
  class OperationError extends TaggedError3()("Worktree.OperationError", {
8343
8346
  message: String5,
8344
- forceRequired: optional3(Boolean2)
8347
+ forceRequired: optional3(Boolean3)
8345
8348
  }) {
8346
8349
  }
8347
8350
  var List = ArraySchema(Directory).annotate({ identifier: "Worktree.List" });
@@ -8361,6 +8364,64 @@ var Resolved = durable({
8361
8364
  });
8362
8365
  var Event16 = { Updated: Updated12, Resolved, Definitions: inventory(Updated12, Resolved) };
8363
8366
  // src/config.ts
8367
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
8368
+ import { homedir } from "node:os";
8369
+ import { join, dirname } from "node:path";
8370
+ function stripJsonComments(jsonc) {
8371
+ const result = [];
8372
+ let i = 0;
8373
+ const len = jsonc.length;
8374
+ while (i < len) {
8375
+ const ch = jsonc[i];
8376
+ if (ch === '"') {
8377
+ result.push(ch);
8378
+ i++;
8379
+ while (i < len && jsonc[i] !== '"') {
8380
+ if (jsonc[i] === "\\") {
8381
+ result.push(jsonc[i], jsonc[i + 1] ?? "");
8382
+ i += 2;
8383
+ } else {
8384
+ result.push(jsonc[i]);
8385
+ i++;
8386
+ }
8387
+ }
8388
+ if (i < len) {
8389
+ result.push(jsonc[i]);
8390
+ i++;
8391
+ }
8392
+ continue;
8393
+ }
8394
+ if (ch === "/" && jsonc[i + 1] === "/") {
8395
+ while (i < len && jsonc[i] !== `
8396
+ `)
8397
+ i++;
8398
+ continue;
8399
+ }
8400
+ if (ch === "/" && jsonc[i + 1] === "*") {
8401
+ i += 2;
8402
+ while (i < len && !(jsonc[i] === "*" && jsonc[i + 1] === "/"))
8403
+ i++;
8404
+ i += 2;
8405
+ continue;
8406
+ }
8407
+ result.push(ch);
8408
+ i++;
8409
+ }
8410
+ return result.join("");
8411
+ }
8412
+ function readJsoncFile(filePath) {
8413
+ try {
8414
+ const raw = readFileSync(filePath, "utf-8");
8415
+ const stripped = stripJsonComments(raw);
8416
+ const parsed = JSON.parse(stripped);
8417
+ return parsed;
8418
+ } catch (err) {
8419
+ if (err.code !== "ENOENT") {
8420
+ console.warn(`[nexus] Failed to load config from ${filePath}: ${err.message}`);
8421
+ }
8422
+ return null;
8423
+ }
8424
+ }
8364
8425
  var DEFAULT_CONFIG = {
8365
8426
  models: {
8366
8427
  architect: "anthropic/claude-sonnet-4-6",
@@ -8388,12 +8449,18 @@ class NexusConfigManager {
8388
8449
  globalConfig = null;
8389
8450
  storageConfig = null;
8390
8451
  constructor() {
8391
- this.loadConfigs();
8392
- }
8393
- loadConfigs() {
8394
8452
  this.projectConfig = null;
8395
8453
  this.globalConfig = null;
8396
8454
  }
8455
+ loadConfigs(basePath) {
8456
+ const projectPath = join(basePath, ".opencode", "nexus.jsonc");
8457
+ this.projectConfig = readJsoncFile(projectPath);
8458
+ const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
8459
+ this.globalConfig = readJsoncFile(globalPath);
8460
+ }
8461
+ loadFromPath(basePath) {
8462
+ this.loadConfigs(basePath);
8463
+ }
8397
8464
  getConfig() {
8398
8465
  return {
8399
8466
  models: {
@@ -8451,24 +8518,108 @@ class NexusConfigManager {
8451
8518
  }
8452
8519
  getRoleDisplayName(role) {
8453
8520
  const names = {
8454
- architect: "\uD83C\uDFD7️ Architect",
8455
- coder: "\uD83D\uDCBB Coder",
8456
- reviewer: "\uD83D\uDD0D Reviewer",
8457
- tester: "\uD83E\uDDEA Tester",
8458
- explorer: "\uD83D\uDD2C Explorer",
8459
- documenter: "\uD83D\uDCDD Documenter"
8521
+ architect: "Architect",
8522
+ coder: "Coder",
8523
+ reviewer: "Reviewer",
8524
+ tester: "Tester",
8525
+ explorer: "Explorer",
8526
+ documenter: "Documenter"
8460
8527
  };
8461
8528
  return names[role] || role;
8462
8529
  }
8530
+ getRoleEmoji(role) {
8531
+ const emojis = {
8532
+ architect: "\uD83C\uDFD7️",
8533
+ coder: "\uD83D\uDCBB",
8534
+ reviewer: "\uD83D\uDD0D",
8535
+ tester: "\uD83E\uDDEA",
8536
+ explorer: "\uD83D\uDD2C",
8537
+ documenter: "\uD83D\uDCDD"
8538
+ };
8539
+ return emojis[role] || "\uD83E\uDD16";
8540
+ }
8463
8541
  exportConfig() {
8464
8542
  return this.getConfig();
8465
8543
  }
8466
8544
  importConfig(config) {
8467
8545
  this.storageConfig = { ...config };
8468
8546
  }
8547
+ writeJsoncFile(filePath, config) {
8548
+ const dir = dirname(filePath);
8549
+ mkdirSync(dir, { recursive: true });
8550
+ const jsonc = [
8551
+ "// Nexus Configuration — https://github.com/serkanalgur/opencode-nexus",
8552
+ "// Precedence: this file > global config > TUI settings > defaults",
8553
+ "",
8554
+ JSON.stringify(config, null, 2)
8555
+ ].join(`
8556
+ `);
8557
+ writeFileSync(filePath, jsonc + `
8558
+ `, "utf-8");
8559
+ }
8560
+ saveProjectConfig(basePath) {
8561
+ const projectPath = join(basePath, ".opencode", "nexus.jsonc");
8562
+ const config = this.getNonDefaultConfig();
8563
+ this.writeJsoncFile(projectPath, config);
8564
+ }
8565
+ saveGlobalConfig() {
8566
+ const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
8567
+ const config = this.getNonDefaultConfig();
8568
+ this.writeJsoncFile(globalPath, config);
8569
+ }
8570
+ saveConfig(level, basePath) {
8571
+ if (level === "project") {
8572
+ this.saveProjectConfig(basePath || process.cwd());
8573
+ } else {
8574
+ this.saveGlobalConfig();
8575
+ }
8576
+ }
8577
+ getNonDefaultConfig() {
8578
+ const current = this.getConfig();
8579
+ const result = {};
8580
+ const models = {};
8581
+ for (const role of this.getRoles()) {
8582
+ if (current.models[role] !== DEFAULT_CONFIG.models[role]) {
8583
+ models[role] = current.models[role];
8584
+ }
8585
+ }
8586
+ if (Object.keys(models).length > 0) {
8587
+ result.models = models;
8588
+ }
8589
+ const budget = {};
8590
+ const budgetKeys = ["maxTotalCost", "maxCostPerTask", "maxCostPerAgent", "alertThreshold"];
8591
+ for (const key of budgetKeys) {
8592
+ if (current.budget[key] !== DEFAULT_CONFIG.budget[key]) {
8593
+ budget[key] = current.budget[key];
8594
+ }
8595
+ }
8596
+ if (Object.keys(budget).length > 0) {
8597
+ result.budget = budget;
8598
+ }
8599
+ const selfHealing = {};
8600
+ const shKeys = ["enabled", "maxRetries", "contextTransfer"];
8601
+ for (const key of shKeys) {
8602
+ if (current.selfHealing[key] !== DEFAULT_CONFIG.selfHealing[key]) {
8603
+ selfHealing[key] = current.selfHealing[key];
8604
+ }
8605
+ }
8606
+ if (Object.keys(selfHealing).length > 0) {
8607
+ result.selfHealing = selfHealing;
8608
+ }
8609
+ return result;
8610
+ }
8469
8611
  resetToDefaults() {
8470
8612
  this.storageConfig = null;
8471
8613
  }
8614
+ applyPreset(name) {
8615
+ const preset = PRESETS[name];
8616
+ if (!preset)
8617
+ throw new Error(`Unknown preset: ${name}. Available: ${Object.keys(PRESETS).join(", ")}`);
8618
+ this.storageConfig = { ...this.storageConfig, ...preset.config };
8619
+ }
8620
+ listPresets() {
8621
+ return Object.keys(PRESETS);
8622
+ }
8472
8623
  getSummary() {
8473
8624
  const config = this.getConfig();
8474
8625
  const lines = [
@@ -8496,212 +8647,1423 @@ class NexusConfigManager {
8496
8647
  `);
8497
8648
  }
8498
8649
  }
8499
-
8500
- // src/orchestrator.ts
8501
- class NexusOrchestrator {
8502
- agents = new Map;
8503
- tasks = new Map;
8504
- dag = null;
8505
- config;
8506
- budget;
8507
- running = false;
8508
- paused = false;
8509
- totalSpent = 0;
8510
- costByAgent = new Map;
8511
- costByModel = new Map;
8512
- messageQueue = [];
8513
- subscribers = new Map;
8514
- memory = new Map;
8515
- eventHandlers = new Map;
8516
- configManager;
8517
- constructor(config) {
8518
- this.config = this.mergeConfig(config);
8519
- this.budget = this.config.budget;
8520
- this.configManager = new NexusConfigManager;
8521
- }
8522
- mergeConfig(partial) {
8523
- const defaults = {
8524
- maxConcurrency: 5,
8525
- schedulerInterval: 1000,
8526
- defaultTimeout: 300000,
8650
+ var PRESETS = {
8651
+ minimal: {
8652
+ name: "Minimal",
8653
+ description: "Low-cost setup with free/cheap models, minimal budget",
8654
+ config: {
8655
+ models: {
8656
+ architect: "google/gemini-2.5-flash",
8657
+ coder: "google/gemini-2.5-flash",
8658
+ reviewer: "google/gemini-2.5-flash",
8659
+ tester: "google/gemini-2.5-flash",
8660
+ explorer: "google/gemini-2.5-flash",
8661
+ documenter: "google/gemini-2.5-flash"
8662
+ },
8663
+ budget: {
8664
+ maxTotalCost: 1,
8665
+ maxCostPerTask: 0.1,
8666
+ maxCostPerAgent: 0.5,
8667
+ alertThreshold: 0.5
8668
+ },
8669
+ selfHealing: {
8670
+ enabled: false,
8671
+ maxRetries: 1,
8672
+ contextTransfer: false
8673
+ }
8674
+ }
8675
+ },
8676
+ balanced: {
8677
+ name: "Balanced",
8678
+ description: "Good quality models with moderate budget",
8679
+ config: {
8680
+ models: {
8681
+ architect: "anthropic/claude-sonnet-4-6",
8682
+ coder: "anthropic/claude-sonnet-4-6",
8683
+ reviewer: "openai/gpt-5-mini",
8684
+ tester: "anthropic/claude-haiku-4-5",
8685
+ explorer: "google/gemini-2.5-flash",
8686
+ documenter: "anthropic/claude-haiku-4-5"
8687
+ },
8527
8688
  budget: {
8528
8689
  maxTotalCost: 10,
8529
8690
  maxCostPerTask: 1,
8530
8691
  maxCostPerAgent: 2,
8531
- alertThreshold: 0.2,
8532
- hardLimit: false
8533
- },
8534
- agents: {
8535
- defaultRole: "coder",
8536
- spawnDelay: 100,
8537
- healthCheckInterval: 30000
8692
+ alertThreshold: 0.2
8538
8693
  },
8539
8694
  selfHealing: {
8540
8695
  enabled: true,
8541
8696
  maxRetries: 3,
8542
- retryDelay: 1000,
8543
- backoffMultiplier: 2,
8544
8697
  contextTransfer: true
8698
+ }
8699
+ }
8700
+ },
8701
+ enterprise: {
8702
+ name: "Enterprise",
8703
+ description: "High-quality models with generous budget and full self-healing",
8704
+ config: {
8705
+ models: {
8706
+ architect: "anthropic/claude-sonnet-4-6",
8707
+ coder: "anthropic/claude-sonnet-4-6",
8708
+ reviewer: "openai/gpt-5",
8709
+ tester: "anthropic/claude-sonnet-4-6",
8710
+ explorer: "anthropic/claude-sonnet-4-6",
8711
+ documenter: "anthropic/claude-haiku-4-5"
8545
8712
  },
8546
- communication: {
8547
- mode: "pubsub",
8548
- maxQueueSize: 100,
8549
- messageTTL: 60000,
8550
- persistence: false
8713
+ budget: {
8714
+ maxTotalCost: 50,
8715
+ maxCostPerTask: 5,
8716
+ maxCostPerAgent: 10,
8717
+ alertThreshold: 0.1
8551
8718
  },
8552
- memory: {
8719
+ selfHealing: {
8553
8720
  enabled: true,
8554
- storage: "memory",
8555
- maxEntriesPerScope: 1000,
8556
- syncInterval: 5000
8557
- },
8558
- dashboard: {
8559
- enabled: false,
8560
- port: 4747,
8561
- host: "127.0.0.1"
8721
+ maxRetries: 5,
8722
+ contextTransfer: true
8723
+ }
8724
+ }
8725
+ },
8726
+ "cost-optimized": {
8727
+ name: "Cost-Optimized",
8728
+ description: "Minimize costs while maintaining reasonable quality",
8729
+ config: {
8730
+ models: {
8731
+ architect: "anthropic/claude-haiku-4-5",
8732
+ coder: "google/gemini-2.5-flash",
8733
+ reviewer: "openai/gpt-5-mini",
8734
+ tester: "google/gemini-2.5-flash",
8735
+ explorer: "google/gemini-2.5-flash",
8736
+ documenter: "google/gemini-2.5-flash"
8562
8737
  },
8563
- security: {
8564
- sastEnabled: true,
8565
- secretsScanning: true,
8566
- scopeEnforcement: true
8738
+ budget: {
8739
+ maxTotalCost: 3,
8740
+ maxCostPerTask: 0.3,
8741
+ maxCostPerAgent: 1,
8742
+ alertThreshold: 0.3
8567
8743
  },
8568
- learning: {
8744
+ selfHealing: {
8569
8745
  enabled: true,
8570
- patternStorage: "memory",
8571
- minConfidence: 0.7
8746
+ maxRetries: 2,
8747
+ contextTransfer: false
8572
8748
  }
8573
- };
8574
- return {
8575
- ...defaults,
8576
- ...partial,
8577
- budget: { ...defaults.budget, ...partial?.budget },
8578
- agents: { ...defaults.agents, ...partial?.agents },
8579
- selfHealing: { ...defaults.selfHealing, ...partial?.selfHealing },
8580
- communication: { ...defaults.communication, ...partial?.communication },
8581
- memory: { ...defaults.memory, ...partial?.memory },
8582
- dashboard: { ...defaults.dashboard, ...partial?.dashboard },
8583
- security: { ...defaults.security, ...partial?.security },
8584
- learning: { ...defaults.learning, ...partial?.learning }
8585
- };
8586
- }
8587
- async execute(request) {
8588
- if (this.running) {
8589
- throw new Error("Orchestrator is already running");
8590
8749
  }
8591
- this.running = true;
8592
- const startTime = Date.now();
8593
- try {
8594
- this.dag = this.buildDAG(request.tasks);
8595
- if (request.budget) {
8596
- this.budget = request.budget;
8597
- }
8598
- await this.executeDAG();
8599
- const results = this.collectResults();
8600
- const totalDuration = Date.now() - startTime;
8601
- return {
8602
- success: true,
8603
- tasks: results,
8604
- totalCost: this.totalSpent,
8605
- totalDuration,
8606
- agentsUsed: this.agents.size
8607
- };
8608
- } catch (error) {
8609
- return {
8610
- success: false,
8611
- tasks: [],
8612
- totalCost: this.totalSpent,
8613
- totalDuration: Date.now() - startTime,
8614
- agentsUsed: this.agents.size
8615
- };
8616
- } finally {
8617
- this.running = false;
8750
+ }
8751
+ };
8752
+
8753
+ // src/broadcast.ts
8754
+ class StateBroadcaster {
8755
+ clients = new Set;
8756
+ orchestrator;
8757
+ broadcastTimer = null;
8758
+ throttleMs;
8759
+ unsubscribers = [];
8760
+ constructor(orchestrator, opts) {
8761
+ this.orchestrator = orchestrator;
8762
+ this.throttleMs = opts?.throttleMs ?? 1000;
8763
+ this.setupEventListeners();
8764
+ }
8765
+ addClient(ws) {
8766
+ this.clients.add(ws);
8767
+ this.sendTo(ws, {
8768
+ type: "orchestrator:state",
8769
+ data: this.orchestrator.getState(),
8770
+ timestamp: new Date().toISOString()
8771
+ });
8772
+ }
8773
+ removeClient(ws) {
8774
+ this.clients.delete(ws);
8775
+ }
8776
+ setupEventListeners() {
8777
+ const orch = this.orchestrator;
8778
+ const eventNames = [
8779
+ "agent:spawned",
8780
+ "agent:terminated",
8781
+ "budget:alert",
8782
+ "budget:exceeded"
8783
+ ];
8784
+ for (const eventName of eventNames) {
8785
+ const unsub = orch.on?.(eventName, (data) => {
8786
+ this.broadcast(eventName, data);
8787
+ });
8788
+ if (unsub)
8789
+ this.unsubscribers.push(unsub);
8618
8790
  }
8619
8791
  }
8620
- buildDAG(tasks) {
8621
- const dag = {
8622
- nodes: new Map,
8623
- addNode: (node) => {
8624
- dag.nodes.set(node.id, node);
8625
- },
8626
- addDependency: (nodeId, dependsOn) => {
8627
- const node = dag.nodes.get(nodeId);
8628
- if (node) {
8629
- node.dependencies.push(dependsOn);
8630
- }
8631
- },
8632
- removeNode: (nodeId) => {
8633
- dag.nodes.delete(nodeId);
8634
- },
8635
- getReadyNodes: () => {
8636
- const ready = [];
8637
- dag.nodes.forEach((node) => {
8638
- if (node.status === "pending") {
8639
- const depsComplete = node.dependencies.every((dep) => {
8640
- const depNode = dag.nodes.get(dep);
8641
- return depNode?.status === "completed";
8642
- });
8643
- if (depsComplete) {
8644
- ready.push(node);
8645
- }
8646
- }
8647
- });
8648
- return ready;
8649
- },
8650
- markComplete: (nodeId, result) => {
8651
- const node = dag.nodes.get(nodeId);
8652
- if (node) {
8653
- node.status = "completed";
8654
- node.result = result;
8655
- }
8656
- },
8657
- markFailed: (nodeId, error) => {
8658
- const node = dag.nodes.get(nodeId);
8659
- if (node) {
8660
- node.status = "failed";
8661
- }
8662
- },
8663
- getParallelGroups: () => {
8664
- const groups = [];
8665
- const ready = dag.getReadyNodes();
8666
- if (ready.length > 0) {
8667
- groups.push(ready);
8668
- }
8669
- return groups;
8670
- },
8671
- isComplete: () => {
8672
- let complete = true;
8673
- dag.nodes.forEach((node) => {
8674
- if (node.status !== "completed" && node.status !== "failed") {
8675
- complete = false;
8676
- }
8677
- });
8678
- return complete;
8792
+ broadcastState() {
8793
+ if (this.broadcastTimer)
8794
+ return;
8795
+ this.broadcastTimer = setTimeout(() => {
8796
+ this.broadcastTimer = null;
8797
+ this.broadcast("orchestrator:state", this.orchestrator.getState());
8798
+ }, this.throttleMs);
8799
+ }
8800
+ broadcast(event, data) {
8801
+ const message = JSON.stringify({
8802
+ type: event,
8803
+ data,
8804
+ timestamp: new Date().toISOString()
8805
+ });
8806
+ for (const client of this.clients) {
8807
+ try {
8808
+ client.send(message);
8809
+ } catch {
8810
+ this.clients.delete(client);
8679
8811
  }
8680
- };
8681
- for (const task of tasks) {
8682
- const node = {
8683
- id: task.id,
8684
- task,
8685
- dependencies: task.dependencies,
8686
- status: "pending"
8687
- };
8688
- dag.addNode(node);
8689
8812
  }
8690
- return dag;
8691
8813
  }
8692
- async executeDAG() {
8693
- while (!this.dag.isComplete() && !this.paused) {
8694
- const readyNodes = this.dag.getReadyNodes();
8695
- for (const node of readyNodes) {
8696
- if (this.agents.size < this.config.maxConcurrency) {
8697
- await this.spawnAndExecute(node);
8698
- }
8699
- }
8700
- await this.sleep(this.config.schedulerInterval);
8814
+ sendTo(ws, data) {
8815
+ try {
8816
+ ws.send(JSON.stringify(data));
8817
+ } catch {
8818
+ this.clients.delete(ws);
8701
8819
  }
8702
8820
  }
8703
- async spawnAndExecute(node) {
8704
- const complexity = this.analyzeComplexity(node.task);
8821
+ getClientCount() {
8822
+ return this.clients.size;
8823
+ }
8824
+ destroy() {
8825
+ if (this.broadcastTimer) {
8826
+ clearTimeout(this.broadcastTimer);
8827
+ this.broadcastTimer = null;
8828
+ }
8829
+ for (const unsub of this.unsubscribers) {
8830
+ unsub();
8831
+ }
8832
+ this.unsubscribers = [];
8833
+ this.clients.clear();
8834
+ }
8835
+ }
8836
+
8837
+ // src/dashboard.ts
8838
+ var CORS_HEADERS = {
8839
+ "Access-Control-Allow-Origin": "*",
8840
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
8841
+ "Access-Control-Allow-Headers": "Content-Type"
8842
+ };
8843
+ function jsonResponse(data, status = 200) {
8844
+ return new Response(JSON.stringify(data), {
8845
+ status,
8846
+ headers: {
8847
+ "Content-Type": "application/json",
8848
+ ...CORS_HEADERS
8849
+ }
8850
+ });
8851
+ }
8852
+
8853
+ class DashboardModule {
8854
+ server = null;
8855
+ clients = new Set;
8856
+ orchestrator;
8857
+ constructor(orchestrator) {
8858
+ this.orchestrator = orchestrator;
8859
+ }
8860
+ start(port, host) {
8861
+ const self = this;
8862
+ try {
8863
+ this.server = Bun.serve({
8864
+ port,
8865
+ hostname: host,
8866
+ fetch(req, server) {
8867
+ const url = new URL(req.url);
8868
+ if (req.method === "OPTIONS") {
8869
+ return new Response(null, { headers: CORS_HEADERS });
8870
+ }
8871
+ if (url.pathname === "/ws/events") {
8872
+ if (server.upgrade(req, { data: {} }))
8873
+ return new Response(null);
8874
+ return new Response("WebSocket upgrade failed", { status: 500 });
8875
+ }
8876
+ if (url.pathname === "/api/state") {
8877
+ return jsonResponse(self.orchestrator.getState());
8878
+ }
8879
+ if (url.pathname === "/api/config") {
8880
+ return jsonResponse(self.orchestrator.configManager.exportConfig());
8881
+ }
8882
+ if (url.pathname === "/api/agents") {
8883
+ const state = self.orchestrator.getState();
8884
+ return jsonResponse(state.agents);
8885
+ }
8886
+ if (url.pathname === "/api/costs") {
8887
+ return jsonResponse(self.orchestrator.getCostReport());
8888
+ }
8889
+ if (url.pathname === "/api/health") {
8890
+ return jsonResponse({ ok: true, uptime: process.uptime() });
8891
+ }
8892
+ return new Response("Nexus Dashboard API", {
8893
+ headers: {
8894
+ "Content-Type": "text/plain",
8895
+ ...CORS_HEADERS
8896
+ }
8897
+ });
8898
+ },
8899
+ websocket: {
8900
+ open(ws) {
8901
+ self.clients.add(ws);
8902
+ ws.send(JSON.stringify({
8903
+ type: "orchestrator:state",
8904
+ data: self.orchestrator.getState(),
8905
+ timestamp: new Date().toISOString()
8906
+ }));
8907
+ },
8908
+ message(ws, message) {
8909
+ try {
8910
+ const msg = JSON.parse(typeof message === "string" ? message : message.toString());
8911
+ if (msg.type === "ping") {
8912
+ ws.send(JSON.stringify({
8913
+ type: "pong",
8914
+ timestamp: new Date().toISOString()
8915
+ }));
8916
+ }
8917
+ } catch {}
8918
+ },
8919
+ close(ws) {
8920
+ self.clients.delete(ws);
8921
+ }
8922
+ }
8923
+ });
8924
+ console.log(`[nexus] Dashboard server running at http://${host}:${port}`);
8925
+ } catch (err) {
8926
+ this.server = null;
8927
+ console.error(`[nexus] Failed to start dashboard on ${host}:${port}: ${err.message}`);
8928
+ throw new Error(`Dashboard failed to start on port ${port}: ${err.message}`);
8929
+ }
8930
+ }
8931
+ broadcast(event, data) {
8932
+ const message = JSON.stringify({
8933
+ type: event,
8934
+ data,
8935
+ timestamp: new Date().toISOString()
8936
+ });
8937
+ for (const client of this.clients) {
8938
+ try {
8939
+ client.send(message);
8940
+ } catch {
8941
+ this.clients.delete(client);
8942
+ }
8943
+ }
8944
+ }
8945
+ stop() {
8946
+ if (this.server) {
8947
+ this.server.stop();
8948
+ this.server = null;
8949
+ }
8950
+ this.clients.clear();
8951
+ }
8952
+ getClientCount() {
8953
+ return this.clients.size;
8954
+ }
8955
+ isRunning() {
8956
+ return this.server !== null;
8957
+ }
8958
+ }
8959
+
8960
+ // src/dag.ts
8961
+ function detectCycles(nodes) {
8962
+ const cycles = [];
8963
+ const visited = new Set;
8964
+ const inStack = new Set;
8965
+ const path = [];
8966
+ function dfs(nodeId) {
8967
+ if (inStack.has(nodeId)) {
8968
+ const cycleStart = path.indexOf(nodeId);
8969
+ cycles.push([...path.slice(cycleStart), nodeId]);
8970
+ return;
8971
+ }
8972
+ if (visited.has(nodeId))
8973
+ return;
8974
+ visited.add(nodeId);
8975
+ inStack.add(nodeId);
8976
+ path.push(nodeId);
8977
+ const node = nodes.find((n) => n.id === nodeId);
8978
+ if (node) {
8979
+ for (const depId of node.dependencies) {
8980
+ dfs(depId);
8981
+ }
8982
+ }
8983
+ path.pop();
8984
+ inStack.delete(nodeId);
8985
+ }
8986
+ for (const node of nodes) {
8987
+ dfs(node.id);
8988
+ }
8989
+ return cycles;
8990
+ }
8991
+
8992
+ // src/message-store.ts
8993
+ import { appendFileSync, readFileSync as readFileSync2, existsSync, mkdirSync as mkdirSync2 } from "node:fs";
8994
+ import { join as join2, dirname as dirname2 } from "node:path";
8995
+ var DEFAULT_CONFIG2 = {
8996
+ storagePath: join2(process.env.HOME || "~", ".local", "share", "opencode-nexus", "messages.jsonl"),
8997
+ maxMessages: 1e4,
8998
+ rotationSize: 1000
8999
+ };
9000
+
9001
+ class MessageStore {
9002
+ messages = [];
9003
+ config;
9004
+ writeCount = 0;
9005
+ constructor(config) {
9006
+ this.config = { ...DEFAULT_CONFIG2, ...config };
9007
+ this.loadFromDisk();
9008
+ }
9009
+ loadFromDisk() {
9010
+ try {
9011
+ if (existsSync(this.config.storagePath)) {
9012
+ const raw = readFileSync2(this.config.storagePath, "utf-8");
9013
+ const lines = raw.split(`
9014
+ `).filter((l) => l.trim());
9015
+ this.messages = lines.map((l) => JSON.parse(l)).slice(-this.config.maxMessages);
9016
+ }
9017
+ } catch {
9018
+ this.messages = [];
9019
+ }
9020
+ }
9021
+ ensureDir() {
9022
+ try {
9023
+ mkdirSync2(dirname2(this.config.storagePath), { recursive: true });
9024
+ } catch {}
9025
+ }
9026
+ add(message) {
9027
+ this.messages.push(message);
9028
+ if (this.messages.length > this.config.maxMessages) {
9029
+ this.messages = this.messages.slice(-this.config.maxMessages);
9030
+ }
9031
+ try {
9032
+ this.ensureDir();
9033
+ appendFileSync(this.config.storagePath, JSON.stringify(message) + `
9034
+ `);
9035
+ this.writeCount++;
9036
+ if (this.writeCount % this.config.rotationSize === 0) {
9037
+ this.rotate();
9038
+ }
9039
+ } catch {}
9040
+ }
9041
+ rotate() {
9042
+ try {
9043
+ const content = this.messages.map((m) => JSON.stringify(m)).join(`
9044
+ `) + `
9045
+ `;
9046
+ const { writeFileSync } = __require("node:fs");
9047
+ writeFileSync(this.config.storagePath, content);
9048
+ } catch {}
9049
+ }
9050
+ getAll() {
9051
+ return [...this.messages];
9052
+ }
9053
+ getByTopic(topic) {
9054
+ return this.messages.filter((m) => m.topic === topic);
9055
+ }
9056
+ getByAgent(agentId) {
9057
+ return this.messages.filter((m) => m.from === agentId || m.to === agentId);
9058
+ }
9059
+ getRecent(count) {
9060
+ return this.messages.slice(-count);
9061
+ }
9062
+ clear() {
9063
+ this.messages = [];
9064
+ this.writeCount = 0;
9065
+ }
9066
+ getStats() {
9067
+ let fileSize = 0;
9068
+ try {
9069
+ if (existsSync(this.config.storagePath)) {
9070
+ const { statSync } = __require("node:fs");
9071
+ fileSize = statSync(this.config.storagePath).size;
9072
+ }
9073
+ } catch {}
9074
+ return {
9075
+ total: this.messages.length,
9076
+ writeCount: this.writeCount,
9077
+ fileSize
9078
+ };
9079
+ }
9080
+ }
9081
+
9082
+ // src/memory-store.ts
9083
+ import { Database } from "bun:sqlite";
9084
+ import { join as join3, dirname as dirname3 } from "node:path";
9085
+ import { mkdirSync as mkdirSync3 } from "node:fs";
9086
+ var DEFAULT_CONFIG3 = {
9087
+ dbPath: join3(process.env.HOME || "~", ".local", "share", "opencode-nexus", "memory.db"),
9088
+ defaultTTL: 0,
9089
+ maxEntries: 1e4
9090
+ };
9091
+
9092
+ class PersistentMemoryStore {
9093
+ db;
9094
+ config;
9095
+ constructor(config) {
9096
+ this.config = { ...DEFAULT_CONFIG3, ...config };
9097
+ try {
9098
+ mkdirSync3(dirname3(this.config.dbPath), { recursive: true });
9099
+ } catch {}
9100
+ this.db = new Database(this.config.dbPath);
9101
+ this.init();
9102
+ }
9103
+ init() {
9104
+ this.db.exec(`
9105
+ CREATE TABLE IF NOT EXISTS memory (
9106
+ id TEXT PRIMARY KEY,
9107
+ key TEXT NOT NULL,
9108
+ value TEXT NOT NULL,
9109
+ scope TEXT NOT NULL,
9110
+ author TEXT NOT NULL,
9111
+ timestamp INTEGER NOT NULL,
9112
+ confidence REAL DEFAULT 1.0,
9113
+ tags TEXT DEFAULT '[]',
9114
+ ttl INTEGER DEFAULT 0,
9115
+ expires_at INTEGER
9116
+ );
9117
+ CREATE INDEX IF NOT EXISTS idx_memory_scope ON memory(scope);
9118
+ CREATE INDEX IF NOT EXISTS idx_memory_key ON memory(key);
9119
+ CREATE INDEX IF NOT EXISTS idx_memory_author ON memory(author);
9120
+ `);
9121
+ }
9122
+ set(entry) {
9123
+ const id = `mem-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
9124
+ const timestamp = Date.now();
9125
+ const expiresAt = entry.ttl ? timestamp + entry.ttl : null;
9126
+ const stmt = this.db.prepare(`
9127
+ INSERT OR REPLACE INTO memory (id, key, value, scope, author, timestamp, confidence, tags, ttl, expires_at)
9128
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9129
+ `);
9130
+ stmt.run(id, entry.key, JSON.stringify(entry.value), entry.scope, entry.author, timestamp, entry.confidence, JSON.stringify(entry.tags || []), entry.ttl || 0, expiresAt);
9131
+ return { ...entry, id, timestamp: new Date(timestamp) };
9132
+ }
9133
+ get(key, scope) {
9134
+ const entries = this.getByKey(key, scope);
9135
+ return entries[entries.length - 1] || null;
9136
+ }
9137
+ getByKey(key, scope) {
9138
+ let query = "SELECT * FROM memory WHERE key = ?";
9139
+ const params = [key];
9140
+ if (scope) {
9141
+ query += " AND scope = ?";
9142
+ params.push(scope);
9143
+ }
9144
+ query += " AND (expires_at IS NULL OR expires_at > ?)";
9145
+ params.push(Date.now());
9146
+ const rows = this.db.prepare(query).all(...params);
9147
+ return rows.map(this.rowToEntry);
9148
+ }
9149
+ getByScope(scope) {
9150
+ const rows = this.db.prepare("SELECT * FROM memory WHERE scope = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC").all(scope, Date.now());
9151
+ return rows.map(this.rowToEntry);
9152
+ }
9153
+ search(query) {
9154
+ const rows = this.db.prepare(`SELECT * FROM memory WHERE (key LIKE ? OR value LIKE ?) AND (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC`).all(`%${query}%`, `%${query}%`, Date.now());
9155
+ return rows.map(this.rowToEntry);
9156
+ }
9157
+ getRecent(count) {
9158
+ const rows = this.db.prepare("SELECT * FROM memory WHERE (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC LIMIT ?").all(Date.now(), count);
9159
+ return rows.map(this.rowToEntry);
9160
+ }
9161
+ getByAuthor(author) {
9162
+ const rows = this.db.prepare("SELECT * FROM memory WHERE author = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY timestamp DESC").all(author, Date.now());
9163
+ return rows.map(this.rowToEntry);
9164
+ }
9165
+ delete(key, scope) {
9166
+ let query = "DELETE FROM memory WHERE key = ?";
9167
+ const params = [key];
9168
+ if (scope) {
9169
+ query += " AND scope = ?";
9170
+ params.push(scope);
9171
+ }
9172
+ const result = this.db.prepare(query).run(...params);
9173
+ return result.changes > 0;
9174
+ }
9175
+ clear(scope) {
9176
+ let query = "DELETE FROM memory";
9177
+ const params = [];
9178
+ if (scope) {
9179
+ query += " WHERE scope = ?";
9180
+ params.push(scope);
9181
+ }
9182
+ const result = this.db.prepare(query).run(...params);
9183
+ return result.changes;
9184
+ }
9185
+ getStats() {
9186
+ const total = this.db.prepare("SELECT COUNT(*) as count FROM memory").get().count;
9187
+ const byScope = {};
9188
+ const scopes = this.db.prepare("SELECT DISTINCT scope FROM memory").all();
9189
+ for (const { scope } of scopes) {
9190
+ byScope[scope] = this.db.prepare("SELECT COUNT(*) as count FROM memory WHERE scope = ?").get(scope).count;
9191
+ }
9192
+ const expired = this.db.prepare("SELECT COUNT(*) as count FROM memory WHERE expires_at IS NOT NULL AND expires_at <= ?").get(Date.now()).count;
9193
+ return { total, byScope, expired };
9194
+ }
9195
+ rowToEntry(row) {
9196
+ let value;
9197
+ try {
9198
+ value = JSON.parse(row.value);
9199
+ } catch {
9200
+ value = row.value;
9201
+ }
9202
+ let tags = [];
9203
+ try {
9204
+ tags = JSON.parse(row.tags || "[]");
9205
+ } catch {
9206
+ tags = [];
9207
+ }
9208
+ return {
9209
+ id: row.id,
9210
+ key: row.key,
9211
+ value,
9212
+ scope: row.scope,
9213
+ author: row.author,
9214
+ timestamp: new Date(row.timestamp),
9215
+ confidence: row.confidence,
9216
+ tags,
9217
+ ttl: row.ttl || undefined
9218
+ };
9219
+ }
9220
+ close() {
9221
+ this.db.close();
9222
+ }
9223
+ }
9224
+
9225
+ // src/health.ts
9226
+ var DEFAULT_HEALTH_CONFIG = {
9227
+ checkInterval: 30000,
9228
+ unhealthyThreshold: 0.5,
9229
+ degradedThreshold: 1e4
9230
+ };
9231
+
9232
+ class HealthMonitor {
9233
+ checks = new Map;
9234
+ interval = null;
9235
+ config;
9236
+ constructor(config) {
9237
+ this.config = { ...DEFAULT_HEALTH_CONFIG, ...config };
9238
+ }
9239
+ start(getAgents) {
9240
+ this.stop();
9241
+ this.interval = setInterval(() => {
9242
+ const agents = getAgents();
9243
+ for (const agent of agents) {
9244
+ if (agent.status === "working" || agent.status === "idle") {
9245
+ this.checkAgent(agent);
9246
+ }
9247
+ }
9248
+ }, this.config.checkInterval);
9249
+ }
9250
+ stop() {
9251
+ if (this.interval) {
9252
+ clearInterval(this.interval);
9253
+ this.interval = null;
9254
+ }
9255
+ }
9256
+ checkAgent(agent) {
9257
+ const errorRate = agent.metrics.tasksFailed / Math.max(1, agent.metrics.tasksCompleted + agent.metrics.tasksFailed);
9258
+ const avgResponseTime = agent.metrics.averageResponseTime || 0;
9259
+ let status = "healthy";
9260
+ if (errorRate > this.config.unhealthyThreshold)
9261
+ status = "unhealthy";
9262
+ else if (avgResponseTime > this.config.degradedThreshold)
9263
+ status = "degraded";
9264
+ const check = {
9265
+ agentId: agent.id,
9266
+ timestamp: new Date,
9267
+ responseTime: avgResponseTime,
9268
+ errorRate,
9269
+ tokensPerSecond: 0,
9270
+ memoryUsage: 0,
9271
+ status
9272
+ };
9273
+ const history = this.checks.get(agent.id) || [];
9274
+ history.push(check);
9275
+ if (history.length > 100)
9276
+ history.shift();
9277
+ this.checks.set(agent.id, history);
9278
+ return check;
9279
+ }
9280
+ getHealth(agentId) {
9281
+ const history = this.checks.get(agentId);
9282
+ return history?.[history.length - 1] || null;
9283
+ }
9284
+ getHistory(agentId) {
9285
+ return this.checks.get(agentId) || [];
9286
+ }
9287
+ getUnhealthyAgents() {
9288
+ const unhealthy = [];
9289
+ for (const [agentId, history] of this.checks) {
9290
+ const latest = history[history.length - 1];
9291
+ if (latest?.status === "unhealthy")
9292
+ unhealthy.push(agentId);
9293
+ }
9294
+ return unhealthy;
9295
+ }
9296
+ getConfig() {
9297
+ return { ...this.config };
9298
+ }
9299
+ isActive() {
9300
+ return this.interval !== null;
9301
+ }
9302
+ }
9303
+
9304
+ // src/fanout.ts
9305
+ class MessageRouter {
9306
+ routes = new Map;
9307
+ wildcardSubscribers = new Set;
9308
+ messageHandlers = new Map;
9309
+ subscribe(subscriberId, topic, handler) {
9310
+ this.messageHandlers.set(subscriberId, handler);
9311
+ if (topic === "*") {
9312
+ this.wildcardSubscribers.add(subscriberId);
9313
+ } else {
9314
+ const subs = this.routes.get(topic) || new Set;
9315
+ subs.add(subscriberId);
9316
+ this.routes.set(topic, subs);
9317
+ }
9318
+ }
9319
+ unsubscribe(subscriberId) {
9320
+ this.messageHandlers.delete(subscriberId);
9321
+ this.wildcardSubscribers.delete(subscriberId);
9322
+ for (const subs of this.routes.values()) {
9323
+ subs.delete(subscriberId);
9324
+ }
9325
+ }
9326
+ route(message) {
9327
+ const deliveredTo = [];
9328
+ const exactSubs = this.routes.get(message.topic);
9329
+ if (exactSubs) {
9330
+ for (const subId of exactSubs) {
9331
+ const handler = this.messageHandlers.get(subId);
9332
+ if (handler) {
9333
+ handler(message);
9334
+ deliveredTo.push(subId);
9335
+ }
9336
+ }
9337
+ }
9338
+ for (const subId of this.wildcardSubscribers) {
9339
+ if (!deliveredTo.includes(subId)) {
9340
+ const handler = this.messageHandlers.get(subId);
9341
+ if (handler) {
9342
+ handler(message);
9343
+ deliveredTo.push(subId);
9344
+ }
9345
+ }
9346
+ }
9347
+ return deliveredTo;
9348
+ }
9349
+ broadcast(message) {
9350
+ for (const handler of this.messageHandlers.values()) {
9351
+ handler(message);
9352
+ }
9353
+ }
9354
+ getSubscribers(topic) {
9355
+ const subs = this.routes.get(topic);
9356
+ return subs ? [...subs] : [];
9357
+ }
9358
+ getWildcardSubscribers() {
9359
+ return [...this.wildcardSubscribers];
9360
+ }
9361
+ getStats() {
9362
+ return {
9363
+ topics: this.routes.size,
9364
+ totalSubscribers: this.messageHandlers.size,
9365
+ wildcardSubscribers: this.wildcardSubscribers.size
9366
+ };
9367
+ }
9368
+ }
9369
+
9370
+ // src/notifications.ts
9371
+ import { spawn } from "node:child_process";
9372
+
9373
+ class NotificationManager {
9374
+ enabled;
9375
+ platform;
9376
+ constructor(enabled = true) {
9377
+ this.enabled = enabled;
9378
+ this.platform = process.platform;
9379
+ }
9380
+ async notify(options) {
9381
+ if (!this.enabled)
9382
+ return false;
9383
+ try {
9384
+ if (this.platform === "darwin") {
9385
+ return await this.notifyMacOS(options);
9386
+ } else if (this.platform === "linux") {
9387
+ return await this.notifyLinux(options);
9388
+ } else if (this.platform === "win32") {
9389
+ return await this.notifyWindows(options);
9390
+ }
9391
+ } catch {}
9392
+ return false;
9393
+ }
9394
+ async notifyMacOS(options) {
9395
+ const soundFlag = options.silent ? "" : options.sound !== false ? 'sound name "Glass"' : "";
9396
+ const script = `display notification "${options.body}" with title "${options.title}" ${soundFlag}`;
9397
+ return new Promise((resolve) => {
9398
+ const proc = spawn("osascript", ["-e", script]);
9399
+ proc.on("close", (code) => resolve(code === 0));
9400
+ proc.on("error", () => resolve(false));
9401
+ setTimeout(() => {
9402
+ proc.kill();
9403
+ resolve(false);
9404
+ }, 3000);
9405
+ });
9406
+ }
9407
+ async notifyLinux(options) {
9408
+ return new Promise((resolve) => {
9409
+ const proc = spawn("notify-send", [options.title, options.body]);
9410
+ proc.on("close", (code) => resolve(code === 0));
9411
+ proc.on("error", () => resolve(false));
9412
+ setTimeout(() => {
9413
+ proc.kill();
9414
+ resolve(false);
9415
+ }, 3000);
9416
+ });
9417
+ }
9418
+ async notifyWindows(options) {
9419
+ return new Promise((resolve) => {
9420
+ const proc = spawn("powershell", ["-Command", `New-BurntToastNotification -Text '${options.title}','${options.body}'`]);
9421
+ proc.on("close", (code) => resolve(code === 0));
9422
+ proc.on("error", () => resolve(false));
9423
+ setTimeout(() => {
9424
+ proc.kill();
9425
+ resolve(false);
9426
+ }, 3000);
9427
+ });
9428
+ }
9429
+ setEnabled(enabled) {
9430
+ this.enabled = enabled;
9431
+ }
9432
+ isEnabled() {
9433
+ return this.enabled;
9434
+ }
9435
+ }
9436
+
9437
+ // src/learning.ts
9438
+ class LearningModule {
9439
+ entries = new Map;
9440
+ minConfidence;
9441
+ constructor(minConfidence = 0.5) {
9442
+ this.minConfidence = minConfidence;
9443
+ }
9444
+ recordFailure(pattern, solution, context, tags = []) {
9445
+ const existing = this.findByPattern(pattern);
9446
+ if (existing) {
9447
+ existing.failCount++;
9448
+ existing.lastSeen = new Date;
9449
+ existing.confidence = Math.max(0, existing.confidence - 0.1);
9450
+ return existing;
9451
+ }
9452
+ const entry = {
9453
+ id: `learn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
9454
+ pattern,
9455
+ solution,
9456
+ context,
9457
+ confidence: 0.3,
9458
+ successCount: 0,
9459
+ failCount: 1,
9460
+ lastSeen: new Date,
9461
+ tags
9462
+ };
9463
+ this.entries.set(entry.id, entry);
9464
+ return entry;
9465
+ }
9466
+ recordSuccess(entryId) {
9467
+ const entry = this.entries.get(entryId);
9468
+ if (entry) {
9469
+ entry.successCount++;
9470
+ entry.confidence = Math.min(1, entry.confidence + 0.15);
9471
+ entry.lastSeen = new Date;
9472
+ }
9473
+ }
9474
+ findSolutions(errorPattern) {
9475
+ const matches = [];
9476
+ for (const entry of this.entries.values()) {
9477
+ if (entry.confidence < this.minConfidence)
9478
+ continue;
9479
+ const similarity = this.calculateSimilarity(errorPattern, entry.pattern);
9480
+ if (similarity > 0.3) {
9481
+ matches.push({ entry, similarity });
9482
+ }
9483
+ }
9484
+ return matches.sort((a, b) => b.similarity - a.similarity);
9485
+ }
9486
+ getReliable() {
9487
+ return [...this.entries.values()].filter((e) => e.confidence >= this.minConfidence && e.successCount > 0).sort((a, b) => b.confidence - a.confidence);
9488
+ }
9489
+ getStats() {
9490
+ const all = [...this.entries.values()];
9491
+ const reliable = all.filter((e) => e.confidence >= this.minConfidence && e.successCount > 0);
9492
+ const avgConfidence = all.length > 0 ? all.reduce((sum, e) => sum + e.confidence, 0) / all.length : 0;
9493
+ return { total: all.length, reliable: reliable.length, avgConfidence };
9494
+ }
9495
+ getEntries() {
9496
+ return [...this.entries.values()];
9497
+ }
9498
+ findByPattern(pattern) {
9499
+ for (const entry of this.entries.values()) {
9500
+ if (entry.pattern === pattern)
9501
+ return entry;
9502
+ }
9503
+ return;
9504
+ }
9505
+ calculateSimilarity(a, b) {
9506
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
9507
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
9508
+ const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
9509
+ const union = new Set([...wordsA, ...wordsB]);
9510
+ return union.size > 0 ? intersection.size / union.size : 0;
9511
+ }
9512
+ clear() {
9513
+ this.entries.clear();
9514
+ }
9515
+ }
9516
+
9517
+ // src/modules.ts
9518
+ class ModuleRegistry {
9519
+ modules = new Map;
9520
+ contexts = new Map;
9521
+ register(module) {
9522
+ if (this.modules.has(module.name)) {
9523
+ throw new Error(`Module '${module.name}' is already registered`);
9524
+ }
9525
+ this.modules.set(module.name, module);
9526
+ }
9527
+ async setupAll(ctx) {
9528
+ for (const [name, module] of this.modules) {
9529
+ try {
9530
+ if (module.setup) {
9531
+ await module.setup(ctx);
9532
+ this.contexts.set(name, ctx);
9533
+ }
9534
+ if (module.hooks) {
9535
+ for (const hook of module.hooks) {
9536
+ ctx.on(hook.event, hook.handler);
9537
+ }
9538
+ }
9539
+ } catch (err) {
9540
+ console.error(`[nexus] Failed to setup module '${name}':`, err);
9541
+ }
9542
+ }
9543
+ }
9544
+ async teardownAll() {
9545
+ for (const [name, module] of this.modules) {
9546
+ try {
9547
+ if (module.teardown) {
9548
+ await module.teardown();
9549
+ }
9550
+ } catch (err) {
9551
+ console.error(`[nexus] Failed to teardown module '${name}':`, err);
9552
+ }
9553
+ }
9554
+ this.contexts.clear();
9555
+ }
9556
+ get(name) {
9557
+ return this.modules.get(name);
9558
+ }
9559
+ list() {
9560
+ return [...this.modules.values()];
9561
+ }
9562
+ getTools() {
9563
+ const tools = [];
9564
+ for (const module of this.modules.values()) {
9565
+ if (module.tools) {
9566
+ tools.push(...module.tools);
9567
+ }
9568
+ }
9569
+ return tools;
9570
+ }
9571
+ isRegistered(name) {
9572
+ return this.modules.has(name);
9573
+ }
9574
+ unregister(name) {
9575
+ this.contexts.delete(name);
9576
+ return this.modules.delete(name);
9577
+ }
9578
+ }
9579
+
9580
+ // src/security.ts
9581
+ var DEFAULT_SECURITY_CONFIG = {
9582
+ enabled: true,
9583
+ scanSecrets: true,
9584
+ scanPatterns: true,
9585
+ customPatterns: [],
9586
+ excludeFiles: ["node_modules", ".git", "dist", "*.test.ts", "*.spec.ts"]
9587
+ };
9588
+ var SECRET_PATTERNS = [
9589
+ { pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible API key found" },
9590
+ { pattern: /(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible password/secret found" },
9591
+ { pattern: /(?:token|access_token|auth_token)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible token found" },
9592
+ { pattern: /(?:private[_-]?key)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "Possible private key found" },
9593
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, category: "secrets", message: "Private key block found" },
9594
+ { pattern: /(?:AWS|aws)(?:_ACCESS_KEY_ID|_SECRET_ACCESS_KEY)\s*[:=]\s*['"][^'"]+['"]/gi, category: "secrets", message: "AWS credentials found" }
9595
+ ];
9596
+ var DANGEROUS_PATTERNS = [
9597
+ { pattern: /eval\s*\(/g, category: "injection", message: "eval() usage detected", suggestion: "Use safer alternatives like JSON.parse() or Function constructor" },
9598
+ { pattern: /exec\s*\(/g, category: "injection", message: "exec() usage detected", suggestion: "Use execFile() or spawn() with argument arrays" },
9599
+ { pattern: /child_process/g, category: "injection", message: "child_process usage detected", suggestion: "Ensure user input is sanitized before shell execution" },
9600
+ { pattern: /innerHTML\s*=/g, category: "xss", message: "innerHTML assignment detected", suggestion: "Use textContent or sanitize input before setting innerHTML" },
9601
+ { pattern: /document\.write\s*\(/g, category: "xss", message: "document.write() usage detected", suggestion: "Use DOM manipulation methods instead" },
9602
+ { pattern: /new Function\s*\(/g, category: "injection", message: "Function constructor usage detected", suggestion: "Avoid dynamic code execution" },
9603
+ { pattern: /__proto__\s*=/g, category: "prototype-pollution", message: "__proto__ assignment detected", suggestion: "Use Object.create() or Object.assign() instead" },
9604
+ { pattern: /process\.env\[?[\"'][^\"']+[\"']\]?(?!\.replace)/g, category: "data-exposure", message: "Direct process.env access", suggestion: "Use a config module with validation" }
9605
+ ];
9606
+
9607
+ class SecurityScanner {
9608
+ config;
9609
+ issues = [];
9610
+ constructor(config) {
9611
+ this.config = { ...DEFAULT_SECURITY_CONFIG, ...config };
9612
+ }
9613
+ scanContent(content, filename) {
9614
+ if (!this.config.enabled)
9615
+ return [];
9616
+ if (this.isExcluded(filename))
9617
+ return [];
9618
+ const issues = [];
9619
+ if (this.config.scanSecrets) {
9620
+ for (const { pattern, category, message } of SECRET_PATTERNS) {
9621
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
9622
+ for (const match of matches) {
9623
+ const line = this.getLineNumber(content, match.index || 0);
9624
+ issues.push({
9625
+ id: `sec-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
9626
+ severity: "critical",
9627
+ category,
9628
+ message,
9629
+ file: filename,
9630
+ line,
9631
+ suggestion: "Remove hardcoded secrets and use environment variables or a secrets manager"
9632
+ });
9633
+ }
9634
+ }
9635
+ }
9636
+ if (this.config.scanPatterns) {
9637
+ for (const { pattern, category, message, suggestion } of DANGEROUS_PATTERNS) {
9638
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
9639
+ for (const match of matches) {
9640
+ const line = this.getLineNumber(content, match.index || 0);
9641
+ issues.push({
9642
+ id: `sec-${Date.now()}-${Math.random().toString(36).substr(6, 6)}`,
9643
+ severity: "medium",
9644
+ category,
9645
+ message,
9646
+ file: filename,
9647
+ line,
9648
+ suggestion
9649
+ });
9650
+ }
9651
+ }
9652
+ }
9653
+ for (const pattern of this.config.customPatterns) {
9654
+ const matches = content.matchAll(new RegExp(pattern.source, pattern.flags));
9655
+ for (const match of matches) {
9656
+ const line = this.getLineNumber(content, match.index || 0);
9657
+ issues.push({
9658
+ id: `sec-${Date.now()}-${Math.random().toString(36).substr(12, 6)}`,
9659
+ severity: "medium",
9660
+ category: "custom",
9661
+ message: `Custom pattern match in ${filename}`,
9662
+ file: filename,
9663
+ line
9664
+ });
9665
+ }
9666
+ }
9667
+ this.issues.push(...issues);
9668
+ return issues;
9669
+ }
9670
+ getResult() {
9671
+ const severityScores = { critical: 0, high: 10, medium: 25, low: 50 };
9672
+ let deductions = 0;
9673
+ for (const issue of this.issues) {
9674
+ deductions += severityScores[issue.severity] || 25;
9675
+ }
9676
+ const score = Math.max(0, 100 - deductions);
9677
+ return {
9678
+ issues: [...this.issues],
9679
+ score,
9680
+ scannedAt: new Date,
9681
+ filesScanned: new Set(this.issues.map((i) => i.file).filter(Boolean)).size || 0
9682
+ };
9683
+ }
9684
+ getBySeverity(severity) {
9685
+ return this.issues.filter((i) => i.severity === severity);
9686
+ }
9687
+ getByCategory(category) {
9688
+ return this.issues.filter((i) => i.category === category);
9689
+ }
9690
+ isExcluded(filename) {
9691
+ return this.config.excludeFiles.some((exclusion) => {
9692
+ if (exclusion.startsWith("*")) {
9693
+ return filename.endsWith(exclusion.slice(1));
9694
+ }
9695
+ return filename.includes(exclusion);
9696
+ });
9697
+ }
9698
+ getLineNumber(content, index) {
9699
+ return content.substring(0, index).split(`
9700
+ `).length;
9701
+ }
9702
+ clear() {
9703
+ this.issues = [];
9704
+ }
9705
+ getStats() {
9706
+ return {
9707
+ total: this.issues.length,
9708
+ critical: this.getBySeverity("critical").length,
9709
+ high: this.getBySeverity("high").length,
9710
+ medium: this.getBySeverity("medium").length,
9711
+ low: this.getBySeverity("low").length
9712
+ };
9713
+ }
9714
+ }
9715
+
9716
+ // src/orchestrator.ts
9717
+ var DEFAULT_ESCALATION = {
9718
+ maxRetries: 3,
9719
+ retryDelay: 1000,
9720
+ enableRespawn: true,
9721
+ fallbackModels: ["google/gemini-2.5-flash", "anthropic/claude-haiku-4-5"],
9722
+ alertOnFailure: true
9723
+ };
9724
+
9725
+ class NexusOrchestrator {
9726
+ agents = new Map;
9727
+ tasks = new Map;
9728
+ dag = null;
9729
+ config;
9730
+ budget;
9731
+ running = false;
9732
+ paused = false;
9733
+ budgetExceeded = false;
9734
+ totalSpent = 0;
9735
+ costByAgent = new Map;
9736
+ costByModel = new Map;
9737
+ messageQueue = [];
9738
+ subscribers = new Map;
9739
+ messageStore;
9740
+ memoryStore;
9741
+ messageRouter;
9742
+ eventHandlers = new Map;
9743
+ configManager;
9744
+ ctx = null;
9745
+ broadcaster = null;
9746
+ dashboard = null;
9747
+ _healthMonitor = null;
9748
+ learning;
9749
+ escalationPolicy;
9750
+ nodeRetryCounts = new Map;
9751
+ moduleRegistry;
9752
+ securityScanner;
9753
+ onStateChange = null;
9754
+ stateChangeTimer = null;
9755
+ costHistory = [];
9756
+ cleanupInterval = null;
9757
+ get healthMonitor() {
9758
+ return this._healthMonitor;
9759
+ }
9760
+ notifications = null;
9761
+ constructor(config, messageStoreConfig, memoryStoreConfig) {
9762
+ this.config = this.mergeConfig(config);
9763
+ this.budget = this.config.budget;
9764
+ this.configManager = new NexusConfigManager;
9765
+ this.moduleRegistry = new ModuleRegistry;
9766
+ this.messageStore = new MessageStore(messageStoreConfig);
9767
+ this.memoryStore = new PersistentMemoryStore(memoryStoreConfig);
9768
+ this.messageRouter = new MessageRouter;
9769
+ this.escalationPolicy = {
9770
+ ...DEFAULT_ESCALATION,
9771
+ maxRetries: this.config.selfHealing.maxRetries,
9772
+ retryDelay: this.config.selfHealing.retryDelay,
9773
+ enableRespawn: this.config.selfHealing.contextTransfer
9774
+ };
9775
+ this.learning = new LearningModule(this.config.learning.minConfidence);
9776
+ this.securityScanner = new SecurityScanner;
9777
+ }
9778
+ async initialize(ctx, onStateChange) {
9779
+ this.ctx = ctx;
9780
+ this.onStateChange = onStateChange ?? null;
9781
+ this.configManager.loadFromPath(process.cwd());
9782
+ this.cleanupInterval = setInterval(() => this.cleanupStaleData(), 300000);
9783
+ this._healthMonitor = new HealthMonitor({
9784
+ checkInterval: this.config.agents.healthCheckInterval
9785
+ });
9786
+ this.notifications = new NotificationManager(true);
9787
+ const moduleCtx = {
9788
+ orchestrator: this,
9789
+ config: this.config,
9790
+ emit: (event, data) => this.emit(event, data),
9791
+ on: (event, handler) => {
9792
+ this.on(event, handler);
9793
+ }
9794
+ };
9795
+ await this.moduleRegistry.setupAll(moduleCtx);
9796
+ }
9797
+ initBroadcaster(opts) {
9798
+ this.broadcaster = new StateBroadcaster(this, opts);
9799
+ const previousOnStateChange = this.onStateChange;
9800
+ this.onStateChange = () => {
9801
+ previousOnStateChange?.();
9802
+ this.broadcaster?.broadcastState();
9803
+ };
9804
+ }
9805
+ startDashboard(port, host) {
9806
+ const dashPort = port || this.config.dashboard.port;
9807
+ const dashHost = host || this.config.dashboard.host;
9808
+ this.dashboard = new DashboardModule(this);
9809
+ this.dashboard.start(dashPort, dashHost);
9810
+ this.on("agent:spawned", (agent) => this.dashboard.broadcast("agent:spawned", agent));
9811
+ this.on("agent:terminated", (agent) => this.dashboard.broadcast("agent:terminated", agent));
9812
+ this.on("budget:alert", (data) => this.dashboard.broadcast("budget:alert", data));
9813
+ this.on("budget:exceeded", (data) => this.dashboard.broadcast("budget:exceeded", data));
9814
+ }
9815
+ stopDashboard() {
9816
+ if (this.dashboard) {
9817
+ this.dashboard.stop();
9818
+ this.dashboard = null;
9819
+ }
9820
+ }
9821
+ getState() {
9822
+ const agents = Array.from(this.agents.values()).map((a) => ({
9823
+ id: a.id,
9824
+ name: a.name,
9825
+ role: a.role,
9826
+ status: a.status,
9827
+ model: `${a.model.provider}/${a.model.model}`,
9828
+ sessionID: a.sessionID,
9829
+ spawnedAt: a.spawnedAt.toISOString(),
9830
+ tasksCompleted: a.metrics.tasksCompleted,
9831
+ tasksFailed: a.metrics.tasksFailed,
9832
+ totalCost: a.metrics.totalCost
9833
+ }));
9834
+ const tasks = Array.from(this.tasks.values()).map((t) => ({
9835
+ id: t.id,
9836
+ name: t.name,
9837
+ role: t.requiredRole,
9838
+ priority: t.priority || "normal",
9839
+ status: t.status,
9840
+ assignedAgent: t.assignedAgent,
9841
+ result: t.result ? {
9842
+ success: t.result.success,
9843
+ output: t.result.output?.slice(0, 500),
9844
+ error: t.result.error,
9845
+ duration: t.result.duration
9846
+ } : undefined
9847
+ }));
9848
+ return {
9849
+ running: this.running,
9850
+ paused: this.paused,
9851
+ agents,
9852
+ tasks,
9853
+ totalSpent: this.totalSpent,
9854
+ budgetRemaining: this.budget.maxTotalCost - this.totalSpent,
9855
+ lastUpdated: new Date().toISOString()
9856
+ };
9857
+ }
9858
+ mergeConfig(partial) {
9859
+ const defaults = {
9860
+ maxConcurrency: 5,
9861
+ schedulerInterval: 1000,
9862
+ defaultTimeout: 300000,
9863
+ budget: {
9864
+ maxTotalCost: 10,
9865
+ maxCostPerTask: 1,
9866
+ maxCostPerAgent: 2,
9867
+ alertThreshold: 0.2,
9868
+ hardLimit: false
9869
+ },
9870
+ agents: {
9871
+ defaultRole: "coder",
9872
+ spawnDelay: 100,
9873
+ healthCheckInterval: 30000
9874
+ },
9875
+ selfHealing: {
9876
+ enabled: true,
9877
+ maxRetries: 3,
9878
+ retryDelay: 1000,
9879
+ backoffMultiplier: 2,
9880
+ contextTransfer: true
9881
+ },
9882
+ communication: {
9883
+ mode: "pubsub",
9884
+ maxQueueSize: 100,
9885
+ messageTTL: 60000,
9886
+ persistence: false
9887
+ },
9888
+ memory: {
9889
+ enabled: true,
9890
+ storage: "memory",
9891
+ maxEntriesPerScope: 1000,
9892
+ syncInterval: 5000
9893
+ },
9894
+ dashboard: {
9895
+ enabled: true,
9896
+ port: 4747,
9897
+ host: "127.0.0.1"
9898
+ },
9899
+ security: {
9900
+ sastEnabled: true,
9901
+ secretsScanning: true,
9902
+ scopeEnforcement: true
9903
+ },
9904
+ learning: {
9905
+ enabled: true,
9906
+ patternStorage: "memory",
9907
+ minConfidence: 0.7
9908
+ }
9909
+ };
9910
+ return {
9911
+ ...defaults,
9912
+ ...partial,
9913
+ budget: { ...defaults.budget, ...partial?.budget },
9914
+ agents: { ...defaults.agents, ...partial?.agents },
9915
+ selfHealing: { ...defaults.selfHealing, ...partial?.selfHealing },
9916
+ communication: { ...defaults.communication, ...partial?.communication },
9917
+ memory: { ...defaults.memory, ...partial?.memory },
9918
+ dashboard: { ...defaults.dashboard, ...partial?.dashboard },
9919
+ security: { ...defaults.security, ...partial?.security },
9920
+ learning: { ...defaults.learning, ...partial?.learning }
9921
+ };
9922
+ }
9923
+ notifyStateChange() {
9924
+ if (this.stateChangeTimer)
9925
+ return;
9926
+ this.stateChangeTimer = setTimeout(() => {
9927
+ this.stateChangeTimer = null;
9928
+ this.onStateChange?.();
9929
+ }, 100);
9930
+ }
9931
+ async execute(request) {
9932
+ if (this.running) {
9933
+ throw new Error("Orchestrator is already running");
9934
+ }
9935
+ if (!this.ctx) {
9936
+ throw new Error("Orchestrator not initialized - call initialize(ctx) first");
9937
+ }
9938
+ this.running = true;
9939
+ const startTime = Date.now();
9940
+ this.notifyStateChange();
9941
+ try {
9942
+ this.dag = this.buildDAG(request.tasks);
9943
+ const dagNodes = Array.from(this.dag.nodes.values());
9944
+ const cycles = detectCycles(dagNodes);
9945
+ if (cycles.length > 0) {
9946
+ const cycleDescriptions = cycles.map((c) => c.join(" → ")).join(", ");
9947
+ throw new Error(`Circular dependency detected: ${cycleDescriptions}`);
9948
+ }
9949
+ if (request.budget) {
9950
+ this.budget = request.budget;
9951
+ }
9952
+ await this.executeDAG();
9953
+ const results = this.collectResults();
9954
+ const totalDuration = Date.now() - startTime;
9955
+ return {
9956
+ success: true,
9957
+ tasks: results,
9958
+ totalCost: this.totalSpent,
9959
+ totalDuration,
9960
+ agentsUsed: this.agents.size
9961
+ };
9962
+ } catch (error) {
9963
+ return {
9964
+ success: false,
9965
+ tasks: [],
9966
+ totalCost: this.totalSpent,
9967
+ totalDuration: Date.now() - startTime,
9968
+ agentsUsed: this.agents.size
9969
+ };
9970
+ } finally {
9971
+ this.running = false;
9972
+ this.notifyStateChange();
9973
+ }
9974
+ }
9975
+ buildDAG(tasks) {
9976
+ const dag = {
9977
+ nodes: new Map,
9978
+ addNode: (node) => {
9979
+ dag.nodes.set(node.id, node);
9980
+ },
9981
+ addDependency: (nodeId, dependsOn) => {
9982
+ const node = dag.nodes.get(nodeId);
9983
+ if (node) {
9984
+ node.dependencies.push(dependsOn);
9985
+ }
9986
+ },
9987
+ removeNode: (nodeId) => {
9988
+ dag.nodes.delete(nodeId);
9989
+ },
9990
+ getReadyNodes: () => {
9991
+ const ready = [];
9992
+ dag.nodes.forEach((node) => {
9993
+ if (node.status === "pending") {
9994
+ const depsComplete = node.dependencies.every((dep) => {
9995
+ const depNode = dag.nodes.get(dep);
9996
+ return depNode?.status === "completed";
9997
+ });
9998
+ if (depsComplete) {
9999
+ ready.push(node);
10000
+ }
10001
+ }
10002
+ });
10003
+ const priorityOrder = { critical: 0, high: 1, normal: 2, low: 3 };
10004
+ return ready.sort((a, b) => {
10005
+ const pa = priorityOrder[a.task.priority] ?? 2;
10006
+ const pb = priorityOrder[b.task.priority] ?? 2;
10007
+ return pa - pb;
10008
+ });
10009
+ },
10010
+ markComplete: (nodeId, result) => {
10011
+ const node = dag.nodes.get(nodeId);
10012
+ if (node) {
10013
+ node.status = "completed";
10014
+ node.result = result;
10015
+ }
10016
+ },
10017
+ markFailed: (nodeId, error) => {
10018
+ const node = dag.nodes.get(nodeId);
10019
+ if (node) {
10020
+ node.status = "failed";
10021
+ }
10022
+ },
10023
+ getParallelGroups: () => {
10024
+ const groups = [];
10025
+ const ready = dag.getReadyNodes();
10026
+ if (ready.length > 0) {
10027
+ groups.push(ready);
10028
+ }
10029
+ return groups;
10030
+ },
10031
+ isComplete: () => {
10032
+ let complete = true;
10033
+ dag.nodes.forEach((node) => {
10034
+ if (node.status !== "completed" && node.status !== "failed") {
10035
+ complete = false;
10036
+ }
10037
+ });
10038
+ return complete;
10039
+ }
10040
+ };
10041
+ for (const task of tasks) {
10042
+ const node = {
10043
+ id: task.id,
10044
+ task,
10045
+ dependencies: task.dependencies,
10046
+ status: "pending"
10047
+ };
10048
+ dag.addNode(node);
10049
+ }
10050
+ return dag;
10051
+ }
10052
+ async executeDAG() {
10053
+ while (!this.dag.isComplete() && !this.paused) {
10054
+ const readyNodes = this.dag.getReadyNodes();
10055
+ const spawnPromises = [];
10056
+ for (const node of readyNodes) {
10057
+ if (this.agents.size < this.config.maxConcurrency) {
10058
+ spawnPromises.push(this.spawnAndExecute(node));
10059
+ }
10060
+ }
10061
+ await Promise.all(spawnPromises);
10062
+ await this.sleep(this.config.schedulerInterval);
10063
+ }
10064
+ }
10065
+ async spawnAndExecute(node, transferContext) {
10066
+ const complexity = this.analyzeComplexity(node.task);
8705
10067
  const model = this.selectModel(node.task.requiredRole, complexity);
8706
10068
  const agent = await this.spawnAgent({
8707
10069
  role: node.task.requiredRole,
@@ -8710,86 +10072,232 @@ class NexusOrchestrator {
8710
10072
  });
8711
10073
  node.spawnedAgent = agent;
8712
10074
  node.status = "running";
8713
- this.monitorAgent(agent, node);
10075
+ node.task.assignedAgent = agent.id;
10076
+ this.notifyStateChange();
10077
+ await this.executeTask(agent, node, transferContext);
8714
10078
  }
8715
- async monitorAgent(agent, node) {
8716
- const timeout = node.task.timeout || this.config.defaultTimeout;
8717
- setTimeout(() => {
8718
- if (node.status === "running") {
8719
- this.handleAgentTimeout(agent, node);
8720
- }
8721
- }, timeout);
8722
- }
8723
- handleAgentTimeout(agent, node) {
8724
- if (this.config.selfHealing.enabled) {
8725
- this.handleFailure(agent, node, new Error("Agent timed out"));
8726
- } else {
10079
+ async executeTask(agent, node, transferContext) {
10080
+ if (!this.ctx || !agent.sessionID) {
8727
10081
  node.status = "failed";
8728
10082
  node.result = {
8729
10083
  success: false,
8730
- error: "Agent timed out",
8731
- duration: node.task.timeout || this.config.defaultTimeout,
10084
+ error: "No session available for agent",
10085
+ duration: 0,
10086
+ tokensUsed: 0,
10087
+ cost: 0
10088
+ };
10089
+ this.notifyStateChange();
10090
+ return;
10091
+ }
10092
+ const startTime = Date.now();
10093
+ const timeout = node.task.timeout || this.config.defaultTimeout;
10094
+ try {
10095
+ const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
10096
+ let taskPrompt = `${rolePrompt}
10097
+
10098
+ ## Task
10099
+ ${node.task.name}
10100
+
10101
+ ${node.task.description}
10102
+
10103
+ ## Scope
10104
+ Files: ${node.task.files.include.join(", ")}`;
10105
+ if (transferContext) {
10106
+ taskPrompt += `
10107
+
10108
+ ## Previous Agent Context (from failed agent ${transferContext.previousAgentId})`;
10109
+ taskPrompt += `
10110
+ Partial results: ${transferContext.partialResults.join(", ") || "None"}`;
10111
+ taskPrompt += `
10112
+ Decisions made: ${transferContext.decisions.join(", ") || "None"}`;
10113
+ taskPrompt += `
10114
+ Progress: ${transferContext.taskProgress}%`;
10115
+ taskPrompt += `
10116
+ Errors encountered: ${transferContext.errorLog.join(", ") || "None"}`;
10117
+ taskPrompt += `
10118
+
10119
+ Please continue from where the previous agent left off.`;
10120
+ }
10121
+ await this.ctx.session.prompt({
10122
+ sessionID: agent.sessionID,
10123
+ text: taskPrompt
10124
+ });
10125
+ const waitPromise = this.ctx.session.wait({ sessionID: agent.sessionID });
10126
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Task timed out")), timeout));
10127
+ await Promise.race([waitPromise, timeoutPromise]);
10128
+ const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
10129
+ const lastAssistantMsg = messages.filter((m) => m.role === "assistant").pop();
10130
+ const output = lastAssistantMsg?.content || "Task completed";
10131
+ const duration = Date.now() - startTime;
10132
+ const result = {
10133
+ success: true,
10134
+ output: typeof output === "string" ? output : JSON.stringify(output),
10135
+ duration,
10136
+ tokensUsed: 0,
10137
+ cost: this.estimateModelCost(agent.model.model)
10138
+ };
10139
+ this.dag.markComplete(node.id, result);
10140
+ agent.metrics.tasksCompleted++;
10141
+ agent.metrics.totalCost += result.cost;
10142
+ this.totalSpent += result.cost;
10143
+ this.costByAgent.set(agent.id, (this.costByAgent.get(agent.id) || 0) + result.cost);
10144
+ this.checkBudget();
10145
+ if (this.notifications?.isEnabled()) {
10146
+ this.notifications.notify({ title: "Nexus: Task Complete", body: `${node.task.name} completed successfully` });
10147
+ }
10148
+ const priorPattern = this.learning.findSolutions(`task ${node.id} failed`);
10149
+ if (priorPattern.length > 0) {
10150
+ this.learning.recordSuccess(priorPattern[0].entry.id);
10151
+ }
10152
+ if (typeof output === "string" && output.length > 0) {
10153
+ const securityIssues = this.securityScanner.scanContent(output, node.task.name);
10154
+ if (securityIssues.length > 0) {
10155
+ this.emit("security:issues-found", {
10156
+ taskId: node.id,
10157
+ taskName: node.task.name,
10158
+ issues: securityIssues,
10159
+ totalIssues: securityIssues.length
10160
+ });
10161
+ }
10162
+ }
10163
+ } catch (error) {
10164
+ const duration = Date.now() - startTime;
10165
+ const result = {
10166
+ success: false,
10167
+ error: error.message || "Task failed",
10168
+ duration,
8732
10169
  tokensUsed: 0,
8733
10170
  cost: 0
8734
10171
  };
10172
+ this.dag.markFailed(node.id, new Error(result.error));
10173
+ agent.metrics.tasksFailed++;
10174
+ if (this.config.selfHealing.enabled) {
10175
+ await this.handleFailure(agent, node, new Error(result.error));
10176
+ }
10177
+ } finally {
10178
+ agent.status = "idle";
10179
+ node.task.status = node.status === "completed" ? "completed" : "failed";
10180
+ this.notifyStateChange();
10181
+ }
10182
+ }
10183
+ buildRolePrompt(role) {
10184
+ const rolePrompts = {
10185
+ architect: "You are a software architect. Focus on system design, architecture patterns, and high-level technical decisions. Analyze requirements and propose structured solutions.",
10186
+ coder: "You are a senior software engineer. Write clean, efficient, well-documented code. Follow best practices and coding standards.",
10187
+ reviewer: "You are a code reviewer. Review code for correctness, security, performance, and maintainability. Provide constructive feedback.",
10188
+ tester: "You are a QA engineer. Write comprehensive tests, identify edge cases, and ensure code quality.",
10189
+ explorer: "You are a code explorer. Navigate and analyze codebases, understand architecture, and provide detailed reports.",
10190
+ documenter: "You are a technical writer. Create clear, comprehensive documentation for code and APIs."
10191
+ };
10192
+ return rolePrompts[role] || `You are a ${role}. Complete the assigned task professionally.`;
10193
+ }
10194
+ collectContext(agent) {
10195
+ const partialResults = [];
10196
+ const decisions = [];
10197
+ const errorLog = [];
10198
+ if (agent.sessionID && this.ctx) {
10199
+ if (agent.metrics.tasksCompleted > 0) {
10200
+ partialResults.push(`${agent.metrics.tasksCompleted} task(s) completed before failure`);
10201
+ }
10202
+ if (agent.metrics.tasksFailed > 0) {
10203
+ errorLog.push(`${agent.metrics.tasksFailed} task(s) failed`);
10204
+ }
8735
10205
  }
10206
+ const memoryEntries = [];
10207
+ const byAgentId = this.memoryStore.getByAuthor(agent.id);
10208
+ const byRole = this.memoryStore.getByAuthor(agent.role);
10209
+ memoryEntries.push(...byAgentId, ...byRole);
10210
+ const taskProgress = agent.metrics.tasksCompleted > 0 ? Math.min(50, agent.metrics.tasksCompleted * 25) : 0;
10211
+ return {
10212
+ previousAgentId: agent.id,
10213
+ partialResults,
10214
+ decisions,
10215
+ memoryEntries,
10216
+ taskProgress,
10217
+ errorLog
10218
+ };
8736
10219
  }
8737
10220
  async handleFailure(agent, node, error) {
8738
- const failureCount = agent.metrics.tasksFailed + 1;
8739
- agent.metrics.tasksFailed++;
8740
- if (failureCount < this.config.selfHealing.maxRetries) {
8741
- const delay = this.config.selfHealing.retryDelay * Math.pow(this.config.selfHealing.backoffMultiplier, failureCount - 1);
8742
- setTimeout(async () => {
8743
- await this.retryAgent(agent, node);
8744
- }, delay);
8745
- } else if (this.config.selfHealing.contextTransfer) {
8746
- await this.respawnAgent(agent, node);
8747
- } else {
8748
- node.status = "failed";
8749
- node.result = {
8750
- success: false,
8751
- error: error.message,
8752
- duration: 0,
8753
- tokensUsed: 0,
8754
- cost: 0
8755
- };
10221
+ const policy = this.escalationPolicy;
10222
+ const retryCount = this.nodeRetryCounts.get(node.id) || 0;
10223
+ const pattern = error.message || "Unknown error";
10224
+ const solution = `Retry (attempt ${retryCount + 1}/${policy.maxRetries})`;
10225
+ const context = `during ${node.task.requiredRole} task "${node.task.name}"`;
10226
+ this.learning.recordFailure(pattern, solution, context, [node.task.requiredRole]);
10227
+ if (retryCount < policy.maxRetries) {
10228
+ this.nodeRetryCounts.set(node.id, retryCount + 1);
10229
+ const delay = policy.retryDelay * Math.pow(2, retryCount);
10230
+ await this.sleep(delay);
10231
+ node.status = "pending";
10232
+ this.notifyStateChange();
10233
+ await this.spawnAndExecute(node);
10234
+ return;
8756
10235
  }
8757
- }
8758
- async retryAgent(agent, node) {
8759
- agent.status = "working";
8760
- node.status = "running";
8761
- this.emit("agent:retry", { agent, node });
8762
- }
8763
- async respawnAgent(oldAgent, node) {
8764
- const newAgent = await this.spawnAgent({
8765
- role: oldAgent.role,
8766
- task: node.task,
8767
- model: oldAgent.model.model
8768
- });
8769
- if (this.config.selfHealing.contextTransfer) {
8770
- await this.transferContext(oldAgent, newAgent);
10236
+ if (policy.enableRespawn && this.config.selfHealing.contextTransfer) {
10237
+ const context = this.collectContext(agent);
10238
+ this.setMemory("session", `context:${agent.id}`, context, agent.id);
10239
+ await this.terminateAgent(agent.id);
10240
+ node.status = "pending";
10241
+ this.notifyStateChange();
10242
+ await this.spawnAndExecute(node, context);
10243
+ return;
8771
10244
  }
8772
- await this.terminateAgent(oldAgent.id);
8773
- node.spawnedAgent = newAgent;
8774
- node.status = "running";
8775
- this.monitorAgent(newAgent, node);
8776
- }
8777
- async transferContext(oldAgent, newAgent) {
8778
- this.emit("context:transfer", { from: oldAgent, to: newAgent });
10245
+ if (policy.fallbackModels.length > 0) {
10246
+ const fallbackModel = policy.fallbackModels.shift();
10247
+ if (fallbackModel) {
10248
+ await this.terminateAgent(agent.id);
10249
+ node.status = "pending";
10250
+ this.notifyStateChange();
10251
+ await this.spawnAgent({ role: node.task.requiredRole, model: fallbackModel });
10252
+ return;
10253
+ }
10254
+ }
10255
+ if (policy.alertOnFailure) {
10256
+ this.emit("agent:escalation", { agentId: agent.id, taskId: node.id, error: error.message });
10257
+ if (this.notifications?.isEnabled()) {
10258
+ this.notifications.notify({ title: "Nexus: Task Failed", body: `${node.task.name} failed: ${error.message}`, sound: true });
10259
+ }
10260
+ }
10261
+ node.status = "failed";
10262
+ node.result = {
10263
+ success: false,
10264
+ error: error.message,
10265
+ duration: 0,
10266
+ tokensUsed: 0,
10267
+ cost: 0
10268
+ };
10269
+ this.notifyStateChange();
8779
10270
  }
8780
10271
  async spawnAgent(config) {
10272
+ if (!this.ctx) {
10273
+ throw new Error("Orchestrator not initialized");
10274
+ }
10275
+ if (this.budgetExceeded) {
10276
+ throw new Error("Budget exceeded — cannot spawn new agents");
10277
+ }
8781
10278
  const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
10279
+ const modelConfig = config.model || this.configManager.getModelForRole(config.role);
10280
+ const slashIndex = modelConfig.indexOf("/");
10281
+ const provider = slashIndex > -1 ? modelConfig.slice(0, slashIndex) : modelConfig;
10282
+ const modelName = slashIndex > -1 ? modelConfig.slice(slashIndex + 1) : modelConfig;
10283
+ const roleEmoji = this.configManager.getRoleEmoji(config.role);
10284
+ const title = `${roleEmoji} ${this.configManager.getRoleDisplayName(config.role)} — ${modelConfig}`;
10285
+ const session = await this.ctx.session.create({
10286
+ title,
10287
+ agent: "build",
10288
+ model: modelName ? { providerID: provider, id: modelName } : undefined
10289
+ });
8782
10290
  const agent = {
8783
10291
  id: agentId,
8784
- name: `${config.role}-${agentId.slice(0, 8)}`,
10292
+ name: title,
8785
10293
  role: config.role,
8786
- status: "spawning",
10294
+ status: "idle",
8787
10295
  model: {
8788
- provider: "opencode",
8789
- model: config.model || "default",
10296
+ provider,
10297
+ model: modelName || "default",
8790
10298
  estimatedCost: 0,
8791
10299
  estimatedQuality: 0.5,
8792
- reasoning: "Default selection"
10300
+ reasoning: `Configured for ${config.role}`
8793
10301
  },
8794
10302
  spawnedAt: new Date,
8795
10303
  lastActivity: new Date,
@@ -8800,12 +10308,17 @@ class NexusOrchestrator {
8800
10308
  totalCost: 0,
8801
10309
  averageResponseTime: 0,
8802
10310
  errorRate: 0
8803
- }
10311
+ },
10312
+ sessionID: session.id
8804
10313
  };
8805
10314
  this.agents.set(agentId, agent);
8806
10315
  this.emit("agent:spawned", agent);
8807
- await this.sleep(this.config.agents.spawnDelay);
8808
- agent.status = "idle";
10316
+ this.notifyStateChange();
10317
+ this.enforceAgentLimit();
10318
+ const monitor = this.getOrCreateHealthMonitor();
10319
+ if (!monitor.isActive()) {
10320
+ monitor.start(() => Array.from(this.agents.values()));
10321
+ }
8809
10322
  return agent;
8810
10323
  }
8811
10324
  async terminateAgent(agentId) {
@@ -8814,76 +10327,41 @@ class NexusOrchestrator {
8814
10327
  agent.status = "terminated";
8815
10328
  this.agents.delete(agentId);
8816
10329
  this.emit("agent:terminated", agent);
10330
+ this.notifyStateChange();
10331
+ if (this._healthMonitor && this.agents.size === 0) {
10332
+ this._healthMonitor.stop();
10333
+ }
8817
10334
  }
8818
10335
  }
8819
10336
  analyzeComplexity(task) {
8820
10337
  const fileCount = task.files.include.length;
8821
- const codeLines = this.estimateCodeLines(task);
10338
+ const codeLines = task.files.include.length * 50;
8822
10339
  const dependencyDepth = task.dependencies.length;
8823
- const domainKnowledge = this.estimateDomainKnowledge(task);
8824
- const riskLevel = this.estimateRiskLevel(task);
8825
- const overall = Math.min(100, fileCount * 10 + codeLines / 10 + dependencyDepth * 15 + domainKnowledge * 20 + (riskLevel === "high" ? 30 : riskLevel === "medium" ? 15 : 0));
8826
- return {
8827
- overall,
8828
- factors: {
8829
- fileCount,
8830
- codeLines,
8831
- dependencyDepth,
8832
- domainKnowledge,
8833
- riskLevel
8834
- }
8835
- };
8836
- }
8837
- estimateCodeLines(task) {
8838
- return task.files.include.length * 50;
8839
- }
8840
- estimateDomainKnowledge(task) {
10340
+ let domainKnowledge = 0;
8841
10341
  const keywords = ["security", "auth", "payment", "crypto", "database"];
8842
- let score = 0;
8843
10342
  for (const keyword of keywords) {
8844
10343
  if (task.description.toLowerCase().includes(keyword)) {
8845
- score += 20;
10344
+ domainKnowledge += 20;
8846
10345
  }
8847
10346
  }
8848
- return Math.min(100, score);
8849
- }
8850
- estimateRiskLevel(task) {
10347
+ domainKnowledge = Math.min(100, domainKnowledge);
10348
+ let riskLevel = "low";
8851
10349
  const highRiskKeywords = ["migration", "production", "security", "payment"];
8852
- const mediumRiskKeywords = ["refactor", "update", "modify"];
8853
10350
  for (const keyword of highRiskKeywords) {
8854
10351
  if (task.description.toLowerCase().includes(keyword)) {
8855
- return "high";
8856
- }
8857
- }
8858
- for (const keyword of mediumRiskKeywords) {
8859
- if (task.description.toLowerCase().includes(keyword)) {
8860
- return "medium";
10352
+ riskLevel = "high";
10353
+ break;
8861
10354
  }
8862
10355
  }
8863
- return "low";
8864
- }
8865
- selectModel(role, complexity) {
8866
- const configModel = this.configManager.getModelForRole(role);
8867
- const [provider, ...modelParts] = configModel.split("/");
8868
- const model = modelParts.join("/");
8869
- const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
8870
- if (budgetRemaining < 1 && configModel !== "opencode/minimax-m2.5-free") {
8871
- return {
8872
- provider: "opencode",
8873
- model: "minimax-m2.5-free",
8874
- estimatedCost: 0,
8875
- estimatedQuality: 0.5,
8876
- reasoning: "Budget constrained, using free tier"
8877
- };
8878
- }
10356
+ const overall = Math.min(100, fileCount * 10 + codeLines / 10 + dependencyDepth * 15 + domainKnowledge * 20 + (riskLevel === "high" ? 30 : riskLevel === "medium" ? 15 : 0));
8879
10357
  return {
8880
- provider,
8881
- model,
8882
- estimatedCost: this.estimateModelCost(model),
8883
- estimatedQuality: this.estimateModelQuality(model),
8884
- reasoning: `Configured model for ${role}`
10358
+ overall,
10359
+ factors: { fileCount, codeLines, dependencyDepth, domainKnowledge, riskLevel }
8885
10360
  };
8886
10361
  }
10362
+ selectModel(role, complexity) {
10363
+ return this.selectBestModel(role, complexity);
10364
+ }
8887
10365
  estimateModelCost(model) {
8888
10366
  const costs = {
8889
10367
  "claude-sonnet-4-6": 0.15,
@@ -8908,21 +10386,86 @@ class NexusOrchestrator {
8908
10386
  };
8909
10387
  return quality[model] || 0.6;
8910
10388
  }
10389
+ estimateModelSpeed(model) {
10390
+ const speeds = {
10391
+ "claude-opus-4-7": 0.4,
10392
+ "claude-sonnet-4-6": 0.7,
10393
+ "gpt-5": 0.6,
10394
+ "claude-haiku-4-5": 0.9,
10395
+ "gemini-2.5-flash": 0.95,
10396
+ "gpt-5-mini": 0.85,
10397
+ "minimax-m2.5-free": 0.8
10398
+ };
10399
+ return speeds[model] || 0.5;
10400
+ }
10401
+ scoreModel(modelId, role, complexity) {
10402
+ const [provider, ...parts] = modelId.split("/");
10403
+ const model = parts.join("/");
10404
+ const cost = this.estimateModelCost(model);
10405
+ const quality = this.estimateModelQuality(model);
10406
+ const maxCost = 15;
10407
+ const costScore = 1 - cost / maxCost;
10408
+ const speedScore = this.estimateModelSpeed(model);
10409
+ const qualityWeight = complexity.overall > 70 ? 0.6 : complexity.overall > 40 ? 0.4 : 0.2;
10410
+ const costWeight = 1 - qualityWeight;
10411
+ const overallScore = quality * qualityWeight + costScore * costWeight;
10412
+ return {
10413
+ model,
10414
+ provider,
10415
+ costScore,
10416
+ qualityScore: quality,
10417
+ speedScore,
10418
+ overallScore,
10419
+ reasoning: `Score: ${overallScore.toFixed(2)} (quality: ${quality.toFixed(2)}, cost: ${costScore.toFixed(2)}, speed: ${speedScore.toFixed(2)})`
10420
+ };
10421
+ }
10422
+ selectBestModel(role, complexity) {
10423
+ const configModel = this.configManager.getModelForRole(role);
10424
+ const candidates = [
10425
+ configModel,
10426
+ "anthropic/claude-sonnet-4-6",
10427
+ "anthropic/claude-haiku-4-5",
10428
+ "openai/gpt-5-mini",
10429
+ "google/gemini-2.5-flash",
10430
+ "opencode/minimax-m2.5-free"
10431
+ ];
10432
+ const unique = [...new Set(candidates)];
10433
+ const scored = unique.map((m) => this.scoreModel(m, role, complexity));
10434
+ const budgetRemaining = this.budget.maxTotalCost - this.totalSpent;
10435
+ const affordable = scored.filter((s) => {
10436
+ const cost = this.estimateModelCost(s.model);
10437
+ return cost <= budgetRemaining || cost === 0;
10438
+ });
10439
+ const best = (affordable.length > 0 ? affordable : scored).sort((a, b) => b.overallScore - a.overallScore)[0];
10440
+ return {
10441
+ provider: best.provider,
10442
+ model: best.model,
10443
+ estimatedCost: this.estimateModelCost(best.model),
10444
+ estimatedQuality: best.qualityScore,
10445
+ reasoning: best.reasoning
10446
+ };
10447
+ }
8911
10448
  trackCost(agentId, model, cost, tokens) {
8912
10449
  this.totalSpent += cost;
8913
10450
  const agentCost = this.costByAgent.get(agentId) || 0;
8914
10451
  this.costByAgent.set(agentId, agentCost + cost);
8915
10452
  const modelCost = this.costByModel.get(model) || 0;
8916
10453
  this.costByModel.set(model, modelCost + cost);
10454
+ this.costHistory.push({ timestamp: Date.now(), cost, agentId });
8917
10455
  this.checkBudget();
10456
+ this.notifyStateChange();
8918
10457
  }
8919
10458
  checkBudget() {
8920
10459
  const remaining = this.budget.maxTotalCost - this.totalSpent;
8921
10460
  const remainingPercent = remaining / this.budget.maxTotalCost;
8922
10461
  if (remainingPercent <= this.config.budget.alertThreshold) {
8923
10462
  this.emit("budget:alert", { remaining, remainingPercent });
10463
+ if (this.notifications?.isEnabled()) {
10464
+ this.notifications.notify({ title: "Nexus: Budget Alert", body: `Budget low: $${remaining.toFixed(2)} remaining (${(remainingPercent * 100).toFixed(1)}%)`, sound: true });
10465
+ }
8924
10466
  }
8925
- if (this.budget.hardLimit && remaining <= 0) {
10467
+ if (this.budget.hardLimit && remaining <= 0 && !this.budgetExceeded) {
10468
+ this.budgetExceeded = true;
8926
10469
  this.emit("budget:exceeded", { totalSpent: this.totalSpent });
8927
10470
  this.pause();
8928
10471
  }
@@ -8930,12 +10473,15 @@ class NexusOrchestrator {
8930
10473
  publish(topic, message) {
8931
10474
  const fullMessage = {
8932
10475
  ...message,
10476
+ topic: message.topic ?? topic,
8933
10477
  id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
8934
10478
  timestamp: new Date
8935
10479
  };
8936
10480
  this.messageQueue.push(fullMessage);
10481
+ this.messageStore.add(fullMessage);
8937
10482
  const handlers = this.subscribers.get(topic) || [];
8938
10483
  handlers.forEach((handler) => handler(fullMessage));
10484
+ this.messageRouter.route(fullMessage);
8939
10485
  }
8940
10486
  subscribe(topic, handler) {
8941
10487
  const handlers = this.subscribers.get(topic) || [];
@@ -8943,130 +10489,128 @@ class NexusOrchestrator {
8943
10489
  this.subscribers.set(topic, handlers);
8944
10490
  return () => {
8945
10491
  const idx = handlers.indexOf(handler);
8946
- if (idx > -1) {
10492
+ if (idx > -1)
8947
10493
  handlers.splice(idx, 1);
8948
- }
8949
- };
8950
- }
8951
- send(agentId, message) {
8952
- const fullMessage = {
8953
- ...message,
8954
- id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
8955
- to: agentId,
8956
- timestamp: new Date
8957
10494
  };
8958
- this.messageQueue.push(fullMessage);
8959
- this.emit("message:sent", fullMessage);
8960
10495
  }
8961
10496
  setMemory(scope, key, value, author) {
8962
- const entry = {
8963
- id: `mem-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
10497
+ const entry = this.memoryStore.set({
8964
10498
  key,
8965
10499
  value,
8966
10500
  scope,
8967
10501
  author,
8968
- timestamp: new Date,
8969
10502
  confidence: 1,
8970
10503
  tags: []
8971
- };
8972
- this.memory.set(`${scope}:${key}`, entry);
10504
+ });
8973
10505
  this.emit("memory:set", entry);
8974
10506
  }
8975
10507
  getMemory(scope, key) {
8976
- return this.memory.get(`${scope}:${key}`);
8977
- }
8978
- searchMemory(query, scope) {
8979
- const results = [];
8980
- this.memory.forEach((entry) => {
8981
- if (scope && entry.scope !== scope)
8982
- return;
8983
- if (entry.key.includes(query) || JSON.stringify(entry.value).includes(query)) {
8984
- results.push(entry);
8985
- }
8986
- });
8987
- return results;
10508
+ return this.memoryStore.get(key, scope) ?? undefined;
8988
10509
  }
8989
10510
  getStatus(detailed) {
8990
- const status = {
8991
- running: this.running,
8992
- paused: this.paused,
8993
- agents: this.agents.size,
8994
- tasks: this.tasks.size,
8995
- totalCost: this.totalSpent,
8996
- budgetRemaining: this.budget.maxTotalCost - this.totalSpent
8997
- };
8998
- if (detailed) {
8999
- return JSON.stringify({
9000
- ...status,
9001
- agentsByStatus: this.getAgentsByStatus(),
9002
- costByAgent: Object.fromEntries(this.costByAgent),
9003
- costByModel: Object.fromEntries(this.costByModel)
9004
- }, null, 2);
9005
- }
9006
- return JSON.stringify(status, null, 2);
10511
+ const state = this.getState();
10512
+ if (detailed)
10513
+ return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded }, null, 2);
10514
+ return JSON.stringify({
10515
+ running: state.running,
10516
+ paused: state.paused,
10517
+ budgetExceeded: this.budgetExceeded,
10518
+ agents: state.agents.length,
10519
+ tasks: state.tasks.length,
10520
+ totalCost: state.totalSpent,
10521
+ budgetRemaining: state.budgetRemaining
10522
+ }, null, 2);
9007
10523
  }
9008
10524
  listAgents(filter) {
9009
- const agents = Array.from(this.agents.values());
9010
- const filtered = filter ? agents.filter((a) => a.status === filter) : agents;
10525
+ const state = this.getState();
10526
+ const filtered = filter ? state.agents.filter((a) => a.status === filter) : state.agents;
9011
10527
  return JSON.stringify(filtered, null, 2);
9012
10528
  }
9013
10529
  getCostReport() {
9014
- const report = {
9015
- totalSpent: this.totalSpent,
9016
- budgetRemaining: this.budget.maxTotalCost - this.totalSpent,
9017
- byAgent: new Map,
9018
- byModel: new Map,
9019
- timeline: []
9020
- };
9021
- this.agents.forEach((agent) => {
9022
- report.byAgent.set(agent.id, {
9023
- agentId: agent.id,
9024
- agentName: agent.name,
9025
- totalCost: agent.metrics.totalCost,
9026
- tokenCount: agent.metrics.totalTokens,
9027
- taskCount: agent.metrics.tasksCompleted
9028
- });
9029
- });
10530
+ const state = this.getState();
9030
10531
  return JSON.stringify({
9031
- ...report,
9032
- byAgent: Object.fromEntries(report.byAgent),
9033
- byModel: Object.fromEntries(report.byModel)
10532
+ totalSpent: state.totalSpent,
10533
+ budgetRemaining: state.budgetRemaining,
10534
+ byAgent: Object.fromEntries(this.costByAgent),
10535
+ byModel: Object.fromEntries(this.costByModel)
9034
10536
  }, null, 2);
9035
10537
  }
9036
10538
  pause() {
9037
10539
  this.paused = true;
9038
10540
  this.emit("orchestrator:paused", {});
10541
+ this.notifyStateChange();
9039
10542
  }
9040
10543
  resume() {
9041
10544
  this.paused = false;
9042
10545
  this.emit("orchestrator:resumed", {});
10546
+ this.notifyStateChange();
9043
10547
  }
9044
- shutdown() {
10548
+ isBudgetExceeded() {
10549
+ return this.budgetExceeded;
10550
+ }
10551
+ resetBudgetExceeded() {
10552
+ this.budgetExceeded = false;
10553
+ }
10554
+ async shutdown() {
10555
+ await this.moduleRegistry.teardownAll();
10556
+ this._healthMonitor?.stop();
10557
+ this.stopDashboard();
10558
+ this.memoryStore.close();
10559
+ if (this.cleanupInterval) {
10560
+ clearInterval(this.cleanupInterval);
10561
+ this.cleanupInterval = null;
10562
+ }
10563
+ if (this.stateChangeTimer) {
10564
+ clearTimeout(this.stateChangeTimer);
10565
+ this.stateChangeTimer = null;
10566
+ }
9045
10567
  this.agents.forEach((agent) => {
9046
10568
  agent.status = "terminated";
9047
10569
  });
9048
10570
  this.agents.clear();
9049
10571
  this.running = false;
9050
10572
  this.emit("orchestrator:shutdown", {});
10573
+ this.onStateChange?.();
10574
+ }
10575
+ cleanupStaleData() {
10576
+ const now = Date.now();
10577
+ for (const [id, agent] of this.agents) {
10578
+ if (agent.status === "terminated" && now - agent.lastActivity.getTime() > 3600000) {
10579
+ this.agents.delete(id);
10580
+ }
10581
+ }
10582
+ if (this.costHistory.length > 1000) {
10583
+ this.costHistory = this.costHistory.slice(-500);
10584
+ }
10585
+ }
10586
+ enforceAgentLimit() {
10587
+ const MAX_AGENTS = this.config.maxConcurrency || 10;
10588
+ if (this.agents.size > MAX_AGENTS) {
10589
+ const terminated = [...this.agents.entries()].filter(([_, a]) => a.status === "terminated").sort((a, b) => a[1].lastActivity.getTime() - b[1].lastActivity.getTime());
10590
+ const toRemove = terminated.slice(0, this.agents.size - MAX_AGENTS);
10591
+ for (const [id] of toRemove) {
10592
+ this.agents.delete(id);
10593
+ }
10594
+ }
10595
+ }
10596
+ getOrCreateHealthMonitor() {
10597
+ if (!this._healthMonitor) {
10598
+ this._healthMonitor = new HealthMonitor({
10599
+ checkInterval: this.config.agents.healthCheckInterval
10600
+ });
10601
+ }
10602
+ return this._healthMonitor;
9051
10603
  }
9052
10604
  collectResults() {
9053
10605
  const results = [];
9054
10606
  if (this.dag) {
9055
10607
  this.dag.nodes.forEach((node) => {
9056
- if (node.result) {
10608
+ if (node.result)
9057
10609
  results.push(node.result);
9058
- }
9059
10610
  });
9060
10611
  }
9061
10612
  return results;
9062
10613
  }
9063
- getAgentsByStatus() {
9064
- const counts = {};
9065
- this.agents.forEach((agent) => {
9066
- counts[agent.status] = (counts[agent.status] || 0) + 1;
9067
- });
9068
- return counts;
9069
- }
9070
10614
  sleep(ms) {
9071
10615
  return new Promise((resolve) => setTimeout(resolve, ms));
9072
10616
  }
@@ -9076,9 +10620,8 @@ class NexusOrchestrator {
9076
10620
  this.eventHandlers.set(event, handlers);
9077
10621
  return () => {
9078
10622
  const idx = handlers.indexOf(handler);
9079
- if (idx > -1) {
10623
+ if (idx > -1)
9080
10624
  handlers.splice(idx, 1);
9081
- }
9082
10625
  };
9083
10626
  }
9084
10627
  emit(event, data) {
@@ -9090,31 +10633,113 @@ class NexusOrchestrator {
9090
10633
  const command = parts[1];
9091
10634
  switch (command) {
9092
10635
  case "status":
9093
- console.log(this.getStatus(true));
9094
- break;
10636
+ return this.getStatus(true);
9095
10637
  case "agents":
9096
- console.log(this.listAgents(parts[2]));
9097
- break;
10638
+ return this.listAgents(parts[2]);
9098
10639
  case "costs":
9099
- console.log(this.getCostReport());
9100
- break;
10640
+ return this.getCostReport();
9101
10641
  case "pause":
9102
10642
  this.pause();
9103
- break;
10643
+ return "Orchestrator paused";
9104
10644
  case "resume":
9105
10645
  this.resume();
9106
- break;
10646
+ return "Orchestrator resumed";
10647
+ case "dashboard":
10648
+ return JSON.stringify(this.getState(), null, 2);
9107
10649
  default:
9108
- console.log("Unknown command. Use /nexus help");
10650
+ return "Unknown command. Available: status, agents, costs, pause, resume, dashboard";
9109
10651
  }
9110
10652
  }
9111
10653
  }
9112
10654
 
10655
+ // src/templates.ts
10656
+ var TEMPLATES = {
10657
+ feature: {
10658
+ name: "Feature Development",
10659
+ description: "Full feature pipeline: architect → coder → reviewer → tester",
10660
+ steps: [
10661
+ { role: "architect", name: "Design", description: "Design the feature architecture and API", files: { include: ["**/*.ts"], exclude: ["node_modules"] } },
10662
+ { role: "coder", name: "Implement", description: "Implement the feature according to design", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10663
+ { role: "reviewer", name: "Review", description: "Code review for quality, security, and correctness", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10664
+ { role: "tester", name: "Test", description: "Write and run tests for the feature", files: { include: ["test/**/*.ts"], exclude: ["node_modules"] } }
10665
+ ]
10666
+ },
10667
+ bugfix: {
10668
+ name: "Bug Fix",
10669
+ description: "Bug investigation and fix: explorer → coder → reviewer",
10670
+ steps: [
10671
+ { role: "explorer", name: "Investigate", description: "Investigate the bug root cause", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10672
+ { role: "coder", name: "Fix", description: "Implement the bug fix", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10673
+ { role: "reviewer", name: "Review", description: "Review the fix for correctness", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } }
10674
+ ]
10675
+ },
10676
+ refactor: {
10677
+ name: "Refactoring",
10678
+ description: "Code refactoring: architect → coder → reviewer → tester",
10679
+ steps: [
10680
+ { role: "architect", name: "Plan", description: "Plan the refactoring approach", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10681
+ { role: "coder", name: "Refactor", description: "Execute the refactoring", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10682
+ { role: "reviewer", name: "Review", description: "Review refactored code", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10683
+ { role: "tester", name: "Verify", description: "Run existing tests to ensure no regressions", files: { include: ["test/**/*.ts"], exclude: ["node_modules"] } }
10684
+ ]
10685
+ },
10686
+ documentation: {
10687
+ name: "Documentation",
10688
+ description: "Documentation update: explorer → documenter → reviewer",
10689
+ steps: [
10690
+ { role: "explorer", name: "Analyze", description: "Analyze codebase to understand what needs documentation", files: { include: ["src/**/*.ts"], exclude: ["node_modules"] } },
10691
+ { role: "documenter", name: "Write", description: "Write documentation", files: { include: ["**/*.md", "src/**/*.ts"], exclude: ["node_modules"] } },
10692
+ { role: "reviewer", name: "Review", description: "Review documentation for accuracy", files: { include: ["**/*.md"], exclude: ["node_modules"] } }
10693
+ ]
10694
+ }
10695
+ };
10696
+ var DEFAULT_COMPLEXITY = {
10697
+ overall: 50,
10698
+ factors: {
10699
+ fileCount: 1,
10700
+ codeLines: 100,
10701
+ dependencyDepth: 1,
10702
+ domainKnowledge: 1,
10703
+ riskLevel: "medium"
10704
+ }
10705
+ };
10706
+ function instantiateTemplate(templateName, baseDir) {
10707
+ const template = TEMPLATES[templateName];
10708
+ if (!template)
10709
+ throw new Error(`Unknown template: ${templateName}. Available: ${Object.keys(TEMPLATES).join(", ")}`);
10710
+ return template.steps.map((step, index) => ({
10711
+ id: `${templateName}-${index + 1}-${step.role}`,
10712
+ name: `${template.name}: ${step.name}`,
10713
+ description: step.description,
10714
+ requiredRole: step.role,
10715
+ complexity: { ...DEFAULT_COMPLEXITY },
10716
+ files: {
10717
+ include: step.files.include.map((f) => f.startsWith("/") ? f : `${baseDir}/${f}`),
10718
+ exclude: step.files.exclude
10719
+ },
10720
+ dependencies: index > 0 ? [`${templateName}-${index}-${template.steps[index - 1].role}`] : [],
10721
+ timeout: step.timeout,
10722
+ status: "pending",
10723
+ priority: "normal"
10724
+ }));
10725
+ }
10726
+ function listTemplates() {
10727
+ return Object.keys(TEMPLATES);
10728
+ }
10729
+ function getTemplate(name) {
10730
+ return TEMPLATES[name];
10731
+ }
10732
+
9113
10733
  // src/index.ts
9114
10734
  var src_default = define({
9115
10735
  id: "nexus",
9116
10736
  async setup(ctx) {
9117
10737
  const orchestrator = new NexusOrchestrator;
10738
+ await orchestrator.initialize(ctx, () => {
10739
+ const state = orchestrator.getState();
10740
+ ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state))).catch(() => {});
10741
+ });
10742
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
9118
10743
  await ctx.tool.transform((editor) => {
9119
10744
  editor.namespace({
9120
10745
  name: "nexus",
@@ -9132,7 +10757,9 @@ var src_default = define({
9132
10757
  },
9133
10758
  execute: async (input) => {
9134
10759
  const { detailed } = input;
9135
- return { content: orchestrator.getStatus(detailed) };
10760
+ const status = orchestrator.getStatus(detailed);
10761
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
10762
+ return { content: status };
9136
10763
  }
9137
10764
  });
9138
10765
  editor.add({
@@ -9162,10 +10789,231 @@ var src_default = define({
9162
10789
  return { content: orchestrator.getCostReport() };
9163
10790
  }
9164
10791
  });
10792
+ editor.add({
10793
+ name: "dashboard",
10794
+ description: "Get full orchestrator state for dashboard display",
10795
+ input: {
10796
+ type: "object",
10797
+ properties: {},
10798
+ additionalProperties: false
10799
+ },
10800
+ execute: async () => {
10801
+ const state = orchestrator.getState();
10802
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state)));
10803
+ return { content: JSON.stringify(state, null, 2) };
10804
+ }
10805
+ });
10806
+ editor.add({
10807
+ name: "queue",
10808
+ description: "Show current task queue with priorities",
10809
+ input: {
10810
+ type: "object",
10811
+ properties: {},
10812
+ additionalProperties: false
10813
+ },
10814
+ execute: async () => {
10815
+ const state = orchestrator.getState();
10816
+ const tasks = state.tasks || [];
10817
+ return { content: JSON.stringify(tasks, null, 2) };
10818
+ }
10819
+ });
10820
+ editor.add({
10821
+ name: "config.save",
10822
+ description: "Save Nexus config to disk (project or global)",
10823
+ input: {
10824
+ type: "object",
10825
+ properties: {
10826
+ level: { type: "string", enum: ["project", "global"], description: "Config level to save" },
10827
+ basePath: { type: "string", description: "Project root (for project-level, defaults to cwd)" }
10828
+ },
10829
+ required: ["level"],
10830
+ additionalProperties: false
10831
+ },
10832
+ execute: async (input) => {
10833
+ const { level, basePath } = input;
10834
+ orchestrator.configManager.saveConfig(level, basePath || process.cwd());
10835
+ const location = level === "project" ? `${basePath || process.cwd()}/.opencode/nexus.jsonc` : "~/.config/opencode/nexus.jsonc";
10836
+ return { content: `Config saved to ${level} level at ${location}` };
10837
+ }
10838
+ });
10839
+ editor.add({
10840
+ name: "config.init",
10841
+ description: "Initialize default config files for project and/or global",
10842
+ input: {
10843
+ type: "object",
10844
+ properties: {
10845
+ level: { type: "string", enum: ["project", "global", "both"], description: "Which config to initialize" },
10846
+ basePath: { type: "string", description: "Project root" }
10847
+ },
10848
+ required: ["level"],
10849
+ additionalProperties: false
10850
+ },
10851
+ execute: async (input) => {
10852
+ const { level, basePath } = input;
10853
+ const path = basePath || process.cwd();
10854
+ const locations = [];
10855
+ if (level === "project" || level === "both") {
10856
+ orchestrator.configManager.saveProjectConfig(path);
10857
+ locations.push(`${path}/.opencode/nexus.jsonc`);
10858
+ }
10859
+ if (level === "global" || level === "both") {
10860
+ orchestrator.configManager.saveGlobalConfig();
10861
+ locations.push("~/.config/opencode/nexus.jsonc");
10862
+ }
10863
+ return { content: `Config initialized at ${level} level(s): ${locations.join(", ")}` };
10864
+ }
10865
+ });
10866
+ editor.add({
10867
+ name: "preset",
10868
+ description: "Apply a preset configuration",
10869
+ input: {
10870
+ type: "object",
10871
+ properties: {
10872
+ name: { type: "string", description: "Preset name (minimal, balanced, enterprise, cost-optimized)" }
10873
+ },
10874
+ required: ["name"],
10875
+ additionalProperties: false
10876
+ },
10877
+ execute: async (input) => {
10878
+ const { name } = input;
10879
+ try {
10880
+ orchestrator.configManager.applyPreset(name);
10881
+ return { content: `Applied preset: ${PRESETS[name]?.name || name}` };
10882
+ } catch (error) {
10883
+ return { content: `Error: ${error.message}` };
10884
+ }
10885
+ }
10886
+ });
10887
+ editor.add({
10888
+ name: "dashboard.start",
10889
+ description: "Start the web dashboard server",
10890
+ input: {
10891
+ type: "object",
10892
+ properties: {
10893
+ port: { type: "number", description: "Port (default: 4747)" },
10894
+ host: { type: "string", description: "Host (default: 127.0.0.1)" }
10895
+ },
10896
+ additionalProperties: false
10897
+ },
10898
+ execute: async (input) => {
10899
+ const { port, host } = input;
10900
+ orchestrator.startDashboard(port, host);
10901
+ return { content: `Dashboard started at http://${host || "127.0.0.1"}:${port || 4747}` };
10902
+ }
10903
+ });
10904
+ editor.add({
10905
+ name: "dashboard.stop",
10906
+ description: "Stop the web dashboard server",
10907
+ input: {
10908
+ type: "object",
10909
+ properties: {},
10910
+ additionalProperties: false
10911
+ },
10912
+ execute: async () => {
10913
+ orchestrator.stopDashboard();
10914
+ return { content: "Dashboard stopped" };
10915
+ }
10916
+ });
10917
+ editor.add({
10918
+ name: "spawn",
10919
+ description: "Spawn a sub-agent for a task",
10920
+ input: {
10921
+ type: "object",
10922
+ properties: {
10923
+ role: { type: "string", description: "Agent role (architect, coder, reviewer, tester, explorer, documenter)" },
10924
+ task: { type: "string", description: "Task description" },
10925
+ model: { type: "string", description: "Model override (optional)" }
10926
+ },
10927
+ required: ["role", "task"],
10928
+ additionalProperties: false
10929
+ },
10930
+ execute: async (input) => {
10931
+ const { role, task, model } = input;
10932
+ try {
10933
+ const agent = await orchestrator.spawnAgent({ role, model });
10934
+ await orchestrator.ctx.session.prompt({
10935
+ sessionID: agent.sessionID,
10936
+ text: task
10937
+ });
10938
+ const complexity = orchestrator.analyzeComplexity({
10939
+ id: `spawn-${Date.now()}`,
10940
+ name: task,
10941
+ description: task,
10942
+ files: { include: [] },
10943
+ dependencies: [],
10944
+ requiredRole: role,
10945
+ complexity: { overall: 0, factors: { fileCount: 0, codeLines: 0, dependencyDepth: 0, domainKnowledge: 0, riskLevel: "low" } },
10946
+ priority: "normal",
10947
+ status: "running"
10948
+ });
10949
+ agent.complexity = complexity;
10950
+ const modelSelection = orchestrator.selectModel(role, complexity);
10951
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
10952
+ const taskPreview = task.length > 80 ? task.substring(0, 77) + "..." : task;
10953
+ const output = [
10954
+ `${agent.name}`,
10955
+ `\uD83D\uDCCB Task: ${taskPreview}`,
10956
+ `\uD83D\uDCCA Complexity: ${complexity.overall}/100 (${complexity.factors.riskLevel} risk)`,
10957
+ `\uD83E\uDD16 Model reasoning: ${modelSelection.reasoning}`,
10958
+ `\uD83D\uDCCE Session: ${agent.sessionID}`
10959
+ ].join(`
10960
+ `);
10961
+ return { content: output };
10962
+ } catch (error) {
10963
+ return { content: `Failed to spawn agent: ${error.message}` };
10964
+ }
10965
+ }
10966
+ });
10967
+ editor.add({
10968
+ name: "template",
10969
+ description: "List or instantiate task templates",
10970
+ input: {
10971
+ type: "object",
10972
+ properties: {
10973
+ name: { type: "string", description: "Template name to instantiate (or 'list' to show all)" },
10974
+ baseDir: { type: "string", description: "Base directory for file paths" }
10975
+ },
10976
+ additionalProperties: false
10977
+ },
10978
+ execute: async (input) => {
10979
+ const { name, baseDir } = input;
10980
+ if (!name || name === "list") {
10981
+ const templates = listTemplates();
10982
+ return { content: templates.map((t) => `${t}: ${TEMPLATES[t].description}`).join(`
10983
+ `) };
10984
+ }
10985
+ try {
10986
+ const tasks = instantiateTemplate(name, baseDir || process.cwd());
10987
+ return { content: JSON.stringify(tasks, null, 2) };
10988
+ } catch (error) {
10989
+ return { content: `Error: ${error.message}` };
10990
+ }
10991
+ }
10992
+ });
10993
+ editor.add({
10994
+ name: "security.scan",
10995
+ description: "Scan content for security issues",
10996
+ input: {
10997
+ type: "object",
10998
+ properties: {
10999
+ content: { type: "string", description: "Code content to scan" },
11000
+ filename: { type: "string", description: "Filename for context" }
11001
+ },
11002
+ required: ["content"],
11003
+ additionalProperties: false
11004
+ },
11005
+ execute: async (input) => {
11006
+ const { content, filename } = input;
11007
+ const issues = orchestrator.securityScanner.scanContent(content, filename || "unknown");
11008
+ const result = orchestrator.securityScanner.getResult();
11009
+ return { content: JSON.stringify({ issues: issues.length, score: result.score, details: issues }, null, 2) };
11010
+ }
11011
+ });
9165
11012
  });
9166
11013
  await ctx.session.hook("prompt", (event) => {
9167
- if (event.prompt.text.includes("/nexus")) {
9168
- orchestrator.handleCommand(event.prompt.text);
11014
+ if (event.prompt.text.startsWith("/nexus")) {
11015
+ const result = orchestrator.handleCommand(event.prompt.text);
11016
+ event.metadata = { ...event.metadata, nexusResult: result };
9169
11017
  }
9170
11018
  });
9171
11019
  return () => {
@@ -9175,7 +11023,21 @@ var src_default = define({
9175
11023
  });
9176
11024
  export {
9177
11025
  DEFAULT_CONFIG,
11026
+ HealthMonitor,
11027
+ LearningModule,
11028
+ MessageRouter,
11029
+ MessageStore,
11030
+ ModuleRegistry,
9178
11031
  NexusConfigManager,
9179
11032
  NexusOrchestrator,
9180
- src_default as default
11033
+ PRESETS,
11034
+ PersistentMemoryStore,
11035
+ SecurityScanner,
11036
+ StateBroadcaster,
11037
+ TEMPLATES,
11038
+ src_default as default,
11039
+ detectCycles,
11040
+ getTemplate,
11041
+ instantiateTemplate,
11042
+ listTemplates
9181
11043
  };