@agentrix/cli 0.0.12 → 0.0.13

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.
@@ -6,8 +6,7 @@ var chalk = require('chalk');
6
6
  var shared = require('@agentrix/shared');
7
7
  var node_crypto = require('node:crypto');
8
8
  var axios = require('axios');
9
- var _package = require('./logger---ZD5a2u.cjs');
10
- var fs$1 = require('node:fs');
9
+ var _package = require('./logger-DzYRcKN1.cjs');
11
10
  var node_readline = require('node:readline');
12
11
  var fs = require('fs');
13
12
  var path = require('path');
@@ -15,10 +14,13 @@ var os = require('node:os');
15
14
  var open = require('open');
16
15
  var socket_ioClient = require('socket.io-client');
17
16
  var node_events = require('node:events');
17
+ var fs$1 = require('node:fs');
18
18
  var path$1 = require('node:path');
19
19
  var child_process = require('child_process');
20
20
  var psList = require('ps-list');
21
21
  var spawn = require('cross-spawn');
22
+ var sandboxRuntime = require('@xmz-ai/sandbox-runtime');
23
+ var platform_js = require('@xmz-ai/sandbox-runtime/dist/utils/platform.js');
22
24
  var fastify = require('fastify');
23
25
  var zod = require('zod');
24
26
  var fastifyTypeProviderZod = require('fastify-type-provider-zod');
@@ -64,22 +66,12 @@ async function daemonPost(path, body) {
64
66
  error: errorMessage
65
67
  };
66
68
  }
67
- try {
68
- process.kill(state.pid, 0);
69
- } catch (error) {
70
- const errorMessage = "Daemon is not running, file is stale";
71
- _package.logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
72
- return {
73
- error: errorMessage
74
- };
75
- }
76
69
  try {
77
70
  const timeout = process.env.AGENTRIX_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.AGENTRIX_DAEMON_HTTP_TIMEOUT) : 1e4;
