omnara 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ const __filename = __omnaraFileURLToPath(import.meta.url);
5
5
  const __dirname = pathShim.dirname(__filename);
6
6
 
7
7
  // src/cli.ts
8
- import path6 from "path";
8
+ import path7 from "path";
9
9
  import { createRequire as createRequire2 } from "node:module";
10
10
  import { Command as Command3 } from "commander";
11
11
 
@@ -51,13 +51,15 @@ var BASE_ALLOWED_WORKER_TOOLS = [
51
51
  ];
52
52
  var AIClient = class {
53
53
  baseOptions;
54
+ runningQuery;
54
55
  constructor(config) {
55
56
  const defaultOptions = {
56
57
  maxTurns: 100,
57
58
  model: "sonnet",
58
59
  cwd: process.cwd(),
59
60
  includePartialMessages: false,
60
- allowedTools: [...BASE_ALLOWED_ORCHESTRATOR_TOOLS]
61
+ allowedTools: [...BASE_ALLOWED_ORCHESTRATOR_TOOLS],
62
+ settingSources: ["user", "project", "local"]
61
63
  };
62
64
  this.baseOptions = {
63
65
  ...defaultOptions,
@@ -70,9 +72,11 @@ var AIClient = class {
70
72
  }
71
73
  async *streamUserMessage(prompt2, overrides) {
72
74
  const q = this.createQuery(prompt2, overrides);
75
+ this.runningQuery = q;
73
76
  for await (const message of q) {
74
77
  yield message;
75
78
  }
79
+ this.runningQuery = void 0;
76
80
  }
77
81
  createQuery(prompt2, overrides) {
78
82
  const merged = {
@@ -85,6 +89,9 @@ var AIClient = class {
85
89
  options: merged
86
90
  });
87
91
  }
92
+ async cancel() {
93
+ await this.runningQuery?.interrupt();
94
+ }
88
95
  ensureWorkingDirectory(cwd) {
89
96
  if (!cwd) {
90
97
  return;
@@ -433,6 +440,7 @@ function extractUserContent(content) {
433
440
  // src/orchestrator/worker-manager.ts
434
441
  import { EventEmitter } from "events";
435
442
  import { randomUUID as randomUUID2 } from "crypto";
443
+ import { simpleGit as simpleGit2 } from "simple-git";
436
444
 
437
445
  // src/orchestrator/tools/worker-status-tool.ts
438
446
  import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
@@ -861,9 +869,16 @@ var WorktreeManager = class {
861
869
  worktreeBasePath;
862
870
  activeWorktrees;
863
871
  enabled;
872
+ base_ref;
873
+ // Queue mechanism for sequential worktree creation
874
+ creationQueue = [];
875
+ isProcessingQueue = false;
876
+ // Map to track worktrees by name (not workerId) to enable reuse
877
+ worktreesByName = /* @__PURE__ */ new Map();
864
878
  constructor(options) {
865
879
  this.repoPath = options.repoPath;
866
880
  this.enabled = options.enabled ?? false;
881
+ this.base_ref = options.base_ref;
867
882
  const repoName = path4.basename(this.repoPath);
868
883
  this.worktreeBasePath = options.worktreeBasePath || path4.join(DEFAULT_DIR, "worktrees", repoName);
869
884
  this.git = simpleGit(this.repoPath);
@@ -872,6 +887,33 @@ var WorktreeManager = class {
872
887
  this.validateGitRepository();
873
888
  }
874
889
  }
890
+ /**
891
+ * Find a valid base ref to use when creating new branches
892
+ * Tries in order: configured base_ref, main, master, origin/HEAD
893
+ * @returns The first valid ref found, or undefined if none exist
894
+ */
895
+ async findValidBaseRef() {
896
+ if (this.base_ref) {
897
+ try {
898
+ await this.git.raw(["rev-parse", "--verify", this.base_ref]);
899
+ logger.info(`Using configured base_ref: ${this.base_ref}`);
900
+ return this.base_ref;
901
+ } catch {
902
+ logger.warn(`Configured base_ref "${this.base_ref}" does not exist, trying fallbacks`);
903
+ }
904
+ }
905
+ const fallbackRefs = ["main", "master", "origin/HEAD"];
906
+ for (const ref of fallbackRefs) {
907
+ try {
908
+ await this.git.raw(["rev-parse", "--verify", ref]);
909
+ logger.info(`Using fallback base_ref: ${ref}`);
910
+ return ref;
911
+ } catch {
912
+ }
913
+ }
914
+ logger.warn("No valid base ref found (tried: configured base_ref, main, master, origin/HEAD)");
915
+ return void 0;
916
+ }
875
917
  /**
876
918
  * Validate that the repoPath is a git repository
877
919
  * Logs an error if worktrees are enabled but not in a git repository
@@ -899,6 +941,10 @@ var WorktreeManager = class {
899
941
  /**
900
942
  * Creates a new worktree for the specified worker
901
943
  *
944
+ * This method uses a sequential queue to ensure that worktree creation
945
+ * happens one at a time, eliminating race conditions when multiple workers
946
+ * request the same worktree simultaneously.
947
+ *
902
948
  * @param workerId - Unique identifier for the worker
903
949
  * @param worktreeName - Semantic name for the worktree (e.g., "main-feature", "bugfix-auth")
904
950
  * @returns Information about the created worktree
@@ -912,15 +958,81 @@ var WorktreeManager = class {
912
958
  "create"
913
959
  );
914
960
  }
915
- logger.info(`Creating worktree "${worktreeName}" for worker ${workerId}`);
961
+ if (this.activeWorktrees.has(workerId)) {
962
+ throw new WorktreeError(`Worktree already exists for worker ${workerId}`, workerId, "create");
963
+ }
964
+ return new Promise((resolve, reject) => {
965
+ this.creationQueue.push({
966
+ workerId,
967
+ worktreeName,
968
+ resolve,
969
+ reject
970
+ });
971
+ logger.info(
972
+ `Queued worktree creation for "${worktreeName}" (worker: ${workerId}). Queue size: ${this.creationQueue.length}`
973
+ );
974
+ if (!this.isProcessingQueue) {
975
+ this.processCreationQueue();
976
+ }
977
+ });
978
+ }
979
+ /**
980
+ * Processes the creation queue sequentially
981
+ * Ensures only one worktree creation happens at a time
982
+ */
983
+ async processCreationQueue() {
984
+ if (this.isProcessingQueue) {
985
+ return;
986
+ }
987
+ this.isProcessingQueue = true;
916
988
  try {
917
- if (this.activeWorktrees.has(workerId)) {
918
- throw new WorktreeError(
919
- `Worktree already exists for worker ${workerId}`,
920
- workerId,
921
- "create"
922
- );
989
+ while (this.creationQueue.length > 0) {
990
+ const request = this.creationQueue.shift();
991
+ try {
992
+ const existingWorktree = this.worktreesByName.get(request.worktreeName);
993
+ if (existingWorktree) {
994
+ logger.info(
995
+ `Worker ${request.workerId} reusing existing worktree "${request.worktreeName}" at ${existingWorktree.worktreePath}`
996
+ );
997
+ const info = {
998
+ workerId: request.workerId,
999
+ worktreeName: request.worktreeName,
1000
+ worktreePath: existingWorktree.worktreePath,
1001
+ branchName: existingWorktree.branchName,
1002
+ createdAt: /* @__PURE__ */ new Date()
1003
+ };
1004
+ this.activeWorktrees.set(request.workerId, info);
1005
+ request.resolve(info);
1006
+ } else {
1007
+ const info = await this.doCreateWorktree(request.workerId, request.worktreeName);
1008
+ request.resolve(info);
1009
+ }
1010
+ } catch (error) {
1011
+ logger.error(
1012
+ `Failed to process worktree creation for worker ${request.workerId}:`,
1013
+ error
1014
+ );
1015
+ request.reject(
1016
+ error instanceof WorktreeError ? error : new WorktreeError(
1017
+ `Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`,
1018
+ request.workerId,
1019
+ "create",
1020
+ error instanceof Error ? error : void 0
1021
+ )
1022
+ );
1023
+ }
923
1024
  }
1025
+ } finally {
1026
+ this.isProcessingQueue = false;
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Internal method that performs the actual worktree creation
1031
+ * This should only be called by processCreationQueue
1032
+ */
1033
+ async doCreateWorktree(workerId, worktreeName) {
1034
+ logger.info(`Creating worktree "${worktreeName}" for worker ${workerId}`);
1035
+ try {
924
1036
  await fs4.mkdir(this.worktreeBasePath, { recursive: true });
925
1037
  const branchName = `${worktreeName}`;
926
1038
  const worktreePath = path4.join(this.worktreeBasePath, worktreeName);
@@ -948,6 +1060,7 @@ var WorktreeManager = class {
948
1060
  createdAt: /* @__PURE__ */ new Date()
949
1061
  };
950
1062
  this.activeWorktrees.set(workerId, info2);
1063
+ this.worktreesByName.set(worktreeName, info2);
951
1064
  return info2;
952
1065
  }
953
1066
  if (worktreeDirExists && !worktreeRegistered) {
@@ -963,10 +1076,18 @@ var WorktreeManager = class {
963
1076
  );
964
1077
  await this.git.raw(["worktree", "add", worktreePath, branchName]);
965
1078
  } else if (!branchExists && !worktreeRegistered) {
966
- logger.info(
967
- `Creating new worktree "${worktreeName}" at ${worktreePath} with new branch ${branchName}`
968
- );
969
- await this.git.raw(["worktree", "add", "-b", branchName, worktreePath]);
1079
+ const baseRef = await this.findValidBaseRef();
1080
+ if (baseRef) {
1081
+ logger.info(
1082
+ `Creating new worktree "${worktreeName}" at ${worktreePath} with new branch ${branchName} from base ref ${baseRef}`
1083
+ );
1084
+ await this.git.raw(["worktree", "add", "-b", branchName, worktreePath, baseRef]);
1085
+ } else {
1086
+ logger.info(
1087
+ `Creating new worktree "${worktreeName}" at ${worktreePath} with new branch ${branchName} (no base ref specified)`
1088
+ );
1089
+ await this.git.raw(["worktree", "add", "-b", branchName, worktreePath]);
1090
+ }
970
1091
  }
971
1092
  const info = {
972
1093
  workerId,
@@ -976,6 +1097,7 @@ var WorktreeManager = class {
976
1097
  createdAt: /* @__PURE__ */ new Date()
977
1098
  };
978
1099
  this.activeWorktrees.set(workerId, info);
1100
+ this.worktreesByName.set(worktreeName, info);
979
1101
  logger.info(`Worktree "${worktreeName}" ready at ${worktreePath} on branch ${branchName}`);
980
1102
  return info;
981
1103
  } catch (error) {
@@ -1006,30 +1128,42 @@ var WorktreeManager = class {
1006
1128
  return;
1007
1129
  }
1008
1130
  try {
1009
- const args = ["worktree", "remove", info.worktreePath];
1010
- if (force) {
1011
- args.push("--force");
1012
- }
1013
- try {
1014
- await this.git.raw(args);
1015
- } catch (worktreeError) {
1016
- const errorMessage = worktreeError instanceof Error ? worktreeError.message : String(worktreeError);
1017
- if (errorMessage.includes("is not a working tree") || errorMessage.includes("does not exist")) {
1018
- logger.warn(
1019
- `Worktree ${info.worktreePath} doesn't exist, may have been manually removed. Cleaning up...`
1020
- );
1021
- await this.git.raw(["worktree", "prune"]);
1022
- try {
1023
- await fs4.rm(info.worktreePath, { recursive: true, force: true });
1024
- logger.info(`Manually removed stale worktree directory: ${info.worktreePath}`);
1025
- } catch {
1131
+ const otherWorkersUsingWorktree = Array.from(this.activeWorktrees.values()).filter(
1132
+ (wt) => wt.worktreeName === info.worktreeName && wt.workerId !== workerId
1133
+ );
1134
+ const shouldRemoveWorktree = otherWorkersUsingWorktree.length === 0;
1135
+ if (shouldRemoveWorktree) {
1136
+ const args = ["worktree", "remove", info.worktreePath];
1137
+ if (force) {
1138
+ args.push("--force");
1139
+ }
1140
+ try {
1141
+ await this.git.raw(args);
1142
+ logger.info(`Removed worktree "${info.worktreeName}" at ${info.worktreePath}`);
1143
+ } catch (worktreeError) {
1144
+ const errorMessage = worktreeError instanceof Error ? worktreeError.message : String(worktreeError);
1145
+ if (errorMessage.includes("is not a working tree") || errorMessage.includes("does not exist")) {
1146
+ logger.warn(
1147
+ `Worktree ${info.worktreePath} doesn't exist, may have been manually removed. Cleaning up...`
1148
+ );
1149
+ await this.git.raw(["worktree", "prune"]);
1150
+ try {
1151
+ await fs4.rm(info.worktreePath, { recursive: true, force: true });
1152
+ logger.info(`Manually removed stale worktree directory: ${info.worktreePath}`);
1153
+ } catch {
1154
+ }
1155
+ } else {
1156
+ throw worktreeError;
1026
1157
  }
1027
- } else {
1028
- throw worktreeError;
1029
1158
  }
1159
+ this.worktreesByName.delete(info.worktreeName);
1160
+ } else {
1161
+ logger.info(
1162
+ `Not removing worktree "${info.worktreeName}" - still in use by ${otherWorkersUsingWorktree.length} other worker(s)`
1163
+ );
1030
1164
  }
1031
1165
  this.activeWorktrees.delete(workerId);
1032
- logger.info(`Removed worktree for worker ${workerId}`);
1166
+ logger.info(`Removed worktree reference for worker ${workerId}`);
1033
1167
  } catch (error) {
1034
1168
  logger.error(`Failed to remove worktree for worker ${workerId}:`, error);
1035
1169
  throw new WorktreeError(
@@ -1108,41 +1242,6 @@ var WorktreeManager = class {
1108
1242
  }
1109
1243
  };
1110
1244
 
1111
- // src/config/omnara-config.ts
1112
- import { parse as parseJsonc } from "jsonc-parser";
1113
- import * as fs5 from "fs/promises";
1114
- import * as path5 from "path";
1115
- import * as z2 from "zod";
1116
- var OmnaraConfig = z2.object({
1117
- worktrees: z2.object({
1118
- enabled: z2.boolean().default(false)
1119
- }).default({
1120
- enabled: false
1121
- })
1122
- });
1123
- async function readOmnaraConfig(configPath) {
1124
- const resolveConfig = async (path7) => {
1125
- if (!path7) {
1126
- return null;
1127
- }
1128
- await fs5.access(path7);
1129
- const content = await fs5.readFile(path7, "utf-8");
1130
- const errors = [];
1131
- const config = parseJsonc(content, errors, {
1132
- allowTrailingComma: true,
1133
- allowEmptyContent: true
1134
- });
1135
- return OmnaraConfig.parse(config);
1136
- };
1137
- const resolveDefault = () => {
1138
- return OmnaraConfig.parse({});
1139
- };
1140
- return await resolveConfig(configPath).catch(() => null) ?? await resolveConfig(path5.join(process.cwd(), "omnara.jsonc")).catch(() => null) ?? await resolveConfig(path5.join(process.cwd(), "omnara.json")).catch(() => null) ?? await resolveDefault();
1141
- }
1142
- function isWorktreesEnabled(config) {
1143
- return config.worktrees.enabled;
1144
- }
1145
-
1146
1245
  // src/orchestrator/worker-manager.ts
1147
1246
  var WorkerManager = class extends EventEmitter {
1148
1247
  constructor(options) {
@@ -1150,37 +1249,16 @@ var WorkerManager = class extends EventEmitter {
1150
1249
  this.options = options;
1151
1250
  this.worktreeManager = new WorktreeManager({
1152
1251
  repoPath: options.repoPath,
1153
- enabled: false
1252
+ enabled: options.worktreesEnabled,
1253
+ base_ref: options.worktreesBaseRef
1154
1254
  });
1155
- this.initializeConfig();
1255
+ logger.info(
1256
+ `WorkerManager initialized with worktrees ${options.worktreesEnabled ? "enabled" : "disabled"}`
1257
+ );
1156
1258
  }
1157
1259
  workers = /* @__PURE__ */ new Map();
1158
1260
  runningQueries = /* @__PURE__ */ new Map();
1159
1261
  worktreeManager;
1160
- configInitialized = false;
1161
- /**
1162
- * Initialize the configuration and update WorktreeManager accordingly
1163
- */
1164
- async initializeConfig() {
1165
- try {
1166
- const config = await readOmnaraConfig();
1167
- const worktreesEnabled = isWorktreesEnabled(config);
1168
- if (worktreesEnabled) {
1169
- logger.info("Worktrees enabled via omnara.jsonc configuration");
1170
- } else {
1171
- logger.info("Worktrees disabled (no worktrees config found in omnara.jsonc)");
1172
- }
1173
- this.worktreeManager = new WorktreeManager({
1174
- repoPath: this.options.repoPath,
1175
- enabled: worktreesEnabled
1176
- });
1177
- this.configInitialized = true;
1178
- } catch (err) {
1179
- logger.error("Failed to read omnara.jsonc configuration:", err);
1180
- logger.info("Worktrees disabled due to config read error");
1181
- this.configInitialized = true;
1182
- }
1183
- }
1184
1262
  listWorkers() {
1185
1263
  return Array.from(this.workers.values());
1186
1264
  }
@@ -1333,9 +1411,9 @@ var WorkerManager = class extends EventEmitter {
1333
1411
  success: false,
1334
1412
  text: "Worker canceled by user request."
1335
1413
  });
1336
- const query2 = this.runningQueries.get(id);
1337
- if (query2) {
1338
- void query2.interrupt().catch(() => {
1414
+ const query3 = this.runningQueries.get(id);
1415
+ if (query3) {
1416
+ void query3.interrupt().catch(() => {
1339
1417
  });
1340
1418
  }
1341
1419
  record.messageQueue.close();
@@ -1385,10 +1463,10 @@ var WorkerManager = class extends EventEmitter {
1385
1463
  });
1386
1464
  record.lastUpdateAt = Date.now();
1387
1465
  if (interrupt) {
1388
- const query2 = this.runningQueries.get(record.id);
1389
- if (query2) {
1466
+ const query3 = this.runningQueries.get(record.id);
1467
+ if (query3) {
1390
1468
  try {
1391
- await query2.interrupt();
1469
+ await query3.interrupt();
1392
1470
  } catch (error) {
1393
1471
  console.error("[WorkerManager] Failed to interrupt worker:", error);
1394
1472
  }
@@ -1502,7 +1580,7 @@ var WorkerManager = class extends EventEmitter {
1502
1580
  record.sessionId,
1503
1581
  record.messageQueue
1504
1582
  );
1505
- const query2 = client.createQuery(messageStream, {
1583
+ const query3 = client.createQuery(messageStream, {
1506
1584
  hooks: {
1507
1585
  PostToolUse: [
1508
1586
  {
@@ -1543,13 +1621,37 @@ var WorkerManager = class extends EventEmitter {
1543
1621
  ]
1544
1622
  }
1545
1623
  });
1546
- this.runningQueries.set(record.id, query2);
1624
+ this.runningQueries.set(record.id, query3);
1547
1625
  const summarizedChunks = [];
1548
1626
  try {
1549
- for await (const message of query2) {
1627
+ for await (const message of query3) {
1550
1628
  this.handleMessage(record, message, summarizedChunks);
1551
1629
  }
1552
1630
  if (record.status === "running") {
1631
+ if (record.worktreePath) {
1632
+ try {
1633
+ logger.info(
1634
+ `Auto-committing changes for worker ${record.id} in worktree ${record.worktreePath}`
1635
+ );
1636
+ const worktreeGit = simpleGit2(record.worktreePath);
1637
+ await worktreeGit.add("-A");
1638
+ const commitMessage = `Worker task: ${record.task}
1639
+
1640
+ Automatically committed changes from worker completion.`;
1641
+ await worktreeGit.commit(commitMessage);
1642
+ logger.info(`Successfully auto-committed changes for worker ${record.id}`);
1643
+ record.progress.push({
1644
+ timestamp: Date.now(),
1645
+ message: "Auto-committed all changes to git"
1646
+ });
1647
+ } catch (commitError) {
1648
+ logger.error(`Failed to auto-commit changes for worker ${record.id}:`, commitError);
1649
+ record.progress.push({
1650
+ timestamp: Date.now(),
1651
+ message: `Failed to auto-commit changes: ${commitError instanceof Error ? commitError.message : String(commitError)}`
1652
+ });
1653
+ }
1654
+ }
1553
1655
  record.status = "completed";
1554
1656
  record.result = summarizedChunks.join("\n").trim() || "Worker completed without additional details.";
1555
1657
  record.lastUpdateAt = Date.now();
@@ -1731,12 +1833,7 @@ WORKTREE ISOLATION:
1731
1833
  - All your file operations, git commands, and changes are isolated to this worktree.
1732
1834
  - Other workers cannot see your changes and you cannot see theirs.
1733
1835
  - When you're done, your worktree will be automatically cleaned up.
1734
-
1735
- COMMIT YOUR CHANGES:
1736
- - Before completing your task, commit your changes using git.
1737
- - Use a descriptive commit message that explains what you did.
1738
- - Your commits will stay on your isolated branch and can be reviewed before merging.
1739
- - This ensures your work is preserved even if cleanup happens unexpectedly.
1836
+ - Your changes will be automatically committed when you complete your task. Focus on making your changes and reporting your results.
1740
1837
  `;
1741
1838
  }
1742
1839
  return `${userPrompt.trim()}
@@ -1766,7 +1863,7 @@ ${directive}`;
1766
1863
 
1767
1864
  // src/orchestrator/tools/worker-tools.ts
1768
1865
  import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
1769
- import { z as z3 } from "zod";
1866
+ import { z as z2 } from "zod";
1770
1867
  function createWorkerMcpServer(workerManager, context) {
1771
1868
  return createSdkMcpServer2({
1772
1869
  name: "worker",
@@ -1796,14 +1893,14 @@ Usage notes:
1796
1893
  - The worker's result is returned to you, not the user - you must summarize or forward relevant information
1797
1894
  - Worker progress status is automatically included in your context - you can see their current state without additional tools`,
1798
1895
  {
1799
- task: z3.string().min(1, "task name is required").describe(
1896
+ task: z2.string().min(1, "task name is required").describe(
1800
1897
  'A short, unique name for this task (e.g., "FizzBuzz Python", "Search codebase", "Fix bug #123"). Used to identify and track this worker.'
1801
1898
  ),
1802
- prompt: z3.string().min(1, "prompt is required"),
1803
- worktree_name: z3.string().min(1, "worktree_name is required").describe(
1899
+ prompt: z2.string().min(1, "prompt is required"),
1900
+ worktree_name: z2.string().min(1, "worktree_name is required").describe(
1804
1901
  "A short descriptive name for a worktree to create that can be used by workers simultaneously. This is synonymous with branch."
1805
1902
  ),
1806
- metadata: z3.record(z3.any()).optional()
1903
+ metadata: z2.record(z2.any()).optional()
1807
1904
  },
1808
1905
  async (args, extra) => {
1809
1906
  const typedExtra = extra;
@@ -1901,11 +1998,11 @@ Orchestrator: [launches worker: "refactor database"]
1901
1998
  User: "Wait, actually don't touch the database - just refactor the API layer instead"
1902
1999
  Orchestrator: [message_worker: task="refactor database", message="User wants to change scope - refactor API layer instead, leave database alone", interrupt=true]`,
1903
2000
  {
1904
- task: z3.string().min(1, "task name is required").describe("The task name of the worker to message"),
1905
- message: z3.string().min(1, "message is required").describe(
2001
+ task: z2.string().min(1, "task name is required").describe("The task name of the worker to message"),
2002
+ message: z2.string().min(1, "message is required").describe(
1906
2003
  "The message to send to the worker. Be clear and specific about what has changed or what additional context is being provided."
1907
2004
  ),
1908
- interrupt: z3.boolean().default(false).describe(
2005
+ interrupt: z2.boolean().default(false).describe(
1909
2006
  "If true, interrupt current generation immediately; if false (default), queue message for next turn"
1910
2007
  )
1911
2008
  },
@@ -1976,11 +2073,234 @@ import { readFileSync } from "fs";
1976
2073
  var orchestratorPromptUrl = new URL("./orchestrator-prompt.md", import.meta.url);
1977
2074
  var ORCHESTRATOR_SYSTEM_PROMPT = readFileSync(orchestratorPromptUrl, "utf-8");
1978
2075
 
2076
+ // src/orchestrator/agent-orchestrator.ts
2077
+ import "@anthropic-ai/claude-agent-sdk";
2078
+
2079
+ // src/orchestrator/utils/completions-manager.ts
2080
+ import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2081
+ import { glob } from "glob";
2082
+ import * as path5 from "path";
2083
+ var CompletionsManager = class {
2084
+ slashCommandsCache;
2085
+ filesCache = /* @__PURE__ */ new Map();
2086
+ async fetchSlashCommands() {
2087
+ if (this.slashCommandsCache && Date.now() - this.slashCommandsCache.fetched.getTime() < 5 * 60 * 1e3) {
2088
+ return this.slashCommandsCache.slashCommands;
2089
+ }
2090
+ let commands = [];
2091
+ for await (const message of query2({
2092
+ prompt: "Respond with one word.",
2093
+ options: { maxTurns: 1, model: "haiku", settingSources: ["user", "project", "local"] }
2094
+ })) {
2095
+ if (message.type === "system" && message.subtype === "init") {
2096
+ console.log("Fetched available slash commands:", message.slash_commands);
2097
+ commands = message.slash_commands;
2098
+ }
2099
+ }
2100
+ this.slashCommandsCache = {
2101
+ fetched: /* @__PURE__ */ new Date(),
2102
+ slashCommands: commands
2103
+ };
2104
+ return commands;
2105
+ }
2106
+ async fetchFiles(searchPattern = "") {
2107
+ const cached = this.filesCache.get(searchPattern);
2108
+ if (cached && Date.now() - cached.fetched.getTime() < 30 * 1e3) {
2109
+ return cached.files;
2110
+ }
2111
+ try {
2112
+ let patterns = [];
2113
+ if (searchPattern === "") {
2114
+ patterns = [
2115
+ "package.json",
2116
+ "tsconfig.json",
2117
+ "pyproject.toml",
2118
+ "Cargo.toml",
2119
+ "README.md",
2120
+ "CLAUDE.md",
2121
+ "src/index.*",
2122
+ "src/main.*",
2123
+ "src/app.*",
2124
+ "index.*",
2125
+ "main.*",
2126
+ "app.*",
2127
+ "*.config.*",
2128
+ "src/**/*.tsx",
2129
+ "src/**/*.ts",
2130
+ "*.py"
2131
+ ];
2132
+ } else {
2133
+ if (searchPattern.includes("/")) {
2134
+ patterns.push(
2135
+ `**/${searchPattern}*`,
2136
+ // Match in any directory
2137
+ `**/${searchPattern}*/**`,
2138
+ // Match directories
2139
+ `${searchPattern}*`,
2140
+ // Match from root
2141
+ `${searchPattern}*/**`
2142
+ // Match root directories
2143
+ );
2144
+ } else {
2145
+ patterns.push(
2146
+ `**/*${searchPattern}*`,
2147
+ // Files containing the pattern
2148
+ `**/*${searchPattern}*.*`,
2149
+ // Files with extensions containing pattern
2150
+ `**/${searchPattern}*`,
2151
+ // Files starting with the pattern
2152
+ `${searchPattern}*`
2153
+ // Files in current directory
2154
+ );
2155
+ }
2156
+ }
2157
+ const allFiles = /* @__PURE__ */ new Set();
2158
+ for (const pattern of patterns) {
2159
+ try {
2160
+ const files = await glob(pattern, {
2161
+ ignore: [
2162
+ "**/node_modules/**",
2163
+ "**/.git/**",
2164
+ "**/dist/**",
2165
+ "**/build/**",
2166
+ "**/.venv/**",
2167
+ "**/target/**",
2168
+ "**/__pycache__/**",
2169
+ "**/.pytest_cache/**",
2170
+ "**/.next/**",
2171
+ "**/.nuxt/**",
2172
+ "**/coverage/**",
2173
+ "**/*.min.js",
2174
+ "**/*.map",
2175
+ "**/vendor/**",
2176
+ "**/.cache/**"
2177
+ ],
2178
+ maxDepth: 10,
2179
+ absolute: false,
2180
+ nodir: true
2181
+ // Only files, not directories
2182
+ });
2183
+ files.forEach((f) => allFiles.add(f));
2184
+ } catch {
2185
+ }
2186
+ }
2187
+ const fileList = Array.from(allFiles).sort((a, b) => {
2188
+ if (searchPattern === "") {
2189
+ const importantFiles = [
2190
+ "package.json",
2191
+ "tsconfig.json",
2192
+ "pyproject.toml",
2193
+ "README.md",
2194
+ "CLAUDE.md"
2195
+ ];
2196
+ const aName2 = path5.basename(a);
2197
+ const bName2 = path5.basename(b);
2198
+ const aImportant = importantFiles.includes(aName2);
2199
+ const bImportant = importantFiles.includes(bName2);
2200
+ if (aImportant && !bImportant) return -1;
2201
+ if (!aImportant && bImportant) return 1;
2202
+ const aDepth2 = (a.match(/\//g) || []).length;
2203
+ const bDepth2 = (b.match(/\//g) || []).length;
2204
+ if (aDepth2 !== bDepth2) return aDepth2 - bDepth2;
2205
+ return a.localeCompare(b);
2206
+ }
2207
+ const aName = path5.basename(a).toLowerCase();
2208
+ const bName = path5.basename(b).toLowerCase();
2209
+ const searchLower = searchPattern.toLowerCase();
2210
+ const aExact = aName === searchLower;
2211
+ const bExact = bName === searchLower;
2212
+ if (aExact && !bExact) return -1;
2213
+ if (!aExact && bExact) return 1;
2214
+ const aStarts = aName.startsWith(searchLower);
2215
+ const bStarts = bName.startsWith(searchLower);
2216
+ if (aStarts && !bStarts) return -1;
2217
+ if (!aStarts && bStarts) return 1;
2218
+ const aDepth = (a.match(/\//g) || []).length;
2219
+ const bDepth = (b.match(/\//g) || []).length;
2220
+ if (aDepth !== bDepth) return aDepth - bDepth;
2221
+ return a.localeCompare(b);
2222
+ }).slice(0, searchPattern === "" ? 20 : 50);
2223
+ this.filesCache.set(searchPattern, {
2224
+ fetched: /* @__PURE__ */ new Date(),
2225
+ files: fileList
2226
+ });
2227
+ if (this.filesCache.size > 100) {
2228
+ const entries = Array.from(this.filesCache.entries());
2229
+ entries.sort((a, b) => a[1].fetched.getTime() - b[1].fetched.getTime()).slice(0, 50).forEach(([key]) => this.filesCache.delete(key));
2230
+ }
2231
+ return fileList;
2232
+ } catch (error) {
2233
+ console.error("Error fetching files:", error);
2234
+ return [];
2235
+ }
2236
+ }
2237
+ async getCompletion(query3, limit = 50) {
2238
+ const completions = [];
2239
+ if (query3.startsWith("/")) {
2240
+ const slashQuery = query3.slice(1);
2241
+ const commands = (await this.fetchSlashCommands()).filter((cmd) => cmd.startsWith(slashQuery)).slice(0, limit);
2242
+ for (const cmd of commands) {
2243
+ completions.push({
2244
+ type: "slash_command",
2245
+ result: cmd
2246
+ });
2247
+ }
2248
+ } else if (query3.startsWith("@")) {
2249
+ const fileQuery = query3.slice(1);
2250
+ const files = await this.fetchFiles(fileQuery);
2251
+ for (const file of files.slice(0, limit)) {
2252
+ completions.push({
2253
+ type: "file",
2254
+ result: file
2255
+ });
2256
+ }
2257
+ }
2258
+ return completions;
2259
+ }
2260
+ };
2261
+
2262
+ // src/config/omnara-config.ts
2263
+ import { parse as parseJsonc } from "jsonc-parser";
2264
+ import * as fs5 from "fs/promises";
2265
+ import * as path6 from "path";
2266
+ import * as z3 from "zod";
2267
+ var OmnaraConfig = z3.object({
2268
+ worktrees: z3.object({
2269
+ enabled: z3.boolean().default(false),
2270
+ base_ref: z3.string().optional()
2271
+ }).default({
2272
+ enabled: false
2273
+ })
2274
+ });
2275
+ async function readOmnaraConfig(configPath) {
2276
+ const resolveConfig = async (path8) => {
2277
+ if (!path8) {
2278
+ return null;
2279
+ }
2280
+ await fs5.access(path8);
2281
+ const content = await fs5.readFile(path8, "utf-8");
2282
+ const errors = [];
2283
+ const config = parseJsonc(content, errors, {
2284
+ allowTrailingComma: true,
2285
+ allowEmptyContent: true
2286
+ });
2287
+ return OmnaraConfig.parse(config);
2288
+ };
2289
+ const resolveDefault = () => {
2290
+ return OmnaraConfig.parse({});
2291
+ };
2292
+ return await resolveConfig(configPath).catch(() => null) ?? await resolveConfig(path6.join(process.cwd(), "omnara.jsonc")).catch(() => null) ?? await resolveConfig(path6.join(process.cwd(), "omnara.json")).catch(() => null) ?? await resolveDefault();
2293
+ }
2294
+ function isWorktreesEnabled(config) {
2295
+ return config.worktrees.enabled;
2296
+ }
2297
+
1979
2298
  // src/orchestrator/agent-orchestrator.ts
1980
2299
  var WORKER_LAUNCH_TOOL = "mcp__worker__launch_worker";
1981
2300
  var WORKER_GET_TOOL = "mcp__worker__get_workers";
1982
2301
  var WORKER_MESSAGE_TOOL = "mcp__worker__message_worker";
1983
2302
  var AgentOrchestrator = class {
2303
+ completionsManager;
1984
2304
  aiClient;
1985
2305
  workerManager;
1986
2306
  sessions = /* @__PURE__ */ new Map();
@@ -2004,7 +2324,9 @@ var AgentOrchestrator = class {
2004
2324
  this.workerManager = dependencies?.workerManager ?? new WorkerManager({
2005
2325
  createClient: workerClientFactory,
2006
2326
  resolveSessionIdBySdk,
2007
- repoPath: dependencies?.repoPath ?? process.cwd()
2327
+ repoPath: dependencies?.repoPath ?? process.cwd(),
2328
+ worktreesEnabled: dependencies?.worktreesEnabled ?? false,
2329
+ worktreesBaseRef: dependencies?.worktreesBaseRef
2008
2330
  });
2009
2331
  const workerMcpServer = createWorkerMcpServer(this.workerManager, {
2010
2332
  resolveSessionIdBySdk,
@@ -2028,6 +2350,7 @@ var AgentOrchestrator = class {
2028
2350
  }));
2029
2351
  this.aiClient = mainClientFactory();
2030
2352
  this.registerWorkerListeners();
2353
+ this.completionsManager = new CompletionsManager();
2031
2354
  }
2032
2355
  ensureSessionExists(sessionId) {
2033
2356
  const existing = this.sessions.get(sessionId);
@@ -2109,6 +2432,14 @@ var AgentOrchestrator = class {
2109
2432
  this.sessions.clear();
2110
2433
  this.started = false;
2111
2434
  }
2435
+ async cancel() {
2436
+ await Promise.all(
2437
+ this.getWorkerManager().listWorkers().filter((w) => w.status === "running").map((worker) => {
2438
+ return this.getWorkerManager().cancelWorker(worker.id);
2439
+ })
2440
+ );
2441
+ await this.aiClient.cancel();
2442
+ }
2112
2443
  handleDisconnect(connection) {
2113
2444
  for (const sessionId of connection.sessionIds) {
2114
2445
  const session = this.sessions.get(sessionId);
@@ -2528,10 +2859,17 @@ var AgentOrchestrator = class {
2528
2859
  buildWorkerStatusMessage(triggerWorkerId) {
2529
2860
  const allWorkers = this.workerManager.listWorkers();
2530
2861
  if (allWorkers.length === 0) {
2531
- return "[WORKER STATUS UPDATE]\n\nNo active workers.";
2532
- }
2533
- const lines = ["[WORKER STATUS UPDATE - Generation Triggered]"];
2534
- lines.push("");
2862
+ return "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n[AUTOMATED SYSTEM NOTIFICATION: WORKER STATUS UPDATE]\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nNo active workers.\n\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nEND OF AUTOMATED NOTIFICATION\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550";
2863
+ }
2864
+ const lines = [
2865
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
2866
+ "[AUTOMATED SYSTEM NOTIFICATION: WORKER STATUS UPDATE]",
2867
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
2868
+ "",
2869
+ "This is an automated notification triggered by a worker completion.",
2870
+ "You should provide an INCREMENTAL summary of only the NEW completion to the user.",
2871
+ ""
2872
+ ];
2535
2873
  for (const worker of allWorkers) {
2536
2874
  const isTrigger = worker.id === triggerWorkerId;
2537
2875
  const prefix = isTrigger ? ">>> " : " ";
@@ -2565,6 +2903,9 @@ var AgentOrchestrator = class {
2565
2903
  }
2566
2904
  lines.push("");
2567
2905
  }
2906
+ lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2907
+ lines.push("END OF AUTOMATED NOTIFICATION");
2908
+ lines.push("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
2568
2909
  return lines.join("\n");
2569
2910
  }
2570
2911
  broadcastToAll(message) {
@@ -2594,7 +2935,41 @@ var AgentOrchestrator = class {
2594
2935
  this.handleDisconnect(connection);
2595
2936
  }
2596
2937
  }
2938
+ async getCompletions(request) {
2939
+ return {
2940
+ type: "completions",
2941
+ completions: await this.completionsManager.getCompletion(request.query)
2942
+ };
2943
+ }
2597
2944
  };
2945
+ async function startAgentOrchestrator(dependencies) {
2946
+ let worktreesEnabled = dependencies?.worktreesEnabled ?? false;
2947
+ let worktreesBaseRef = dependencies?.worktreesBaseRef;
2948
+ if (dependencies?.worktreesEnabled === void 0) {
2949
+ try {
2950
+ const config = await readOmnaraConfig();
2951
+ worktreesEnabled = isWorktreesEnabled(config);
2952
+ worktreesBaseRef = config.worktrees.base_ref;
2953
+ if (worktreesEnabled) {
2954
+ logger.info("Worktrees enabled via omnara.jsonc configuration");
2955
+ } else {
2956
+ logger.info("Worktrees disabled (no worktrees config found in omnara.jsonc)");
2957
+ }
2958
+ } catch (err) {
2959
+ logger.error("Failed to read omnara.jsonc configuration:", err);
2960
+ logger.info("Worktrees disabled due to config read error");
2961
+ worktreesEnabled = false;
2962
+ worktreesBaseRef = void 0;
2963
+ }
2964
+ }
2965
+ const server = new AgentOrchestrator({
2966
+ ...dependencies,
2967
+ worktreesEnabled,
2968
+ worktreesBaseRef
2969
+ });
2970
+ await server.start();
2971
+ return server;
2972
+ }
2598
2973
 
2599
2974
  // src/relay/relay-bridge.ts
2600
2975
  import { randomUUID as randomUUID4 } from "crypto";
@@ -2616,8 +2991,8 @@ var ApiClient = class {
2616
2991
  constructor(options) {
2617
2992
  this.baseUrl = normalizeBaseUrl(options.baseUrl);
2618
2993
  }
2619
- async getJson(path7, options) {
2620
- const url = resolveUrl(this.baseUrl, path7);
2994
+ async getJson(path8, options) {
2995
+ const url = resolveUrl(this.baseUrl, path8);
2621
2996
  const headers = this.buildHeaders(options);
2622
2997
  const response = await fetch(url, {
2623
2998
  method: "GET",
@@ -2626,8 +3001,8 @@ var ApiClient = class {
2626
3001
  });
2627
3002
  return await this.handleResponse(response);
2628
3003
  }
2629
- async postJson(path7, body, options) {
2630
- const url = resolveUrl(this.baseUrl, path7);
3004
+ async postJson(path8, body, options) {
3005
+ const url = resolveUrl(this.baseUrl, path8);
2631
3006
  const headers = this.buildHeaders(options, { includeJson: true });
2632
3007
  const response = await fetch(url, {
2633
3008
  method: "POST",
@@ -2685,12 +3060,12 @@ function normalizeBaseUrl(raw) {
2685
3060
  }
2686
3061
  return raw.endsWith("/") ? raw : `${raw}/`;
2687
3062
  }
2688
- function resolveUrl(base, path7) {
3063
+ function resolveUrl(base, path8) {
2689
3064
  const baseUrl = new URL(base);
2690
- if (!path7.startsWith("/")) {
2691
- path7 = `/${path7}`;
3065
+ if (!path8.startsWith("/")) {
3066
+ path8 = `/${path8}`;
2692
3067
  }
2693
- const resolved = new URL(path7, baseUrl);
3068
+ const resolved = new URL(path8, baseUrl);
2694
3069
  return resolved.toString();
2695
3070
  }
2696
3071
  function extractDetail(payload) {
@@ -3433,6 +3808,166 @@ var control_message_schema_default = {
3433
3808
  title: "AgentWorkStatus",
3434
3809
  type: "string"
3435
3810
  },
3811
+ CancelAgentMessage: {
3812
+ additionalProperties: false,
3813
+ description: "Request to cancel a running agent generation.",
3814
+ properties: {
3815
+ type: {
3816
+ const: "cancel_agent",
3817
+ default: "cancel_agent",
3818
+ title: "Type",
3819
+ type: "string"
3820
+ },
3821
+ target_session_id: {
3822
+ format: "uuid",
3823
+ title: "Target Session Id",
3824
+ type: "string"
3825
+ },
3826
+ reason: {
3827
+ anyOf: [
3828
+ {
3829
+ type: "string"
3830
+ },
3831
+ {
3832
+ type: "null"
3833
+ }
3834
+ ],
3835
+ default: null,
3836
+ title: "Reason"
3837
+ }
3838
+ },
3839
+ required: [
3840
+ "target_session_id"
3841
+ ],
3842
+ title: "CancelAgentMessage",
3843
+ type: "object"
3844
+ },
3845
+ CompletionItem: {
3846
+ description: "A single completion item with type and result",
3847
+ properties: {
3848
+ type: {
3849
+ enum: [
3850
+ "slash_command",
3851
+ "file",
3852
+ "agent"
3853
+ ],
3854
+ title: "Type",
3855
+ type: "string"
3856
+ },
3857
+ result: {
3858
+ title: "Result",
3859
+ type: "string"
3860
+ }
3861
+ },
3862
+ required: [
3863
+ "type",
3864
+ "result"
3865
+ ],
3866
+ title: "CompletionItem",
3867
+ type: "object"
3868
+ },
3869
+ CompletionsRequest: {
3870
+ description: "Query for autocomplete options",
3871
+ properties: {
3872
+ type: {
3873
+ const: "completions",
3874
+ default: "completions",
3875
+ title: "Type",
3876
+ type: "string"
3877
+ },
3878
+ query: {
3879
+ title: "Query",
3880
+ type: "string"
3881
+ }
3882
+ },
3883
+ required: [
3884
+ "query"
3885
+ ],
3886
+ title: "CompletionsRequest",
3887
+ type: "object"
3888
+ },
3889
+ CompletionsResponse: {
3890
+ description: "List of autocomplete options (i.e. files or /commands)",
3891
+ properties: {
3892
+ type: {
3893
+ const: "completions",
3894
+ default: "completions",
3895
+ title: "Type",
3896
+ type: "string"
3897
+ },
3898
+ completions: {
3899
+ items: {
3900
+ $ref: "#/$defs/CompletionItem"
3901
+ },
3902
+ title: "Completions",
3903
+ type: "array"
3904
+ }
3905
+ },
3906
+ required: [
3907
+ "completions"
3908
+ ],
3909
+ title: "CompletionsResponse",
3910
+ type: "object"
3911
+ },
3912
+ ErrorResponse: {
3913
+ properties: {
3914
+ type: {
3915
+ const: "error",
3916
+ default: "error",
3917
+ title: "Type",
3918
+ type: "string"
3919
+ },
3920
+ error: {
3921
+ title: "Error",
3922
+ type: "string"
3923
+ }
3924
+ },
3925
+ required: [
3926
+ "error"
3927
+ ],
3928
+ title: "ErrorResponse",
3929
+ type: "object"
3930
+ },
3931
+ GitDiffRequest: {
3932
+ description: "Query the git diff on a branch",
3933
+ properties: {
3934
+ type: {
3935
+ const: "git_diff",
3936
+ default: "git_diff",
3937
+ title: "Type",
3938
+ type: "string"
3939
+ },
3940
+ branch: {
3941
+ title: "Branch",
3942
+ type: "string"
3943
+ }
3944
+ },
3945
+ required: [
3946
+ "branch"
3947
+ ],
3948
+ title: "GitDiffRequest",
3949
+ type: "object"
3950
+ },
3951
+ GitDiffResponse: {
3952
+ description: "Git diff response for a given branch\nTODO: add the actual definition",
3953
+ properties: {
3954
+ type: {
3955
+ const: "git_diff",
3956
+ default: "git_diff",
3957
+ title: "Type",
3958
+ type: "string"
3959
+ },
3960
+ diff: {
3961
+ title: "Diff",
3962
+ type: "null"
3963
+ }
3964
+ },
3965
+ required: [
3966
+ "diff"
3967
+ ],
3968
+ title: "GitDiffResponse",
3969
+ type: "object"
3970
+ },
3436
3971
  Heartbeat: {
3437
3972
  additionalProperties: false,
3438
3973
  description: "Ping/pong heartbeat frames.",
@@ -3469,6 +4004,77 @@ var control_message_schema_default = {
3469
4004
  title: "Heartbeat",
3470
4005
  type: "object"
3471
4006
  },
4007
+ RelayRequestMessage: {
4008
+ additionalProperties: false,
4009
+ description: "RPC message for generic requests",
4010
+ properties: {
4011
+ type: {
4012
+ const: "request",
4013
+ default: "request",
4014
+ title: "Type",
4015
+ type: "string"
4016
+ },
4017
+ request_id: {
4018
+ format: "uuid",
4019
+ title: "Request Id",
4020
+ type: "string"
4021
+ },
4022
+ request: {
4023
+ anyOf: [
4024
+ {
4025
+ $ref: "#/$defs/CompletionsRequest"
4026
+ },
4027
+ {
4028
+ $ref: "#/$defs/GitDiffRequest"
4029
+ }
4030
+ ],
4031
+ title: "Request"
4032
+ }
4033
+ },
4034
+ required: [
4035
+ "request_id",
4036
+ "request"
4037
+ ],
4038
+ title: "RelayRequestMessage",
4039
+ type: "object"
4040
+ },
4041
+ RelayResponseMessage: {
4042
+ additionalProperties: false,
4043
+ description: "RPC message for responses",
4044
+ properties: {
4045
+ type: {
4046
+ const: "response",
4047
+ default: "response",
4048
+ title: "Type",
4049
+ type: "string"
4050
+ },
4051
+ request_id: {
4052
+ format: "uuid",
4053
+ title: "Request Id",
4054
+ type: "string"
4055
+ },
4056
+ response: {
4057
+ anyOf: [
4058
+ {
4059
+ $ref: "#/$defs/ErrorResponse"
4060
+ },
4061
+ {
4062
+ $ref: "#/$defs/CompletionsResponse"
4063
+ },
4064
+ {
4065
+ $ref: "#/$defs/GitDiffResponse"
4066
+ }
4067
+ ],
4068
+ title: "Response"
4069
+ }
4070
+ },
4071
+ required: [
4072
+ "request_id",
4073
+ "response"
4074
+ ],
4075
+ title: "RelayResponseMessage",
4076
+ type: "object"
4077
+ },
3472
4078
  SessionAnnounce: {
3473
4079
  additionalProperties: false,
3474
4080
  description: "Notification that a session was created or updated.",
@@ -3769,6 +4375,15 @@ var control_message_schema_default = {
3769
4375
  },
3770
4376
  {
3771
4377
  $ref: "#/$defs/Heartbeat"
4378
+ },
4379
+ {
4380
+ $ref: "#/$defs/CancelAgentMessage"
4381
+ },
4382
+ {
4383
+ $ref: "#/$defs/RelayRequestMessage"
4384
+ },
4385
+ {
4386
+ $ref: "#/$defs/RelayResponseMessage"
3772
4387
  }
3773
4388
  ]
3774
4389
  };
@@ -5698,6 +6313,7 @@ var RelayBridge = class extends EventEmitter2 {
5698
6313
  userSessionId = null;
5699
6314
  currentAgentSessionId = null;
5700
6315
  browserOpened = false;
6316
+ canceled = false;
5701
6317
  constructor(orchestrator, options) {
5702
6318
  super();
5703
6319
  this.orchestrator = orchestrator;
@@ -5711,7 +6327,7 @@ var RelayBridge = class extends EventEmitter2 {
5711
6327
  throw new Error("Relay bridge already started");
5712
6328
  }
5713
6329
  this.connectionHandle = this.orchestrator.addClient({
5714
- canSend: () => this.ws?.readyState === WebSocket.OPEN,
6330
+ canSend: () => !this.canceled && this.ws?.readyState === WebSocket.OPEN,
5715
6331
  send: (message) => {
5716
6332
  this.handleOutbound(message);
5717
6333
  }
@@ -5742,6 +6358,7 @@ var RelayBridge = class extends EventEmitter2 {
5742
6358
  this.stopped = true;
5743
6359
  this.currentAgentSessionId = null;
5744
6360
  this.browserOpened = false;
6361
+ this.canceled = false;
5745
6362
  if (this.pingTimer) {
5746
6363
  clearInterval(this.pingTimer);
5747
6364
  this.pingTimer = null;
@@ -5976,9 +6593,7 @@ var RelayBridge = class extends EventEmitter2 {
5976
6593
  this.handleSessionAnnounce(control);
5977
6594
  break;
5978
6595
  case "session_closed":
5979
- this.logger.info(
5980
- `Session closed: ${control.session_id} -> status ${control.status}`
5981
- );
6596
+ this.handleSessionClosed(control);
5982
6597
  break;
5983
6598
  case "heartbeat":
5984
6599
  if (control.mode === "ping") {
@@ -5990,6 +6605,12 @@ var RelayBridge = class extends EventEmitter2 {
5990
6605
  this.sendFrame(response);
5991
6606
  }
5992
6607
  break;
6608
+ case "cancel_agent":
6609
+ this.handleCancelAgent(control);
6610
+ break;
6611
+ case "request":
6612
+ this.handleRelayRequest(control);
6613
+ break;
5993
6614
  default:
5994
6615
  this.logger.debug(`Ignoring control message ${type ?? "unknown"}`);
5995
6616
  }
@@ -6056,6 +6677,68 @@ var RelayBridge = class extends EventEmitter2 {
6056
6677
  this.orchestrator.ensureSessionExists(control.session.session_id);
6057
6678
  }
6058
6679
  }
6680
+ async handleCancelAgent(control) {
6681
+ this.logger.info(
6682
+ `Received cancel request for session ${control.target_session_id}${control.reason ? ` (reason: ${control.reason})` : ""}`
6683
+ );
6684
+ if (control.target_session_id === this.currentAgentSessionId) {
6685
+ this.logger.info("Cancel request for main orchestrator session");
6686
+ await this.orchestrator.cancel();
6687
+ } else {
6688
+ const success = this.orchestrator.getWorkerManager().cancelWorker(control.target_session_id);
6689
+ if (success) {
6690
+ this.logger.info(`Worker cancellation succeeded for ${control.target_session_id}`);
6691
+ } else {
6692
+ this.logger.warn(
6693
+ `Worker cancellation failed for ${control.target_session_id} (worker not found or already completed)`
6694
+ );
6695
+ }
6696
+ }
6697
+ }
6698
+ handleSessionClosed(control) {
6699
+ const closedSessionId = control.session_id;
6700
+ this.logger.info(`Session closed: ${closedSessionId} -> status ${control.status}`);
6701
+ if (closedSessionId === this.currentAgentSessionId) {
6702
+ this.logger.info(
6703
+ `Main orchestrator session ${closedSessionId} was canceled. Stopping message sending.`
6704
+ );
6705
+ this.canceled = true;
6706
+ }
6707
+ }
6708
+ async handleRelayRequest(control) {
6709
+ const requestId = control.request_id;
6710
+ const request = control.request;
6711
+ let response;
6712
+ switch (request.type) {
6713
+ case "completions":
6714
+ response = await this.orchestrator.getCompletions(
6715
+ request
6716
+ );
6717
+ break;
6718
+ case "git_diff":
6719
+ this.logger.warn("Git diff not implemented");
6720
+ response = {
6721
+ type: "git_diff",
6722
+ diff: null
6723
+ };
6724
+ break;
6725
+ default:
6726
+ response = {
6727
+ type: "error",
6728
+ error: `Request failed: Unknown relay request type ${request.type}`
6729
+ };
6730
+ }
6731
+ const socket = this.ws;
6732
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
6733
+ return;
6734
+ }
6735
+ const frame = {
6736
+ type: "response",
6737
+ request_id: requestId,
6738
+ response
6739
+ };
6740
+ this.sendFrame(frame, socket);
6741
+ }
6059
6742
  handleMessageEnvelope(envelope) {
6060
6743
  const content = envelope.payload.content;
6061
6744
  const isUserMessage = envelope.sender.kind === "user";
@@ -6312,16 +6995,16 @@ var RelayBridge = class extends EventEmitter2 {
6312
6995
  }
6313
6996
  }
6314
6997
  };
6315
- function buildRelayWebsocketUrl(baseUrl, path7) {
6998
+ function buildRelayWebsocketUrl(baseUrl, path8) {
6316
6999
  const url = new URL(baseUrl);
6317
7000
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
6318
7001
  url.search = "";
6319
7002
  url.hash = "";
6320
- if (path7.startsWith("/")) {
6321
- url.pathname = path7;
7003
+ if (path8.startsWith("/")) {
7004
+ url.pathname = path8;
6322
7005
  } else {
6323
7006
  const basePath = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
6324
- url.pathname = `${basePath}${path7}`;
7007
+ url.pathname = `${basePath}${path8}`;
6325
7008
  }
6326
7009
  return url.toString();
6327
7010
  }
@@ -6664,7 +7347,7 @@ Commands:
6664
7347
 
6665
7348
  // src/voice/voice-service.ts
6666
7349
  async function launchVoiceForSession(client, accessToken, options) {
6667
- const path7 = `/api/v1/voice/sessions/${options.sessionId}/launch`;
7350
+ const path8 = `/api/v1/voice/sessions/${options.sessionId}/launch`;
6668
7351
  const requestBody = {};
6669
7352
  if (options.force) {
6670
7353
  requestBody.force = true;
@@ -6673,7 +7356,7 @@ async function launchVoiceForSession(client, accessToken, options) {
6673
7356
  requestBody.room_name = options.roomName;
6674
7357
  }
6675
7358
  const response = await client.postJson(
6676
- path7,
7359
+ path8,
6677
7360
  requestBody,
6678
7361
  { accessToken }
6679
7362
  );
@@ -6769,8 +7452,7 @@ async function runAgentOrchestrator(options = {}) {
6769
7452
  } else {
6770
7453
  logger.info("\u2139 Running without Claude Code integration");
6771
7454
  }
6772
- const orchestrator = new AgentOrchestrator();
6773
- await orchestrator.start();
7455
+ const orchestrator = await startAgentOrchestrator();
6774
7456
  const backendUrl = resolveBackendUrl(options.backendUrl);
6775
7457
  const frontendUrl = resolveFrontendUrl(options.frontendUrl);
6776
7458
  const store = new CredentialsStore();
@@ -6875,16 +7557,16 @@ async function runVoice(options) {
6875
7557
  logger.debug(launch.relayToken);
6876
7558
  logger.important("\nKeep this information secure\u2014tokens grant access to your agent session.");
6877
7559
  }
6878
- function buildRelayWebsocketUrl2(baseUrl, path7) {
7560
+ function buildRelayWebsocketUrl2(baseUrl, path8) {
6879
7561
  const url = new URL(baseUrl);
6880
7562
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
6881
7563
  url.search = "";
6882
7564
  url.hash = "";
6883
- if (path7.startsWith("/")) {
6884
- url.pathname = path7;
7565
+ if (path8.startsWith("/")) {
7566
+ url.pathname = path8;
6885
7567
  } else {
6886
7568
  const basePath = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
6887
- url.pathname = `${basePath}${path7}`;
7569
+ url.pathname = `${basePath}${path8}`;
6888
7570
  }
6889
7571
  return url.toString();
6890
7572
  }
@@ -7057,9 +7739,9 @@ function isMainModule() {
7057
7739
  if (!import.meta.url || !process.argv[1]) {
7058
7740
  return false;
7059
7741
  }
7060
- const executedPath = path6.resolve(process.argv[1]);
7742
+ const executedPath = path7.resolve(process.argv[1]);
7061
7743
  const currentPath = import.meta.url.startsWith("file://") ? decodeURIComponent(import.meta.url.slice(7)) : import.meta.url;
7062
- return path6.normalize(currentPath) === path6.normalize(executedPath);
7744
+ return path7.normalize(currentPath) === path7.normalize(executedPath);
7063
7745
  }
7064
7746
  if (isMainModule()) {
7065
7747
  run(process.argv).catch((error) => {