78
- const response = await fetch(`http://127.0.0.1:${state.port}${path}`, {
71
+ const response = await fetch(`http://agentrix-local.xmz.ai:${state.port}${path}`, {
79
72
  method: "POST",
80
73
  headers: { "Content-Type": "application/json" },
81
74
  body: JSON.stringify(body || {}),
82
- // Mostly increased for stress test
83
75
  signal: AbortSignal.timeout(timeout)
84
76
  });
85
77
  if (!response.ok) {
@@ -225,11 +217,8 @@ async function handleAuthLogout() {
225
217
  console.log(chalk.gray("Stopped daemon"));
226
218
  } catch {
227
219
  }
228
- const paths = _package.machine.getStatePaths();
229
- if (fs$1.existsSync(paths.rootDir)) {
230
- fs$1.rmSync(paths.rootDir, { recursive: true, force: true });
231
- console.log(chalk.gray(`Removed agentrix home directory`));
232
- }
220
+ await _package.machine.clearCredentials();
221
+ console.log(chalk.gray(`Removed credentials`));
233
222
  console.log(chalk.green("\u2713 Successfully logged out"));
234
223
  } catch (error) {
235
224
  throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -11885,7 +11874,7 @@ async function findAllAgentrixProcesses() {
11885
11874
  async function findRunawayAgentrixProcesses() {
11886
11875
  const allProcesses = await findAllAgentrixProcesses();
11887
11876
  return allProcesses.filter(
11888
- (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "worker")
11877
+ (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "worker" || p.type === "upgrade-daemon")
11889
11878
  ).map((p) => ({ pid: p.pid, command: p.command }));
11890
11879
  }
11891
11880
  async function killRunawayAgentrixProcesses() {
@@ -11987,6 +11976,27 @@ async function runDoctorCommand(filter) {
11987
11976
  } catch (error) {
11988
11977
  console.log(chalk.red("\u274C Error reading credentials"));
11989
11978
  }
11979
+ console.log(chalk.bold("\n\u{1F512} Sandbox Dependencies"));
11980
+ const platform = platform_js.getPlatform();
11981
+ if (sandboxRuntime.isSupportedPlatform(platform)) {
11982
+ console.log(`Platform: ${chalk.green(platform)} (supported)`);
11983
+ const depsOk = sandboxRuntime.checkSandboxDependencies();
11984
+ if (depsOk) {
11985
+ console.log(chalk.green("\u2713 All sandbox dependencies available"));
11986
+ } else {
11987
+ console.log(chalk.yellow("\u26A0\uFE0F Some sandbox dependencies missing"));
11988
+ if (platform === "linux") {
11989
+ console.log(chalk.gray(" Required: bubblewrap, socat"));
11990
+ console.log(chalk.gray(" Install: sudo apt install bubblewrap socat"));
11991
+ } else if (platform === "macos") {
11992
+ console.log(chalk.gray(" Required: ripgrep"));
11993
+ console.log(chalk.gray(" Install: brew install ripgrep"));
11994
+ }
11995
+ }
11996
+ } else {
11997
+ console.log(`Platform: ${chalk.yellow(platform)} (not supported)`);
11998
+ console.log(chalk.gray(" \u26A0\uFE0F Sandbox will be disabled"));
11999
+ }
11990
12000
  }
11991
12001
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
11992
12002
  try {
@@ -12211,9 +12221,11 @@ function createPromiseWithTimeout(options) {
12211
12221
  class TaskWorkerManager {
12212
12222
  pidToTrackedSession;
12213
12223
  pidToAwaiter;
12214
- constructor() {
12224
+ sandboxPool;
12225
+ constructor(sandboxPool) {
12215
12226
  this.pidToTrackedSession = /* @__PURE__ */ new Map();
12216
12227
  this.pidToAwaiter = /* @__PURE__ */ new Map();
12228
+ this.sandboxPool = sandboxPool || null;
12217
12229
  }
12218
12230
  getCurrentSessions() {
12219
12231
  return Array.from(this.pidToTrackedSession.values());
@@ -12254,9 +12266,15 @@ class TaskWorkerManager {
12254
12266
  this.pidToTrackedSession.set(workerProcess.pid, tracked);
12255
12267
  workerProcess.on("exit", (code, signal) => {
12256
12268
  this.pidToTrackedSession.delete(workerProcess.pid);
12269
+ if (this.sandboxPool) {
12270
+ this.sandboxPool.disposeWorkerSandbox(data.taskId);
12271
+ }
12257
12272
  });
12258
12273
  workerProcess.on("error", (error) => {
12259
12274
  this.pidToTrackedSession.delete(workerProcess.pid);
12275
+ if (this.sandboxPool) {
12276
+ this.sandboxPool.disposeWorkerSandbox(data.taskId);
12277
+ }
12260
12278
  });
12261
12279
  }
12262
12280
  async startWorker(options) {
@@ -12281,14 +12299,53 @@ class TaskWorkerManager {
12281
12299
  "--idle-timeout",
12282
12300
  "120"
12283
12301
  ];
12284
- const workerProcess = spawnAgentrixCLI(args, {
12285
- cwd,
12286
- detached: true,
12287
- stdio: ["ignore", "pipe", "pipe"],
12288
- env: {
12289
- ...process.env
12302
+ let workerProcess;
12303
+ if (this.sandboxPool?.isEnabled()) {
12304
+ try {
12305
+ const sandbox = await this.sandboxPool.createWorkerSandbox(
12306
+ options.taskId,
12307
+ options.userId,
12308
+ cwd
12309
+ );
12310
+ if (!sandbox) {
12311
+ throw new Error("Failed to create sandbox instance");
12312
+ }
12313
+ const { projectPath } = await Promise.resolve().then(function () { return require('./logger-DzYRcKN1.cjs'); }).then(function (n) { return n.machine$1; });
12314
+ const { join } = await import('path');
12315
+ const entrypoint = join(projectPath(), "dist", "index.mjs");
12316
+ const nodeArgs = ["--no-warnings", "--no-deprecation", entrypoint, ...args];
12317
+ const originalCommand = `"${process.execPath}" ${nodeArgs.map((a) => `"${a}"`).join(" ")}`;
12318
+ const sandboxedCommand = await this.sandboxPool.wrapWorkerCommand(
12319
+ options.taskId,
12320
+ originalCommand
12321
+ );
12322
+ _package.logger.debug(`[SESSION] Sandboxed command for task ${options.taskId}: ${sandboxedCommand}`);
12323
+ workerProcess = child_process.spawn(sandboxedCommand, {
12324
+ shell: true,
12325
+ cwd,
12326
+ detached: true,
12327
+ stdio: ["ignore", "pipe", "pipe"],
12328
+ env: {
12329
+ ...process.env
12330
+ // Environment variables controlled by SandboxManager
12331
+ }
12332
+ });
12333
+ _package.logger.info(`[SESSION] Worker started with sandbox, PID: ${workerProcess.pid}`);
12334
+ } catch (error) {
12335
+ _package.logger.error(`[SESSION] Failed to setup sandbox for task ${options.taskId}:`, error);
12336
+ ack.status = "failed";
12337
+ ack.message = `Sandbox setup failed: ${error instanceof Error ? error.message : String(error)}`;
12338
+ return ack;
12290
12339
  }
12291
- });
12340
+ } else {
12341
+ workerProcess = spawnAgentrixCLI(args, {
12342
+ cwd,
12343
+ detached: true,
12344
+ stdio: ["ignore", "pipe", "pipe"],
12345
+ env: { ...process.env }
12346
+ });
12347
+ _package.logger.info(`[SESSION] Worker started without sandbox, PID: ${workerProcess.pid}`);
12348
+ }
12292
12349
  if (process.env.DEBUG) {
12293
12350
  workerProcess.stdout?.on("data", (data) => {
12294
12351
  _package.logger.debug(`[Daemon] worker stdout: ${data.toString()}`);
@@ -12407,6 +12464,114 @@ function setupGracefulShutdown(options) {
12407
12464
  };
12408
12465
  }
12409
12466
 
12467
+ class SandboxPool {
12468
+ networkManager = null;
12469
+ workerSandboxes = /* @__PURE__ */ new Map();
12470
+ settings = null;
12471
+ platform;
12472
+ constructor() {
12473
+ this.platform = platform_js.getPlatform();
12474
+ }
12475
+ async initialize(settings) {
12476
+ this.settings = settings;
12477
+ if (!settings.enabled) {
12478
+ _package.logger.info("[SANDBOX] Sandbox disabled via settings");
12479
+ return false;
12480
+ }
12481
+ if (!sandboxRuntime.isSupportedPlatform(this.platform)) {
12482
+ _package.logger.warn("[SANDBOX] Platform not supported, sandbox disabled");
12483
+ return false;
12484
+ }
12485
+ try {
12486
+ const apiHost = new URL(_package.machine.serverUrl).hostname;
12487
+ const networkConfig = {
12488
+ allowedDomains: [
12489
+ apiHost,
12490
+ ...settings.network.allowedDomains
12491
+ ],
12492
+ deniedDomains: settings.network.deniedDomains,
12493
+ allowLocalBinding: false
12494
+ };
12495
+ this.networkManager = new sandboxRuntime.NetworkManager();
12496
+ await this.networkManager.initialize(networkConfig);
12497
+ _package.logger.info("[SANDBOX] Sandbox pool initialized successfully");
12498
+ return true;
12499
+ } catch (error) {
12500
+ _package.logger.error("[SANDBOX] Failed to initialize:", error);
12501
+ throw error;
12502
+ }
12503
+ }
12504
+ async createWorkerSandbox(taskId, userId, workingDirectory) {
12505
+ if (!this.networkManager || !this.settings?.enabled) {
12506
+ return null;
12507
+ }
12508
+ try {
12509
+ const taskDir = _package.machine.resolveTaskDir(userId, taskId);
12510
+ const logsDir = _package.machine.getStatePaths().logsDir;
12511
+ const baseFilesystem = this.settings.filesystem || {};
12512
+ const baseEnv = this.settings.env || {};
12513
+ const filesystemConfig = {
12514
+ ...baseFilesystem,
12515
+ allowWrite: [
12516
+ ...baseFilesystem.allowWrite || [],
12517
+ taskDir,
12518
+ workingDirectory,
12519
+ logsDir
12520
+ ]
12521
+ };
12522
+ if (this.platform === "linux" && baseFilesystem.allowRead) {
12523
+ filesystemConfig.allowRead = [
12524
+ ...baseFilesystem.allowRead
12525
+ ];
12526
+ }
12527
+ const instanceConfig = {
12528
+ filesystem: filesystemConfig,
12529
+ env: baseEnv
12530
+ };
12531
+ const sandbox = new sandboxRuntime.SandboxManager(this.networkManager, instanceConfig);
12532
+ this.workerSandboxes.set(taskId, sandbox);
12533
+ _package.logger.info(`[SANDBOX] Created sandbox for task ${taskId}`);
12534
+ return sandbox;
12535
+ } catch (error) {
12536
+ _package.logger.error(`[SANDBOX] Failed to create sandbox for task ${taskId}:`, error);
12537
+ return null;
12538
+ }
12539
+ }
12540
+ async wrapWorkerCommand(taskId, command) {
12541
+ const sandbox = this.workerSandboxes.get(taskId);
12542
+ if (!sandbox) {
12543
+ throw new Error(`No sandbox found for task ${taskId}`);
12544
+ }
12545
+ const wrapped = await sandbox.wrapWithSandbox(command);
12546
+ _package.logger.debug(`[SANDBOX] Wrapped command for task ${taskId}`);
12547
+ return wrapped;
12548
+ }
12549
+ disposeWorkerSandbox(taskId) {
12550
+ const sandbox = this.workerSandboxes.get(taskId);
12551
+ if (sandbox) {
12552
+ sandbox.dispose();
12553
+ this.workerSandboxes.delete(taskId);
12554
+ _package.logger.debug(`[SANDBOX] Disposed sandbox for task ${taskId}`);
12555
+ }
12556
+ }
12557
+ async shutdown() {
12558
+ _package.logger.info("[SANDBOX] Shutting down sandbox pool");
12559
+ for (const [taskId, sandbox] of this.workerSandboxes.entries()) {
12560
+ sandbox.dispose();
12561
+ _package.logger.debug(`[SANDBOX] Disposed sandbox for task ${taskId}`);
12562
+ }
12563
+ this.workerSandboxes.clear();
12564
+ if (this.networkManager) {
12565
+ await this.networkManager.shutdown();
12566
+ this.networkManager = null;
12567
+ _package.logger.info("[SANDBOX] Network manager shutdown complete");
12568
+ }
12569
+ }
12570
+ isEnabled() {
12571
+ return this.settings?.enabled === true;
12572
+ }
12573
+ }
12574
+
12410
12575
  async function startDaemon() {
12411
12576
  Object.assign(_package.logger, _package.createLogger({ type: "daemon" }));
12412
12577
  const { requestShutdown, shutdownPromise } = setupGracefulShutdown({
@@ -12435,7 +12600,9 @@ async function startDaemon() {
12435
12600
  }
12436
12601
  const credentials = await authAndSetupMachineIfNeeded();
12437
12602
  _package.logger.debug("[DAEMON RUN] Auth and machine setup complete");
12438
- const sessionManager = new TaskWorkerManager();
12603
+ const sandboxPool = new SandboxPool();
12604
+ await sandboxPool.initialize(_package.machine.getSandboxSettings());
12605
+ const sessionManager = new TaskWorkerManager(sandboxPool);
12439
12606
  const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
12440
12607
  getChildren: () => sessionManager.getCurrentSessions(),
12441
12608
  stopSession: (id) => sessionManager.stopSession(id),
@@ -12483,6 +12650,7 @@ async function startDaemon() {
12483
12650
  const cleanupAndShutdown = async (source, errorMessage) => {
12484
12651
  await machineClient.disconnect();
12485
12652
  await stopControlServer();
12653
+ await sandboxPool.shutdown();
12486
12654
  await cleanupDaemonState();
12487
12655
  await stopCaffeinate();
12488
12656
  await _package.machine.releaseDaemonLock(daemonLockHandle);
@@ -12599,14 +12767,6 @@ function createWorkerEventHandlers(context) {
12599
12767
  }
12600
12768
  }
12601
12769
  }
12602
- },
12603
- "require-permission-response": async (data) => {
12604
- if (data.taskId !== context.taskId) {
12605
- return;
12606
- }
12607
- if (context.onPermissionResponse) {
12608
- await context.onPermissionResponse(data);
12609
- }
12610
12770
  }
12611
12771
  };
12612
12772
  }
@@ -12647,7 +12807,6 @@ class WorkerClient {
12647
12807
  cwd: normalizedCwd,
12648
12808
  stopTask: options.stopTask,
12649
12809
  onTaskMessage: options.onTaskMessage,
12650
- onPermissionResponse: options.onPermissionResponse,
12651
12810
  onGitPush: options.onGitPush,
12652
12811
  dataEncryptionKey: config.dataEncryptionKey
12653
12812
  };
@@ -12676,26 +12835,32 @@ class WorkerClient {
12676
12835
  }
12677
12836
  this.client.disconnect();
12678
12837
  }
12679
- sendTaskMessage(message) {
12680
- const cwdWithSlash = this.context.cwd;
12681
- const cwdWithoutSlash = cwdWithSlash.slice(0, -1);
12682
- let content = JSON.stringify(message);
12683
- content = content.replaceAll(cwdWithSlash, "");
12684
- content = content.replaceAll(cwdWithoutSlash, ".");
12685
- let assistantMessage = JSON.parse(content);
12838
+ sendTaskMessage(message, options) {
12839
+ const { replaceCwd = true } = options || {};
12840
+ let processedMessage = message;
12841
+ if (replaceCwd) {
12842
+ const cwdWithSlash = this.context.cwd;
12843
+ const cwdWithoutSlash = cwdWithSlash.slice(0, -1);
12844
+ let content = JSON.stringify(message);
12845
+ content = content.replaceAll(cwdWithSlash, "");
12846
+ content = content.replaceAll(cwdWithoutSlash, ".");
12847
+ processedMessage = JSON.parse(content);
12848
+ }
12686
12849
  let encryptedMessage;
12687
12850
  if (this.context.dataEncryptionKey) {
12688
- encryptedMessage = shared.encryptSdkMessage(assistantMessage, this.context.dataEncryptionKey);
12689
- assistantMessage = void 0;
12851
+ encryptedMessage = shared.encryptSdkMessage(processedMessage, this.context.dataEncryptionKey);
12852
+ processedMessage = void 0;
12690
12853
  }
12854
+ const eventId = shared.createEventId();
12691
12855
  const payload = {
12692
- eventId: shared.createEventId(),
12856
+ eventId,
12693
12857
  taskId: this.context.taskId,
12694
12858
  from: "worker",
12695
- message: assistantMessage,
12859
+ message: processedMessage,
12696
12860
  encryptedMessage
12697
12861
  };
12698
12862
  this.client.send("task-message", payload);
12863
+ return eventId;
12699
12864
  }
12700
12865
  sendWorkerInitializing() {
12701
12866
  const workerInitializingEvent = {
@@ -12706,12 +12871,13 @@ class WorkerClient {
12706
12871
  };
12707
12872
  this.client.send("worker-initializing", workerInitializingEvent);
12708
12873
  }
12709
- sendWorkerReady() {
12874
+ sendWorkerReady(duration) {
12710
12875
  const workerReadyEvent = {
12711
12876
  eventId: shared.createEventId(),
12712
12877
  taskId: this.context.taskId,
12713
12878
  machineId: this.context.machineId,
12714
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
12879
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12880
+ ...duration !== void 0 && { duration }
12715
12881
  };
12716
12882
  this.client.send("worker-ready", workerReadyEvent);
12717
12883
  }
@@ -12769,17 +12935,13 @@ ${errorMessage}`,
12769
12935
  };
12770
12936
  this.sendTaskMessage(systemMessage);
12771
12937
  }
12772
- sendRequirePermission(toolName, toolInput) {
12773
- const eventId = shared.createEventId();
12774
- const permissionRequest = {
12775
- eventId,
12776
- taskId: this.context.taskId,
12777
- toolName,
12778
- toolInput
12779
- };
12780
- this.client.send("require-permission", permissionRequest);
12781
- _package.logger.info(`[AGENT] Permission requested for tool: ${toolName}`);
12782
- return eventId;
12938
+ /**
12939
+ * Send ask-user message to request user input
12940
+ * @param questions - Array of questions (1-4)
12941
+ * @returns eventId for tracking the request
12942
+ */
12943
+ sendAskUser(questions) {
12944
+ return this.sendTaskMessage({ type: "ask_user", questions }, { replaceCwd: false });
12783
12945
  }
12784
12946
  sendUpdateTaskAgentSessionId(agentSessionId) {
12785
12947
  const updateSessionEvent = {
@@ -12834,7 +12996,6 @@ ${errorMessage}`,
12834
12996
  this.client.onEvent("cancel-task", handlers["cancel-task"]);
12835
12997
  this.client.onEvent("stop-task", handlers["stop-task"]);
12836
12998
  this.client.onEvent("task-message", handlers["task-message"]);
12837
- this.client.onEvent("require-permission-response", handlers["require-permission-response"]);
12838
12999
  }
12839
13000
  }
12840
13001
 
@@ -13037,6 +13198,12 @@ async function hasUncommittedChanges(dir) {
13037
13198
  const status = await git.status();
13038
13199
  return !status.isClean();
13039
13200
  }
13201
+ async function gitStash(dir, message) {
13202
+ const git = simpleGit(dir);
13203
+ {
13204
+ await git.stash(["push"]);
13205
+ }
13206
+ }
13040
13207
  async function getCurrentCommitHash(dir) {
13041
13208
  const git = simpleGit(dir);
13042
13209
  const log = await git.log({ maxCount: 1 });
@@ -13126,6 +13293,30 @@ async function executeHook(hooks, hookName, input, logger) {
13126
13293
  }
13127
13294
  }
13128
13295
 
13296
+ async function checkUncommittedChanges(workingDirectory) {
13297
+ const isRepo = await isGitRepository(workingDirectory);
13298
+ if (!isRepo) {
13299
+ return false;
13300
+ }
13301
+ return await hasUncommittedChanges(workingDirectory);
13302
+ }
13303
+ async function handleUncommittedChanges(workingDirectory, action) {
13304
+ switch (action) {
13305
+ case "Ignore":
13306
+ console.log("[GIT] User chose to ignore uncommitted changes");
13307
+ break;
13308
+ case "Commit":
13309
+ console.log("[GIT] Auto-committing uncommitted changes");
13310
+ await autoCommit(workingDirectory, "WIP: Auto-commit before task");
13311
+ break;
13312
+ case "Stash":
13313
+ console.log("[GIT] Stashing uncommitted changes");
13314
+ await gitStash(workingDirectory);
13315
+ break;
13316
+ case "Abort":
13317
+ throw new Error("Task aborted by user due to uncommitted changes");
13318
+ }
13319
+ }
13129
13320
  function createTaskBranchName(taskId) {
13130
13321
  return `agentrix/${taskId}`;
13131
13322
  }
@@ -13165,17 +13356,6 @@ async function setupLocalWorkspace(workingDirectory, taskId, hooks) {
13165
13356
  const isRepo = await isGitRepository(workingDirectory);
13166
13357
  const isEmpty = isDirectoryEmpty(workingDirectory);
13167
13358
  if (isRepo) {
13168
- const hasChanges = await hasUncommittedChanges(workingDirectory);
13169
- if (hasChanges) {
13170
- throw new Error(
13171
- `Directory ${workingDirectory} has uncommitted changes.
13172
-
13173
- Please commit or stash your changes before starting:
13174
- git add . && git commit -m "WIP"
13175
- or:
13176
- git stash`
13177
- );
13178
- }
13179
13359
  const hasCommits = await hasAnyCommits(workingDirectory);
13180
13360
  if (!hasCommits) {
13181
13361
  console.log("[GIT] Repository has no commits, creating initial commit");
@@ -13303,6 +13483,29 @@ async function markCommitAsSent(userId, taskId, commitHash) {
13303
13483
  await _package.machine.writeLastSentCommitHash(userId, taskId, commitHash);
13304
13484
  }
13305
13485
 
13486
+ function getDefaultPRPrompt(params) {
13487
+ return `All changes have been pushed to branch "${params.branchName}".
13488
+
13489
+ Commit range: ${params.initialCommitHash}..${params.currentCommitHash}
13490
+
13491
+ Based on our conversation context, create a Pull Request:
13492
+ - Title: conventional commits format (feat/fix/docs/refactor/test/chore: description)
13493
+ - Description: what changed, why, and any important decisions
13494
+
13495
+ Use mcp__agentrix__create_pr tool to create the PR.`;
13496
+ }
13497
+ function applyTemplateVariables(template, params) {
13498
+ return template.replace(/\{\{initialCommitHash\}\}/g, params.initialCommitHash).replace(/\{\{currentCommitHash\}\}/g, params.currentCommitHash).replace(/\{\{branchName\}\}/g, params.branchName);
13499
+ }
13500
+ function buildPRPrompt(params, config) {
13501
+ const defaultPrompt = getDefaultPRPrompt(params);
13502
+ if (!config?.customTemplate) {
13503
+ return defaultPrompt;
13504
+ }
13505
+ const customPrompt = applyTemplateVariables(config.customTemplate, params);
13506
+ return config.mode === "replace" ? customPrompt : defaultPrompt + "\n\n" + customPrompt;
13507
+ }
13508
+
13306
13509
  function executeCommandStreaming(command, cwd, callbacks, timeoutMs = 6e4) {
13307
13510
  return new Promise((resolve) => {
13308
13511
  const toolUseId = `shell_${require$$1.randomUUID().replace(/-/g, "")}`;
@@ -13420,6 +13623,7 @@ class MessageCoordinator {
13420
13623
  currentMessageId = null;
13421
13624
  messageIdCounter = 0;
13422
13625
  isStopped = false;
13626
+ runStartTime = null;
13423
13627
  constructor(config) {
13424
13628
  this.config = config;
13425
13629
  }
@@ -13539,11 +13743,11 @@ class MessageCoordinator {
13539
13743
  async processBashCommand(envelope) {
13540
13744
  this.log("info", "COORDINATOR", `Processing bash command: ${envelope.content}`);
13541
13745
  await this.waitForState("idle");
13542
- this.workerState = "executing-command";
13746
+ this.setWorkerState("running");
13543
13747
  try {
13544
13748
  await this.config.handlers.onBashCommand(envelope.content, envelope.originalMessage);
13545
13749
  } finally {
13546
- this.workerState = "idle";
13750
+ this.setWorkerState("idle");
13547
13751
  }
13548
13752
  }
13549
13753
  /**
@@ -13552,11 +13756,11 @@ class MessageCoordinator {
13552
13756
  async processMergeRequest(envelope) {
13553
13757
  this.log("info", "COORDINATOR", "Processing merge-request command");
13554
13758
  await this.waitForState("idle");
13555
- this.workerState = "executing-command";
13759
+ this.setWorkerState("running");
13556
13760
  try {
13557
13761
  await this.config.handlers.onMergeRequest(envelope.originalMessage);
13558
13762
  } finally {
13559
- this.workerState = "idle";
13763
+ this.setWorkerState("idle");
13560
13764
  }
13561
13765
  }
13562
13766
  async waitForState(targetState) {
@@ -13576,11 +13780,24 @@ class MessageCoordinator {
13576
13780
  }
13577
13781
  /**
13578
13782
  * Set the worker state (called by worker when state changes)
13783
+ * Automatically sends WebSocket events and tracks execution duration
13579
13784
  */
13580
13785
  setWorkerState(state) {
13581
- if (this.workerState !== state) {
13582
- this.log("info", "COORDINATOR", `Worker state: ${this.workerState} \u2192 ${state}`);
13583
- this.workerState = state;
13786
+ if (this.workerState === state) return;
13787
+ const prevState = this.workerState;
13788
+ this.log("info", "COORDINATOR", `Worker state: ${prevState} \u2192 ${state}`);
13789
+ this.workerState = state;
13790
+ if (state === "running" && prevState === "idle") {
13791
+ this.runStartTime = Date.now();
13792
+ this.config.workClient.sendWorkRunning();
13793
+ }
13794
+ if (state === "idle" && prevState === "running") {
13795
+ let duration;
13796
+ if (this.runStartTime) {
13797
+ duration = Date.now() - this.runStartTime;
13798
+ this.runStartTime = null;
13799
+ }
13800
+ this.config.workClient.sendWorkerReady(duration);
13584
13801
  }
13585
13802
  }
13586
13803
  /**
@@ -13620,7 +13837,7 @@ class ClaudeWorker {
13620
13837
  messageQueue = [];
13621
13838
  messageResolverRef = { current: null };
13622
13839
  abortController = new AbortController();
13623
- permissionAwaiter = /* @__PURE__ */ new Map();
13840
+ askUserAwaiter = /* @__PURE__ */ new Map();
13624
13841
  filteredToolUseIds = /* @__PURE__ */ new Set();
13625
13842
  timerManager;
13626
13843
  context;
@@ -13630,6 +13847,11 @@ class ClaudeWorker {
13630
13847
  dataEncryptionKey = null;
13631
13848
  coordinator;
13632
13849
  loadedHooks;
13850
+ loadedAgentConfig;
13851
+ // Pending permission requests: toolName -> Promise<'allow' | 'deny'> (to dedupe concurrent requests)
13852
+ pendingPermissions = /* @__PURE__ */ new Map();
13853
+ // Granted permissions cache: toolName -> true (to avoid repeated asks for same tool)
13854
+ grantedPermissions = /* @__PURE__ */ new Set();
13633
13855
  async start() {
13634
13856
  try {
13635
13857
  await this.initialize();
@@ -13650,18 +13872,69 @@ class ClaudeWorker {
13650
13872
  if (this.timerManager) {
13651
13873
  this.timerManager.clearIdleTimer();
13652
13874
  }
13875
+ if (this.logger) {
13876
+ await new Promise((resolve) => {
13877
+ this.logger.on("finish", resolve);
13878
+ this.logger.end();
13879
+ });
13880
+ }
13653
13881
  process.exit(0);
13654
13882
  }
13655
13883
  }
13656
13884
  async initialize() {
13657
13885
  const taskId = this.options.input.taskId;
13658
13886
  const userId = this.options.input.userId;
13659
- let workingDirectory = process.cwd();
13660
- let initialCommitHash;
13661
- this.logger = await this.createLogger({ type: "worker", taskId });
13887
+ this.logger = this.createWorkerLogger({ type: "worker", taskId });
13662
13888
  if (this.options.input.dataEncryptionKey && this.options.secretKey) {
13663
13889
  this.dataEncryptionKey = shared.decryptWithEphemeralKey(shared.decodeBase64(this.options.input.dataEncryptionKey), this.options.secretKey);
13664
13890
  }
13891
+ if (this.options.input.encryptedMessage && this.dataEncryptionKey) {
13892
+ this.options.input.message = shared.decryptSdkMessage(this.options.input.encryptedMessage, this.dataEncryptionKey) || void 0;
13893
+ }
13894
+ let workingDirectory = this.options.input.cwd ? this.options.input.cwd.replace(/^~/, os.homedir()) : process.cwd();
13895
+ const idleTimeoutMs = Math.max(0, this.options.idleTimeoutSecond ?? 0) * 1e3;
13896
+ this.timerManager = this.createIdleTimerManager(idleTimeoutMs, taskId);
13897
+ const workerConfig = this.createWorkerClientConfig(userId, taskId, workingDirectory);
13898
+ const workClient = new WorkerClient(workerConfig.config, workerConfig.handlers);
13899
+ await workClient.connect();
13900
+ workClient.sendWorkerInitializing();
13901
+ this.context = {
13902
+ credentials: this.credentials,
13903
+ options: this.options,
13904
+ workClient,
13905
+ workingDirectory,
13906
+ initialCommitHash: "",
13907
+ // Will be set after setupWorkspace
13908
+ logger: this.logger
13909
+ };
13910
+ this.coordinator = new MessageCoordinator({
13911
+ workerType: "claude",
13912
+ workClient,
13913
+ handlers: {
13914
+ onNormalMessage: async (message) => {
13915
+ await this.enqueueMessage(message);
13916
+ },
13917
+ onBashCommand: async (command, _originalMessage) => {
13918
+ await this.executeBashCommand(command);
13919
+ },
13920
+ onMergeRequest: async (_originalMessage) => {
13921
+ await this.executeMergeRequest();
13922
+ }
13923
+ },
13924
+ logger: (level, category, message) => {
13925
+ const validLevel = level;
13926
+ this.log(validLevel, category, message);
13927
+ }
13928
+ });
13929
+ if (!this.options.input.gitUrl) {
13930
+ const hasChanges = await checkUncommittedChanges(workingDirectory);
13931
+ if (hasChanges) {
13932
+ this.log("info", "GIT", "Detected uncommitted changes, asking user for action");
13933
+ const action = await this.askUncommittedChangesAction();
13934
+ await handleUncommittedChanges(workingDirectory, action);
13935
+ }
13936
+ }
13937
+ let initialCommitHash;
13665
13938
  try {
13666
13939
  const hooks = await this.loadAgentHooks();
13667
13940
  const workspaceResult = await setupWorkspace({
@@ -13673,6 +13946,8 @@ class ClaudeWorker {
13673
13946
  }, hooks);
13674
13947
  workingDirectory = workspaceResult.workingDirectory;
13675
13948
  initialCommitHash = workspaceResult.initialCommitHash;
13949
+ this.context.workingDirectory = workingDirectory;
13950
+ this.context.initialCommitHash = initialCommitHash;
13676
13951
  await _package.machine.writeInitialCommitHash(userId, taskId, initialCommitHash);
13677
13952
  this.log("info", "GIT", `Initial commit: ${initialCommitHash}`);
13678
13953
  this.initialCommitHashForPR = initialCommitHash;
@@ -13680,20 +13955,10 @@ class ClaudeWorker {
13680
13955
  this.logGitStateResult(gitStateResult, "start");
13681
13956
  } catch (error) {
13682
13957
  this.log("error", "GIT", "Failed to setup workspace:", error);
13683
- const basicConfig = this.createBasicWorkerConfig(userId, taskId, workingDirectory);
13684
13958
  const errorMessage = error instanceof Error ? error.message : String(error);
13685
- await WorkerClient.sendErrorAndExit(
13686
- basicConfig,
13687
- `Failed to setup workspace: ${errorMessage}`
13688
- );
13959
+ await workClient.sendErrorMessageAndExit(`Failed to setup workspace: ${errorMessage}`);
13689
13960
  process.exit(1);
13690
13961
  }
13691
- const idleTimeoutMs = Math.max(0, this.options.idleTimeoutSecond ?? 0) * 1e3;
13692
- this.timerManager = this.createIdleTimerManager(idleTimeoutMs, taskId);
13693
- const workerConfig = this.createWorkerClientConfig(userId, taskId, workingDirectory);
13694
- const workClient = new WorkerClient(workerConfig.config, workerConfig.handlers);
13695
- await workClient.connect();
13696
- workClient.sendWorkerInitializing();
13697
13962
  try {
13698
13963
  const metadata = {
13699
13964
  cwd: workingDirectory,
@@ -13710,35 +13975,6 @@ class ClaudeWorker {
13710
13975
  } catch (error) {
13711
13976
  this.log("warn", "DAEMON", "Failed to report session:", error);
13712
13977
  }
13713
- if (this.options.input.encryptedMessage && this.dataEncryptionKey) {
13714
- this.options.input.message = shared.decryptSdkMessage(this.options.input.encryptedMessage, this.dataEncryptionKey) || void 0;
13715
- }
13716
- this.context = {
13717
- credentials: this.credentials,
13718
- options: this.options,
13719
- workClient,
13720
- workingDirectory,
13721
- initialCommitHash,
13722
- logger: this.logger
13723
- };
13724
- this.coordinator = new MessageCoordinator({
13725
- workerType: "claude",
13726
- handlers: {
13727
- onNormalMessage: async (message) => {
13728
- await this.enqueueMessage(message);
13729
- },
13730
- onBashCommand: async (command, _originalMessage) => {
13731
- await this.executeBashCommand(command);
13732
- },
13733
- onMergeRequest: async (_originalMessage) => {
13734
- await this.executeMergeRequest();
13735
- }
13736
- },
13737
- logger: (level, category, message) => {
13738
- const validLevel = level;
13739
- this.log(validLevel, category, message);
13740
- }
13741
- });
13742
13978
  }
13743
13979
  async handleEvent() {
13744
13980
  if (this.options.input.message) {
@@ -13756,15 +13992,30 @@ class ClaudeWorker {
13756
13992
  }
13757
13993
  const hasChanges = await hasUncommittedChanges(this.context.workingDirectory);
13758
13994
  if (hasChanges) {
13759
- await autoCommit(this.context.workingDirectory, "Update task changes");
13995
+ await autoCommit(this.context.workingDirectory, "Checkpoint for PR generation");
13760
13996
  this.log("info", "MERGE", "Auto-committed changes");
13761
13997
  }
13998
+ const currentHash = await getCurrentCommitHash(this.context.workingDirectory);
13999
+ const diffStats = await getDiffStats(this.context.workingDirectory, this.initialCommitHashForPR, currentHash);
14000
+ if (diffStats.files.length === 0) {
14001
+ const errorMessage = "No changes to create PR: no files changed since task started";
14002
+ this.log("error", "MERGE", errorMessage);
14003
+ this.context.workClient.sendSystemErrorMessage(errorMessage);
14004
+ return;
14005
+ }
14006
+ this.log("info", "MERGE", `Found ${diffStats.files.length} files changed`);
13762
14007
  const branchName = await getCurrentBranch(this.context.workingDirectory);
13763
14008
  this.log("info", "MERGE", `Pushing branch ${branchName} to remote`);
13764
14009
  await gitPush(this.context.workingDirectory, branchName, false);
13765
14010
  this.log("info", "MERGE", "Successfully pushed branch to remote");
14011
+ const prPrompt = buildPRPrompt(
14012
+ { initialCommitHash: this.initialCommitHashForPR, currentCommitHash: currentHash, branchName },
14013
+ this.loadedAgentConfig?.customPRPromptTemplate ? {
14014
+ customTemplate: this.loadedAgentConfig.customPRPromptTemplate,
14015
+ mode: this.loadedAgentConfig.prPromptMode
14016
+ } : void 0
14017
+ );
13766
14018
  this.inMergeRequest = true;
13767
- const prPrompt = await this.buildCreatePRPrompt();
13768
14019
  await this.enqueueMessage({
13769
14020
  type: "user",
13770
14021
  message: {
@@ -13793,53 +14044,8 @@ class ClaudeWorker {
13793
14044
  }
13794
14045
  );
13795
14046
  this.timerManager.startIdleTimer();
13796
- this.context.workClient.sendWorkerReady();
13797
14047
  this.log("info", "BASH", `Worker ready after command execution (exit code: ${exitCode})`);
13798
14048
  }
13799
- async buildCreatePRPrompt() {
13800
- if (!this.initialCommitHashForPR) {
13801
- return 'Forbidden create PR by yourself. Must use the mcp__agentrix__create_pr tool. All the changed has been pushed. Please analyze the changes and use the mcp__agentrix__create_pr tool to create a pull request with a title and description. Use conventional commits format for the title (e.g., "feat: add new feature").';
13802
- }
13803
- try {
13804
- const currentHash = await autoCommit(this.context.workingDirectory, "Checkpoint for PR generation");
13805
- const stats = await getDiffStats(
13806
- this.context.workingDirectory,
13807
- this.initialCommitHashForPR,
13808
- currentHash
13809
- );
13810
- const diff = await generateDiffPatch(
13811
- this.context.workingDirectory,
13812
- this.initialCommitHashForPR,
13813
- currentHash
13814
- );
13815
- const statsText = `Files changed: ${stats.files.length}, +${stats.totalInsertions}/-${stats.totalDeletions}
13816
-
13817
- Detailed changes:
13818
- ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("\n")}`;
13819
- return `All the changed has been successfully pushed to the git branch. Please create a Pull Request by analyzing the changes below.
13820
-
13821
- Changes made:
13822
- ${statsText}
13823
-
13824
- Diff (first 5000 chars):
13825
- \`\`\`
13826
- ${diff.substring(0, 5e3)}
13827
- \`\`\`
13828
-
13829
- Requirements:
13830
- - Title: Use conventional commits format (feat/fix/docs/refactor/test/chore: description), maximum 50 characters
13831
- - Description: Provide a clear, detailed explanation of:
13832
- * What changed (the actual modifications made)
13833
- * Why these changes were necessary (the problem being solved)
13834
- * Any important technical decisions or trade-offs
13835
- * Impact on existing functionality
13836
-
13837
- Please must use the mcp__agentrix__create_pr tool to create the pull request with the generated title and description.Forbidden create PR by yourself.`;
13838
- } catch (error) {
13839
- this.log("warn", "GIT", "Failed to generate diff for PR prompt:", error);
13840
- return 'The code has been committed. Please use the mcp__agentrix__create_pr tool to create a pull request with a title and description. Use conventional commits format for the title (e.g., "feat: add new feature").';
13841
- }
13842
- }
13843
14049
  async runClaude() {
13844
14050
  this.log("info", "AGENT", `Starting Claude agent for task ${this.taskId}`);
13845
14051
  const agentSessionId = "agentSessionId" in this.options.input ? this.options.input.agentSessionId : void 0;
@@ -13850,12 +14056,12 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13850
14056
  );
13851
14057
  const sdkMcpServer = this.createAgentrixMcpServer();
13852
14058
  const mcpServers = {
13853
- agentrix: sdkMcpServer,
13854
- ...agentConfig.customMcpServers
14059
+ agentrix: sdkMcpServer
13855
14060
  };
13856
14061
  const allowedTools = [
13857
14062
  "mcp__agentrix__change_task_title",
13858
14063
  "mcp__agentrix__create_pr",
14064
+ "mcp__agentrix__ask_user",
13859
14065
  ...agentConfig.customAllowedTools
13860
14066
  ];
13861
14067
  const messageConsumer = this.createMessageConsumer();
@@ -13875,6 +14081,7 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13875
14081
  systemPrompt: finalSystemPrompt,
13876
14082
  mcpServers,
13877
14083
  allowedTools,
14084
+ plugins: agentConfig.customPlugins,
13878
14085
  abortController: this.abortController,
13879
14086
  env: this.buildEnvironmentOverrides(),
13880
14087
  maxTurns: agentConfig.customMaxTurns ?? this.options.input.maxTurns ?? 50,
@@ -13884,18 +14091,16 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13884
14091
  }
13885
14092
  });
13886
14093
  if (this.messageQueue.length > 0) {
13887
- this.context.workClient.sendWorkRunning();
13888
- this.coordinator.setWorkerState("processing-sdk");
14094
+ this.coordinator.setWorkerState("running");
13889
14095
  } else {
13890
14096
  this.timerManager.startIdleTimer();
13891
- this.context.workClient.sendWorkerReady();
13892
14097
  }
13893
14098
  for await (const message of response) {
13894
14099
  this.timerManager.clearIdleTimer();
13895
14100
  this.context.logger.debug(`sdk message: ${JSON.stringify(message)}`);
13896
14101
  if (message.type === "system" && message.subtype === "init") {
13897
14102
  this.context.workClient.sendUpdateTaskAgentSessionId(message.session_id);
13898
- this.context.workClient.sendWorkRunning();
14103
+ this.coordinator.setWorkerState("running");
13899
14104
  continue;
13900
14105
  }
13901
14106
  const filteredMessage = this.filterMessages(message);
@@ -13905,9 +14110,8 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13905
14110
  if (message.type === "result") {
13906
14111
  this.coordinator.setWorkerState("idle");
13907
14112
  this.timerManager.startIdleTimer();
13908
- this.context.workClient.sendWorkerReady();
13909
14113
  } else {
13910
- this.coordinator.setWorkerState("processing-sdk");
14114
+ this.coordinator.setWorkerState("running");
13911
14115
  }
13912
14116
  }
13913
14117
  this.log("info", "AGENT", `Claude agent finished for task ${this.taskId}`);
@@ -14067,33 +14271,117 @@ Type: ${mimeType}`
14067
14271
  }
14068
14272
  createPermissionHandler() {
14069
14273
  return async (toolName, input) => {
14070
- const eventId = this.context.workClient.sendRequirePermission(toolName, input);
14071
- const timeoutMs = 3e4;
14072
- const permissionResponse = await new Promise(
14073
- (resolve, reject) => {
14074
- const timeout = setTimeout(() => {
14075
- this.permissionAwaiter.delete(eventId);
14076
- reject(new Error(`Permission request timeout for tool ${toolName}`));
14077
- }, timeoutMs);
14078
- this.permissionAwaiter.set(eventId, (response) => {
14079
- clearTimeout(timeout);
14080
- resolve(response);
14081
- });
14274
+ if (this.grantedPermissions.has(toolName)) {
14275
+ this.log("info", "PERMISSION", `Tool "${toolName}" already granted, skipping`);
14276
+ return { behavior: "allow", updatedInput: input };
14277
+ }
14278
+ const pending = this.pendingPermissions.get(toolName);
14279
+ if (pending) {
14280
+ this.log("info", "PERMISSION", `Tool "${toolName}" has pending request, waiting...`);
14281
+ const decision = await pending;
14282
+ if (decision === "allow") {
14283
+ return { behavior: "allow", updatedInput: input };
14284
+ } else {
14285
+ return { behavior: "deny", message: "Permission denied by user" };
14082
14286
  }
14083
- );
14084
- if (permissionResponse.behavior === "allow") {
14085
- return {
14086
- behavior: "allow",
14087
- updatedInput: input
14088
- };
14089
- } else {
14090
- return {
14091
- behavior: "deny",
14092
- message: permissionResponse.message || "Permission denied"
14093
- };
14287
+ }
14288
+ this.log("info", "PERMISSION", `Requesting permission for "${toolName}"`);
14289
+ let resolveDecision;
14290
+ const permissionPromise = new Promise((resolve) => {
14291
+ resolveDecision = resolve;
14292
+ });
14293
+ this.pendingPermissions.set(toolName, permissionPromise);
14294
+ try {
14295
+ const decision = await this.requestToolPermission(toolName);
14296
+ resolveDecision(decision);
14297
+ if (decision === "allow") {
14298
+ this.grantedPermissions.add(toolName);
14299
+ return { behavior: "allow", updatedInput: input };
14300
+ } else {
14301
+ return { behavior: "deny", message: "Permission denied by user" };
14302
+ }
14303
+ } catch (error) {
14304
+ resolveDecision("deny");
14305
+ return { behavior: "deny", message: "Permission request failed" };
14306
+ } finally {
14307
+ this.pendingPermissions.delete(toolName);
14094
14308
  }
14095
14309
  };
14096
14310
  }
14311
+ async requestToolPermission(toolName) {
14312
+ const questions = [{
14313
+ question: `Tool "${toolName}" is requesting permission to execute. Allow this operation?`,
14314
+ header: "Permission",
14315
+ multiSelect: false,
14316
+ options: [
14317
+ { label: "Allow", description: "Allow this tool to execute" },
14318
+ { label: "Deny", description: "Deny this tool execution" }
14319
+ ]
14320
+ }];
14321
+ try {
14322
+ const response = await this.askUser(questions);
14323
+ const answer = response.answers[0];
14324
+ return answer === "Allow" ? "allow" : "deny";
14325
+ } catch (error) {
14326
+ this.log("warn", "PERMISSION", `Permission request failed: ${error}`);
14327
+ return "deny";
14328
+ }
14329
+ }
14330
+ /**
14331
+ * Ask user questions and wait for response
14332
+ * Sends ask_user message via task-message and waits for ask_user_response
14333
+ * @param questions - Array of questions (1-4)
14334
+ * @returns Promise resolving to user's response
14335
+ */
14336
+ async askUser(questions) {
14337
+ const eventId = this.context.workClient.sendAskUser(questions);
14338
+ const timeoutMs = 3e5;
14339
+ return new Promise((resolve, reject) => {
14340
+ const timeout = setTimeout(() => {
14341
+ this.askUserAwaiter.delete(eventId);
14342
+ reject(new Error("Ask user request timed out"));
14343
+ }, timeoutMs);
14344
+ this.askUserAwaiter.set(eventId, (response) => {
14345
+ clearTimeout(timeout);
14346
+ resolve(response);
14347
+ });
14348
+ });
14349
+ }
14350
+ /**
14351
+ * Ask user how to handle uncommitted changes
14352
+ * @returns The action to take: ignore, commit, stash, or abort
14353
+ */
14354
+ async askUncommittedChangesAction() {
14355
+ const questions = [{
14356
+ question: "Uncommitted changes detected in the working directory. How would you like to proceed?",
14357
+ header: "Git Status",
14358
+ multiSelect: false,
14359
+ options: [
14360
+ { label: "Ignore", description: "Keep changes and continue with the task" },
14361
+ { label: "Commit", description: "Commit current changes before starting" },
14362
+ { label: "Stash", description: "Stash changes (git stash) before starting" },
14363
+ { label: "Abort", description: "Cancel the task, do nothing" }
14364
+ ]
14365
+ }];
14366
+ try {
14367
+ const response = await this.askUser(questions);
14368
+ const answer = response.answers[0];
14369
+ if (answer.startsWith("other:")) {
14370
+ this.log("info", "GIT", `User provided custom input: ${answer}, defaulting to Abort`);
14371
+ return "Abort";
14372
+ }
14373
+ const labelToAction = {
14374
+ "Ignore": "Ignore",
14375
+ "Commit": "Commit",
14376
+ "Stash": "Stash",
14377
+ "Abort": "Abort"
14378
+ };
14379
+ return labelToAction[answer] || "Abort";
14380
+ } catch (error) {
14381
+ this.log("warn", "GIT", `Failed to get user response for uncommitted changes: ${error}`);
14382
+ return "Abort";
14383
+ }
14384
+ }
14097
14385
  createAgentrixMcpServer() {
14098
14386
  return claudeAgentSdk.createSdkMcpServer({
14099
14387
  name: "agentrix",
@@ -14154,6 +14442,50 @@ URL: ${result.pullRequestUrl}`
14154
14442
  };
14155
14443
  }
14156
14444
  }
14445
+ ),
14446
+ claudeAgentSdk.tool(
14447
+ "ask_user",
14448
+ 'Ask the user questions when you need clarification or user input. Supports 1-4 questions with 2-4 options each. Use this when you need user decisions or additional information. An "Other" option with free text input is automatically added.',
14449
+ {
14450
+ questions: zod.z.array(zod.z.object({
14451
+ question: zod.z.string().describe("The complete question to ask the user"),
14452
+ header: zod.z.string().max(12).describe("Short label displayed as a chip/tag (max 12 chars)"),
14453
+ multiSelect: zod.z.boolean().describe("Set to true to allow multiple option selections"),
14454
+ options: zod.z.array(zod.z.object({
14455
+ label: zod.z.string().describe("Option label (1-5 words)"),
14456
+ description: zod.z.string().describe("Explanation of what this option means")
14457
+ })).min(2).max(4).describe("Available choices (2-4 options)")
14458
+ })).min(1).max(4).describe("Questions to ask (1-4 questions)")
14459
+ },
14460
+ async (args) => {
14461
+ try {
14462
+ const questionsWithOther = args.questions.map((q) => ({
14463
+ ...q,
14464
+ options: [...q.options, { label: "Other", description: "" }]
14465
+ }));
14466
+ const result = await this.askUser(questionsWithOther);
14467
+ const answerText = result.answers.map((answer) => {
14468
+ if (answer.startsWith("other:")) {
14469
+ return `Other: "${answer.slice(6)}"`;
14470
+ }
14471
+ return answer;
14472
+ }).join("\n");
14473
+ return {
14474
+ content: [{ type: "text", text: `User answers:
14475
+ ${answerText}` }]
14476
+ };
14477
+ } catch (error) {
14478
+ this.log("error", "ASK_USER", "Failed to get user response:", error);
14479
+ return {
14480
+ content: [
14481
+ {
14482
+ type: "text",
14483
+ text: `Failed to get user response: ${error instanceof Error ? error.message : "Unknown error"}`
14484
+ }
14485
+ ]
14486
+ };
14487
+ }
14488
+ }
14157
14489
  )
14158
14490
  ]
14159
14491
  });
@@ -14169,7 +14501,7 @@ URL: ${result.pullRequestUrl}`
14169
14501
  } catch (error) {
14170
14502
  this.log("warn", "GIT", "Failed to handle git state on worker stop:", error);
14171
14503
  }
14172
- this.context.workClient.sendWorkerReady();
14504
+ this.coordinator.setWorkerState("idle");
14173
14505
  }
14174
14506
  filterMessages(message) {
14175
14507
  const msg = message;
@@ -14210,38 +14542,45 @@ URL: ${result.pullRequestUrl}`
14210
14542
  return this.createDefaultAgentConfig();
14211
14543
  }
14212
14544
  const claudeConfig = agentConfig.claude;
14213
- const customMcpServers = Object.fromEntries(
14214
- Object.entries(claudeConfig.mcpServers).map(([name, server]) => [name, server.instance])
14215
- );
14216
- this.log("info", "AGENT", `Agent ${this.options.input.agentId} loaded successfully`);
14217
- return {
14545
+ const customPlugins = claudeConfig.plugins.map((path) => ({
14546
+ type: "local",
14547
+ path
14548
+ }));
14549
+ this.log("info", "AGENT", `Agent ${this.options.input.agentId} loaded successfully (${customPlugins.length} plugins)`);
14550
+ const config = {
14218
14551
  customSystemPrompt: claudeConfig.systemPrompt,
14219
- // Loaded string content
14220
14552
  customModel: claudeConfig.config.model,
14221
14553
  customFallbackModel: claudeConfig.config.fallbackModel,
14222
14554
  customMaxTurns: claudeConfig.config.maxTurns,
14223
14555
  customExtraArgs: claudeConfig.config.extraArgs,
14224
14556
  customPermissionMode: claudeConfig.config.settings?.permissionMode,
14225
- customMcpServers,
14226
14557
  customAllowedTools: claudeConfig.config.settings?.allowedTools || [],
14558
+ customPlugins,
14227
14559
  systemPromptMode: claudeConfig.config.systemPrompt?.mode ?? "append",
14228
- hooks: this.loadedHooks
14229
- // Use cached hooks
14560
+ hooks: this.loadedHooks,
14561
+ customPRPromptTemplate: claudeConfig.prPromptTemplate,
14562
+ prPromptMode: claudeConfig.config.pullRequestPrompt?.mode ?? "append"
14230
14563
  };
14564
+ this.loadedAgentConfig = config;
14565
+ return config;
14231
14566
  } catch (error) {
14232
14567
  this.log("error", "AGENT", `Failed to load agent: ${error instanceof Error ? error.message : String(error)}`);
14233
14568
  return this.createDefaultAgentConfig();
14234
14569
  }
14235
14570
  }
14236
14571
  createDefaultAgentConfig() {
14237
- return {
14572
+ const config = {
14238
14573
  customSystemPrompt: void 0,
14239
14574
  customModel: void 0,
14240
14575
  customMaxTurns: void 0,
14241
- customMcpServers: {},
14242
14576
  customAllowedTools: [],
14243
- systemPromptMode: "append"
14577
+ customPlugins: [],
14578
+ systemPromptMode: "append",
14579
+ customPRPromptTemplate: void 0,
14580
+ prPromptMode: "append"
14244
14581
  };
14582
+ this.loadedAgentConfig = config;
14583
+ return config;
14245
14584
  }
14246
14585
  buildEnvironmentOverrides() {
14247
14586
  return {
@@ -14349,18 +14688,20 @@ URL: ${result.pullRequestUrl}`
14349
14688
  this.timerManager.stopTask("event");
14350
14689
  },
14351
14690
  onTaskMessage: async (message) => {
14352
- if (message.type === "user") {
14691
+ if (shared.isAskUserResponseMessage(message)) {
14692
+ const [eventId, awaiter] = this.askUserAwaiter.entries().next().value || [];
14693
+ if (eventId && awaiter) {
14694
+ this.askUserAwaiter.delete(eventId);
14695
+ awaiter(message);
14696
+ }
14697
+ this.timerManager.clearIdleTimer();
14698
+ return;
14699
+ }
14700
+ if ("type" in message && message.type === "user") {
14353
14701
  const userMessage = message;
14354
14702
  await this.coordinator.enqueue(userMessage);
14355
14703
  this.timerManager.clearIdleTimer();
14356
14704
  }
14357
- },
14358
- onPermissionResponse: async (response) => {
14359
- const awaiter = this.permissionAwaiter.get(response.opCode);
14360
- if (awaiter) {
14361
- this.permissionAwaiter.delete(response.opCode);
14362
- awaiter(response);
14363
- }
14364
14705
  }
14365
14706
  }
14366
14707
  };
@@ -14399,9 +14740,8 @@ URL: ${result.pullRequestUrl}`
14399
14740
  this.log("info", "GIT", `Patch: ${gitStateResult.patchPath}`);
14400
14741
  }
14401
14742
  }
14402
- async createLogger(options) {
14403
- const { createLogger } = await Promise.resolve().then(function () { return require('./logger---ZD5a2u.cjs'); }).then(function (n) { return n.logger$1; });
14404
- return createLogger(options);
14743
+ createWorkerLogger(options) {
14744
+ return _package.createLogger(options);
14405
14745
  }
14406
14746
  log(level, category, message, ...args) {
14407
14747
  if (this.logger) {
@@ -16139,6 +16479,7 @@ class CodexWorker {
16139
16479
  };
16140
16480
  this.coordinator = new MessageCoordinator({
16141
16481
  workerType: "codex",
16482
+ workClient,
16142
16483
  handlers: {
16143
16484
  onNormalMessage: async (message) => {
16144
16485
  const input = await this.convertSDKMessageToCodexInput(message);
@@ -16211,7 +16552,6 @@ class CodexWorker {
16211
16552
  }
16212
16553
  );
16213
16554
  this.timerManager.startIdleTimer();
16214
- this.context.workClient.sendWorkerReady();
16215
16555
  this.log("info", "BASH", `Worker ready after command execution (exit code: ${exitCode})`);
16216
16556
  }
16217
16557
  async buildCreatePRPrompt() {
@@ -16268,8 +16608,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16268
16608
  skipGitRepoCheck: true
16269
16609
  });
16270
16610
  }
16271
- this.context.workClient.sendWorkRunning();
16272
- this.coordinator.setWorkerState("processing-sdk");
16611
+ this.coordinator.setWorkerState("running");
16273
16612
  while (!this.isStopping) {
16274
16613
  this.log("debug", "AGENT", `Loop iteration: turnCount=${this.turnCount}, queueLength=${this.messageQueue.length}`);
16275
16614
  let userInput = null;
@@ -16281,7 +16620,6 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16281
16620
  await this.reportGitState("idle");
16282
16621
  this.coordinator.setWorkerState("idle");
16283
16622
  this.timerManager.startIdleTimer();
16284
- this.context.workClient.sendWorkerReady();
16285
16623
  this.log("info", "AGENT", "Sent worker-ready, waiting for next message");
16286
16624
  userInput = await this.waitForNextMessage();
16287
16625
  }
@@ -16291,7 +16629,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16291
16629
  }
16292
16630
  try {
16293
16631
  this.timerManager.clearIdleTimer();
16294
- this.coordinator.setWorkerState("processing-sdk");
16632
+ this.coordinator.setWorkerState("running");
16295
16633
  await this.processUserInput(userInput);
16296
16634
  this.turnCount++;
16297
16635
  this.log("debug", "AGENT", `Message processed, turnCount now ${this.turnCount}`);
@@ -16327,7 +16665,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16327
16665
  const turnOptions = this.inMergeRequest ? { outputSchema: getPROutputSchema() } : {};
16328
16666
  this.log("debug", "AGENT", "Calling thread.runStreamed");
16329
16667
  const { events } = await this.thread.runStreamed(input, turnOptions);
16330
- this.context.workClient.sendWorkRunning();
16668
+ this.coordinator.setWorkerState("running");
16331
16669
  this.log("debug", "AGENT", "Starting to process events");
16332
16670
  for await (const event of events) {
16333
16671
  if (this.isStopping) {
@@ -16618,7 +16956,7 @@ Type: ${mimeType}`
16618
16956
  }
16619
16957
  async handleStopHook() {
16620
16958
  await this.reportGitState("stop");
16621
- this.context.workClient.sendWorkerReady();
16959
+ this.coordinator.setWorkerState("idle");
16622
16960
  }
16623
16961
  async loadAgentConfiguration() {
16624
16962
  if (!this.options.input.agentId || this.options.input.agentId === "default") {
@@ -16710,14 +17048,15 @@ Type: ${mimeType}`
16710
17048
  this.timerManager.stopTask("event");
16711
17049
  },
16712
17050
  onTaskMessage: async (message) => {
16713
- if (message.type === "user") {
17051
+ if (shared.isAskUserResponseMessage(message)) {
17052
+ this.log("debug", "AGENT", "Received ask_user_response (not used by Codex)");
17053
+ return;
17054
+ }
17055
+ if ("type" in message && message.type === "user") {
16714
17056
  const userMessage = message;
16715
17057
  await this.coordinator.enqueue(userMessage);
16716
17058
  this.timerManager.clearIdleTimer();
16717
17059
  }
16718
- },
16719
- onPermissionResponse: async (response) => {
16720
- this.log("debug", "AGENT", `Permission response received: ${response.behavior}`);
16721
17060
  }
16722
17061
  }
16723
17062
  };
@@ -17072,7 +17411,7 @@ cli.command("upgrade", "Upgrade CLI to the latest version", {}, async (argv) =>
17072
17411
  }
17073
17412
  }
17074
17413
  try {
17075
- const { version } = await Promise.resolve().then(function () { return require('./logger---ZD5a2u.cjs'); }).then(function (n) { return n._package; });
17414
+ const { version } = await Promise.resolve().then(function () { return require('./logger-DzYRcKN1.cjs'); }).then(function (n) { return n._package; });
17076
17415
  console.log(chalk.green(`
17077
17416
  \u2713 Now running version: ${version}`));
17078
17417
  } catch {
@@ -17254,7 +17593,7 @@ cli.command(
17254
17593
  },
17255
17594
  async (argv) => {
17256
17595
  try {
17257
- const { testCommand } = await Promise.resolve().then(function () { return require('./index-BdFjpsoN.cjs'); });
17596
+ const { testCommand } = await Promise.resolve().then(function () { return require('./index-Cs99FGLe.cjs'); });
17258
17597
  await testCommand(argv);
17259
17598
  } catch (error) {
17260
17599
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Test mode failed");