@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.
@@ -1,12 +1,10 @@
1
1
  import yargs from 'yargs';
2
2
  import { hideBin } from 'yargs/helpers';
3
3
  import chalk from 'chalk';
4
- import { encodeBase64, createKeyPairWithUit8Array, encryptMachineEncryptionKey, generateAESKey, decodeBase64, decryptWithEphemeralKey, createEventId, encryptFileContent, machineAuth, encryptSdkMessage, decryptSdkMessage, loadAgentConfig, getAgentContext, workerAuth } from '@agentrix/shared';
4
+ import { encodeBase64, createKeyPairWithUit8Array, encryptMachineEncryptionKey, generateAESKey, decodeBase64, decryptWithEphemeralKey, createEventId, encryptFileContent, machineAuth, encryptSdkMessage, decryptSdkMessage, loadAgentConfig, getAgentContext, workerAuth, isAskUserResponseMessage } from '@agentrix/shared';
5
5
  import { randomBytes, randomUUID } from 'node:crypto';
6
6
  import axios from 'axios';
7
- import { m as machine, l as logger, p as projectPath, a as packageJson, c as createLogger, g as getLogPath, b as logger$1 } from './logger-D-ioMWe6.mjs';
8
- import * as fs from 'node:fs';
9
- import { existsSync, rmSync, createWriteStream, readdirSync, mkdirSync } from 'node:fs';
7
+ import { m as machine, l as logger, p as projectPath, a as packageJson, c as createLogger, g as getLogPath, b as logger$1 } from './logger-BzpMLIL-.mjs';
10
8
  import { createInterface } from 'node:readline';
11
9
  import fs$1, { readFileSync, existsSync as existsSync$1 } from 'fs';
12
10
  import path$1, { join } from 'path';
@@ -14,11 +12,15 @@ import os, { homedir } from 'node:os';
14
12
  import open from 'open';
15
13
  import { io } from 'socket.io-client';
16
14
  import { EventEmitter } from 'node:events';
15
+ import * as fs from 'node:fs';
16
+ import { existsSync, createWriteStream, readdirSync, mkdirSync } from 'node:fs';
17
17
  import * as path from 'node:path';
18
18
  import { join as join$1, basename, extname, dirname } from 'node:path';
19
19
  import { spawn, execSync } from 'child_process';
20
20
  import psList from 'ps-list';
21
21
  import spawn$1 from 'cross-spawn';
22
+ import { isSupportedPlatform, checkSandboxDependencies, NetworkManager, SandboxManager } from '@xmz-ai/sandbox-runtime';
23
+ import { getPlatform } from '@xmz-ai/sandbox-runtime/dist/utils/platform.js';
22
24
  import fastify from 'fastify';
23
25
  import { z } from 'zod';
24
26
  import { validatorCompiler, serializerCompiler } from 'fastify-type-provider-zod';
@@ -44,22 +46,12 @@ async function daemonPost(path, body) {
44
46
  error: errorMessage
45
47
  };
46
48
  }
47
- try {
48
- process.kill(state.pid, 0);
49
- } catch (error) {
50
- const errorMessage = "Daemon is not running, file is stale";
51
- logger.debug(`[CONTROL CLIENT] ${errorMessage}`);
52
- return {
53
- error: errorMessage
54
- };
55
- }
56
49
  try {
57
50
  const timeout = process.env.AGENTRIX_DAEMON_HTTP_TIMEOUT ? parseInt(process.env.AGENTRIX_DAEMON_HTTP_TIMEOUT) : 1e4;
58
- const response = await fetch(`http://127.0.0.1:${state.port}${path}`, {
51
+ const response = await fetch(`http://agentrix-local.xmz.ai:${state.port}${path}`, {
59
52
  method: "POST",
60
53
  headers: { "Content-Type": "application/json" },
61
54
  body: JSON.stringify(body || {}),
62
- // Mostly increased for stress test
63
55
  signal: AbortSignal.timeout(timeout)
64
56
  });
65
57
  if (!response.ok) {
@@ -205,11 +197,8 @@ async function handleAuthLogout() {
205
197
  console.log(chalk.gray("Stopped daemon"));
206
198
  } catch {
207
199
  }
208
- const paths = machine.getStatePaths();
209
- if (existsSync(paths.rootDir)) {
210
- rmSync(paths.rootDir, { recursive: true, force: true });
211
- console.log(chalk.gray(`Removed agentrix home directory`));
212
- }
200
+ await machine.clearCredentials();
201
+ console.log(chalk.gray(`Removed credentials`));
213
202
  console.log(chalk.green("\u2713 Successfully logged out"));
214
203
  } catch (error) {
215
204
  throw new Error(`Failed to logout: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -11865,7 +11854,7 @@ async function findAllAgentrixProcesses() {
11865
11854
  async function findRunawayAgentrixProcesses() {
11866
11855
  const allProcesses = await findAllAgentrixProcesses();
11867
11856
  return allProcesses.filter(
11868
- (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "worker")
11857
+ (p) => p.pid !== process.pid && (p.type === "daemon" || p.type === "worker" || p.type === "upgrade-daemon")
11869
11858
  ).map((p) => ({ pid: p.pid, command: p.command }));
11870
11859
  }
11871
11860
  async function killRunawayAgentrixProcesses() {
@@ -11967,6 +11956,27 @@ async function runDoctorCommand(filter) {
11967
11956
  } catch (error) {
11968
11957
  console.log(chalk.red("\u274C Error reading credentials"));
11969
11958
  }
11959
+ console.log(chalk.bold("\n\u{1F512} Sandbox Dependencies"));
11960
+ const platform = getPlatform();
11961
+ if (isSupportedPlatform(platform)) {
11962
+ console.log(`Platform: ${chalk.green(platform)} (supported)`);
11963
+ const depsOk = checkSandboxDependencies();
11964
+ if (depsOk) {
11965
+ console.log(chalk.green("\u2713 All sandbox dependencies available"));
11966
+ } else {
11967
+ console.log(chalk.yellow("\u26A0\uFE0F Some sandbox dependencies missing"));
11968
+ if (platform === "linux") {
11969
+ console.log(chalk.gray(" Required: bubblewrap, socat"));
11970
+ console.log(chalk.gray(" Install: sudo apt install bubblewrap socat"));
11971
+ } else if (platform === "macos") {
11972
+ console.log(chalk.gray(" Required: ripgrep"));
11973
+ console.log(chalk.gray(" Install: brew install ripgrep"));
11974
+ }
11975
+ }
11976
+ } else {
11977
+ console.log(`Platform: ${chalk.yellow(platform)} (not supported)`);
11978
+ console.log(chalk.gray(" \u26A0\uFE0F Sandbox will be disabled"));
11979
+ }
11970
11980
  }
11971
11981
  console.log(chalk.bold("\n\u{1F916} Daemon Status"));
11972
11982
  try {
@@ -12191,9 +12201,11 @@ function createPromiseWithTimeout(options) {
12191
12201
  class TaskWorkerManager {
12192
12202
  pidToTrackedSession;
12193
12203
  pidToAwaiter;
12194
- constructor() {
12204
+ sandboxPool;
12205
+ constructor(sandboxPool) {
12195
12206
  this.pidToTrackedSession = /* @__PURE__ */ new Map();
12196
12207
  this.pidToAwaiter = /* @__PURE__ */ new Map();
12208
+ this.sandboxPool = sandboxPool || null;
12197
12209
  }
12198
12210
  getCurrentSessions() {
12199
12211
  return Array.from(this.pidToTrackedSession.values());
@@ -12234,9 +12246,15 @@ class TaskWorkerManager {
12234
12246
  this.pidToTrackedSession.set(workerProcess.pid, tracked);
12235
12247
  workerProcess.on("exit", (code, signal) => {
12236
12248
  this.pidToTrackedSession.delete(workerProcess.pid);
12249
+ if (this.sandboxPool) {
12250
+ this.sandboxPool.disposeWorkerSandbox(data.taskId);
12251
+ }
12237
12252
  });
12238
12253
  workerProcess.on("error", (error) => {
12239
12254
  this.pidToTrackedSession.delete(workerProcess.pid);
12255
+ if (this.sandboxPool) {
12256
+ this.sandboxPool.disposeWorkerSandbox(data.taskId);
12257
+ }
12240
12258
  });
12241
12259
  }
12242
12260
  async startWorker(options) {
@@ -12261,14 +12279,53 @@ class TaskWorkerManager {
12261
12279
  "--idle-timeout",
12262
12280
  "120"
12263
12281
  ];
12264
- const workerProcess = spawnAgentrixCLI(args, {
12265
- cwd,
12266
- detached: true,
12267
- stdio: ["ignore", "pipe", "pipe"],
12268
- env: {
12269
- ...process.env
12282
+ let workerProcess;
12283
+ if (this.sandboxPool?.isEnabled()) {
12284
+ try {
12285
+ const sandbox = await this.sandboxPool.createWorkerSandbox(
12286
+ options.taskId,
12287
+ options.userId,
12288
+ cwd
12289
+ );
12290
+ if (!sandbox) {
12291
+ throw new Error("Failed to create sandbox instance");
12292
+ }
12293
+ const { projectPath } = await import('./logger-BzpMLIL-.mjs').then(function (n) { return n.d; });
12294
+ const { join } = await import('path');
12295
+ const entrypoint = join(projectPath(), "dist", "index.mjs");
12296
+ const nodeArgs = ["--no-warnings", "--no-deprecation", entrypoint, ...args];
12297
+ const originalCommand = `"${process.execPath}" ${nodeArgs.map((a) => `"${a}"`).join(" ")}`;
12298
+ const sandboxedCommand = await this.sandboxPool.wrapWorkerCommand(
12299
+ options.taskId,
12300
+ originalCommand
12301
+ );
12302
+ logger.debug(`[SESSION] Sandboxed command for task ${options.taskId}: ${sandboxedCommand}`);
12303
+ workerProcess = spawn(sandboxedCommand, {
12304
+ shell: true,
12305
+ cwd,
12306
+ detached: true,
12307
+ stdio: ["ignore", "pipe", "pipe"],
12308
+ env: {
12309
+ ...process.env
12310
+ // Environment variables controlled by SandboxManager
12311
+ }
12312
+ });
12313
+ logger.info(`[SESSION] Worker started with sandbox, PID: ${workerProcess.pid}`);
12314
+ } catch (error) {
12315
+ logger.error(`[SESSION] Failed to setup sandbox for task ${options.taskId}:`, error);
12316
+ ack.status = "failed";
12317
+ ack.message = `Sandbox setup failed: ${error instanceof Error ? error.message : String(error)}`;
12318
+ return ack;
12270
12319
  }
12271
- });
12320
+ } else {
12321
+ workerProcess = spawnAgentrixCLI(args, {
12322
+ cwd,
12323
+ detached: true,
12324
+ stdio: ["ignore", "pipe", "pipe"],
12325
+ env: { ...process.env }
12326
+ });
12327
+ logger.info(`[SESSION] Worker started without sandbox, PID: ${workerProcess.pid}`);
12328
+ }
12272
12329
  if (process.env.DEBUG) {
12273
12330
  workerProcess.stdout?.on("data", (data) => {
12274
12331
  logger.debug(`[Daemon] worker stdout: ${data.toString()}`);
@@ -12387,6 +12444,114 @@ function setupGracefulShutdown(options) {
12387
12444
  };
12388
12445
  }
12389
12446
 
12447
+ class SandboxPool {
12448
+ networkManager = null;
12449
+ workerSandboxes = /* @__PURE__ */ new Map();
12450
+ settings = null;
12451
+ platform;
12452
+ constructor() {
12453
+ this.platform = getPlatform();
12454
+ }
12455
+ async initialize(settings) {
12456
+ this.settings = settings;
12457
+ if (!settings.enabled) {
12458
+ logger.info("[SANDBOX] Sandbox disabled via settings");
12459
+ return false;
12460
+ }
12461
+ if (!isSupportedPlatform(this.platform)) {
12462
+ logger.warn("[SANDBOX] Platform not supported, sandbox disabled");
12463
+ return false;
12464
+ }
12465
+ try {
12466
+ const apiHost = new URL(machine.serverUrl).hostname;
12467
+ const networkConfig = {
12468
+ allowedDomains: [
12469
+ apiHost,
12470
+ ...settings.network.allowedDomains
12471
+ ],
12472
+ deniedDomains: settings.network.deniedDomains,
12473
+ allowLocalBinding: false
12474
+ };
12475
+ this.networkManager = new NetworkManager();
12476
+ await this.networkManager.initialize(networkConfig);
12477
+ logger.info("[SANDBOX] Sandbox pool initialized successfully");
12478
+ return true;
12479
+ } catch (error) {
12480
+ logger.error("[SANDBOX] Failed to initialize:", error);
12481
+ throw error;
12482
+ }
12483
+ }
12484
+ async createWorkerSandbox(taskId, userId, workingDirectory) {
12485
+ if (!this.networkManager || !this.settings?.enabled) {
12486
+ return null;
12487
+ }
12488
+ try {
12489
+ const taskDir = machine.resolveTaskDir(userId, taskId);
12490
+ const logsDir = machine.getStatePaths().logsDir;
12491
+ const baseFilesystem = this.settings.filesystem || {};
12492
+ const baseEnv = this.settings.env || {};
12493
+ const filesystemConfig = {
12494
+ ...baseFilesystem,
12495
+ allowWrite: [
12496
+ ...baseFilesystem.allowWrite || [],
12497
+ taskDir,
12498
+ workingDirectory,
12499
+ logsDir
12500
+ ]
12501
+ };
12502
+ if (this.platform === "linux" && baseFilesystem.allowRead) {
12503
+ filesystemConfig.allowRead = [
12504
+ ...baseFilesystem.allowRead
12505
+ ];
12506
+ }
12507
+ const instanceConfig = {
12508
+ filesystem: filesystemConfig,
12509
+ env: baseEnv
12510
+ };
12511
+ const sandbox = new SandboxManager(this.networkManager, instanceConfig);
12512
+ this.workerSandboxes.set(taskId, sandbox);
12513
+ logger.info(`[SANDBOX] Created sandbox for task ${taskId}`);
12514
+ return sandbox;
12515
+ } catch (error) {
12516
+ logger.error(`[SANDBOX] Failed to create sandbox for task ${taskId}:`, error);
12517
+ return null;
12518
+ }
12519
+ }
12520
+ async wrapWorkerCommand(taskId, command) {
12521
+ const sandbox = this.workerSandboxes.get(taskId);
12522
+ if (!sandbox) {
12523
+ throw new Error(`No sandbox found for task ${taskId}`);
12524
+ }
12525
+ const wrapped = await sandbox.wrapWithSandbox(command);
12526
+ logger.debug(`[SANDBOX] Wrapped command for task ${taskId}`);
12527
+ return wrapped;
12528
+ }
12529
+ disposeWorkerSandbox(taskId) {
12530
+ const sandbox = this.workerSandboxes.get(taskId);
12531
+ if (sandbox) {
12532
+ sandbox.dispose();
12533
+ this.workerSandboxes.delete(taskId);
12534
+ logger.debug(`[SANDBOX] Disposed sandbox for task ${taskId}`);
12535
+ }
12536
+ }
12537
+ async shutdown() {
12538
+ logger.info("[SANDBOX] Shutting down sandbox pool");
12539
+ for (const [taskId, sandbox] of this.workerSandboxes.entries()) {
12540
+ sandbox.dispose();
12541
+ logger.debug(`[SANDBOX] Disposed sandbox for task ${taskId}`);
12542
+ }
12543
+ this.workerSandboxes.clear();
12544
+ if (this.networkManager) {
12545
+ await this.networkManager.shutdown();
12546
+ this.networkManager = null;
12547
+ logger.info("[SANDBOX] Network manager shutdown complete");
12548
+ }
12549
+ }
12550
+ isEnabled() {
12551
+ return this.settings?.enabled === true;
12552
+ }
12553
+ }
12554
+
12390
12555
  async function startDaemon() {
12391
12556
  Object.assign(logger, createLogger({ type: "daemon" }));
12392
12557
  const { requestShutdown, shutdownPromise } = setupGracefulShutdown({
@@ -12415,7 +12580,9 @@ async function startDaemon() {
12415
12580
  }
12416
12581
  const credentials = await authAndSetupMachineIfNeeded();
12417
12582
  logger.debug("[DAEMON RUN] Auth and machine setup complete");
12418
- const sessionManager = new TaskWorkerManager();
12583
+ const sandboxPool = new SandboxPool();
12584
+ await sandboxPool.initialize(machine.getSandboxSettings());
12585
+ const sessionManager = new TaskWorkerManager(sandboxPool);
12419
12586
  const { port: controlPort, stop: stopControlServer } = await startDaemonControlServer({
12420
12587
  getChildren: () => sessionManager.getCurrentSessions(),
12421
12588
  stopSession: (id) => sessionManager.stopSession(id),
@@ -12463,6 +12630,7 @@ async function startDaemon() {
12463
12630
  const cleanupAndShutdown = async (source, errorMessage) => {
12464
12631
  await machineClient.disconnect();
12465
12632
  await stopControlServer();
12633
+ await sandboxPool.shutdown();
12466
12634
  await cleanupDaemonState();
12467
12635
  await stopCaffeinate();
12468
12636
  await machine.releaseDaemonLock(daemonLockHandle);
@@ -12579,14 +12747,6 @@ function createWorkerEventHandlers(context) {
12579
12747
  }
12580
12748
  }
12581
12749
  }
12582
- },
12583
- "require-permission-response": async (data) => {
12584
- if (data.taskId !== context.taskId) {
12585
- return;
12586
- }
12587
- if (context.onPermissionResponse) {
12588
- await context.onPermissionResponse(data);
12589
- }
12590
12750
  }
12591
12751
  };
12592
12752
  }
@@ -12627,7 +12787,6 @@ class WorkerClient {
12627
12787
  cwd: normalizedCwd,
12628
12788
  stopTask: options.stopTask,
12629
12789
  onTaskMessage: options.onTaskMessage,
12630
- onPermissionResponse: options.onPermissionResponse,
12631
12790
  onGitPush: options.onGitPush,
12632
12791
  dataEncryptionKey: config.dataEncryptionKey
12633
12792
  };
@@ -12656,26 +12815,32 @@ class WorkerClient {
12656
12815
  }
12657
12816
  this.client.disconnect();
12658
12817
  }
12659
- sendTaskMessage(message) {
12660
- const cwdWithSlash = this.context.cwd;
12661
- const cwdWithoutSlash = cwdWithSlash.slice(0, -1);
12662
- let content = JSON.stringify(message);
12663
- content = content.replaceAll(cwdWithSlash, "");
12664
- content = content.replaceAll(cwdWithoutSlash, ".");
12665
- let assistantMessage = JSON.parse(content);
12818
+ sendTaskMessage(message, options) {
12819
+ const { replaceCwd = true } = options || {};
12820
+ let processedMessage = message;
12821
+ if (replaceCwd) {
12822
+ const cwdWithSlash = this.context.cwd;
12823
+ const cwdWithoutSlash = cwdWithSlash.slice(0, -1);
12824
+ let content = JSON.stringify(message);
12825
+ content = content.replaceAll(cwdWithSlash, "");
12826
+ content = content.replaceAll(cwdWithoutSlash, ".");
12827
+ processedMessage = JSON.parse(content);
12828
+ }
12666
12829
  let encryptedMessage;
12667
12830
  if (this.context.dataEncryptionKey) {
12668
- encryptedMessage = encryptSdkMessage(assistantMessage, this.context.dataEncryptionKey);
12669
- assistantMessage = void 0;
12831
+ encryptedMessage = encryptSdkMessage(processedMessage, this.context.dataEncryptionKey);
12832
+ processedMessage = void 0;
12670
12833
  }
12834
+ const eventId = createEventId();
12671
12835
  const payload = {
12672
- eventId: createEventId(),
12836
+ eventId,
12673
12837
  taskId: this.context.taskId,
12674
12838
  from: "worker",
12675
- message: assistantMessage,
12839
+ message: processedMessage,
12676
12840
  encryptedMessage
12677
12841
  };
12678
12842
  this.client.send("task-message", payload);
12843
+ return eventId;
12679
12844
  }
12680
12845
  sendWorkerInitializing() {
12681
12846
  const workerInitializingEvent = {
@@ -12686,12 +12851,13 @@ class WorkerClient {
12686
12851
  };
12687
12852
  this.client.send("worker-initializing", workerInitializingEvent);
12688
12853
  }
12689
- sendWorkerReady() {
12854
+ sendWorkerReady(duration) {
12690
12855
  const workerReadyEvent = {
12691
12856
  eventId: createEventId(),
12692
12857
  taskId: this.context.taskId,
12693
12858
  machineId: this.context.machineId,
12694
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
12859
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12860
+ ...duration !== void 0 && { duration }
12695
12861
  };
12696
12862
  this.client.send("worker-ready", workerReadyEvent);
12697
12863
  }
@@ -12749,17 +12915,13 @@ ${errorMessage}`,
12749
12915
  };
12750
12916
  this.sendTaskMessage(systemMessage);
12751
12917
  }
12752
- sendRequirePermission(toolName, toolInput) {
12753
- const eventId = createEventId();
12754
- const permissionRequest = {
12755
- eventId,
12756
- taskId: this.context.taskId,
12757
- toolName,
12758
- toolInput
12759
- };
12760
- this.client.send("require-permission", permissionRequest);
12761
- logger.info(`[AGENT] Permission requested for tool: ${toolName}`);
12762
- return eventId;
12918
+ /**
12919
+ * Send ask-user message to request user input
12920
+ * @param questions - Array of questions (1-4)
12921
+ * @returns eventId for tracking the request
12922
+ */
12923
+ sendAskUser(questions) {
12924
+ return this.sendTaskMessage({ type: "ask_user", questions }, { replaceCwd: false });
12763
12925
  }
12764
12926
  sendUpdateTaskAgentSessionId(agentSessionId) {
12765
12927
  const updateSessionEvent = {
@@ -12814,7 +12976,6 @@ ${errorMessage}`,
12814
12976
  this.client.onEvent("cancel-task", handlers["cancel-task"]);
12815
12977
  this.client.onEvent("stop-task", handlers["stop-task"]);
12816
12978
  this.client.onEvent("task-message", handlers["task-message"]);
12817
- this.client.onEvent("require-permission-response", handlers["require-permission-response"]);
12818
12979
  }
12819
12980
  }
12820
12981
 
@@ -13017,6 +13178,12 @@ async function hasUncommittedChanges(dir) {
13017
13178
  const status = await git.status();
13018
13179
  return !status.isClean();
13019
13180
  }
13181
+ async function gitStash(dir, message) {
13182
+ const git = simpleGit(dir);
13183
+ {
13184
+ await git.stash(["push"]);
13185
+ }
13186
+ }
13020
13187
  async function getCurrentCommitHash(dir) {
13021
13188
  const git = simpleGit(dir);
13022
13189
  const log = await git.log({ maxCount: 1 });
@@ -13106,6 +13273,30 @@ async function executeHook(hooks, hookName, input, logger) {
13106
13273
  }
13107
13274
  }
13108
13275
 
13276
+ async function checkUncommittedChanges(workingDirectory) {
13277
+ const isRepo = await isGitRepository(workingDirectory);
13278
+ if (!isRepo) {
13279
+ return false;
13280
+ }
13281
+ return await hasUncommittedChanges(workingDirectory);
13282
+ }
13283
+ async function handleUncommittedChanges(workingDirectory, action) {
13284
+ switch (action) {
13285
+ case "Ignore":
13286
+ console.log("[GIT] User chose to ignore uncommitted changes");
13287
+ break;
13288
+ case "Commit":
13289
+ console.log("[GIT] Auto-committing uncommitted changes");
13290
+ await autoCommit(workingDirectory, "WIP: Auto-commit before task");
13291
+ break;
13292
+ case "Stash":
13293
+ console.log("[GIT] Stashing uncommitted changes");
13294
+ await gitStash(workingDirectory);
13295
+ break;
13296
+ case "Abort":
13297
+ throw new Error("Task aborted by user due to uncommitted changes");
13298
+ }
13299
+ }
13109
13300
  function createTaskBranchName(taskId) {
13110
13301
  return `agentrix/${taskId}`;
13111
13302
  }
@@ -13145,17 +13336,6 @@ async function setupLocalWorkspace(workingDirectory, taskId, hooks) {
13145
13336
  const isRepo = await isGitRepository(workingDirectory);
13146
13337
  const isEmpty = isDirectoryEmpty(workingDirectory);
13147
13338
  if (isRepo) {
13148
- const hasChanges = await hasUncommittedChanges(workingDirectory);
13149
- if (hasChanges) {
13150
- throw new Error(
13151
- `Directory ${workingDirectory} has uncommitted changes.
13152
-
13153
- Please commit or stash your changes before starting:
13154
- git add . && git commit -m "WIP"
13155
- or:
13156
- git stash`
13157
- );
13158
- }
13159
13339
  const hasCommits = await hasAnyCommits(workingDirectory);
13160
13340
  if (!hasCommits) {
13161
13341
  console.log("[GIT] Repository has no commits, creating initial commit");
@@ -13283,6 +13463,29 @@ async function markCommitAsSent(userId, taskId, commitHash) {
13283
13463
  await machine.writeLastSentCommitHash(userId, taskId, commitHash);
13284
13464
  }
13285
13465
 
13466
+ function getDefaultPRPrompt(params) {
13467
+ return `All changes have been pushed to branch "${params.branchName}".
13468
+
13469
+ Commit range: ${params.initialCommitHash}..${params.currentCommitHash}
13470
+
13471
+ Based on our conversation context, create a Pull Request:
13472
+ - Title: conventional commits format (feat/fix/docs/refactor/test/chore: description)
13473
+ - Description: what changed, why, and any important decisions
13474
+
13475
+ Use mcp__agentrix__create_pr tool to create the PR.`;
13476
+ }
13477
+ function applyTemplateVariables(template, params) {
13478
+ return template.replace(/\{\{initialCommitHash\}\}/g, params.initialCommitHash).replace(/\{\{currentCommitHash\}\}/g, params.currentCommitHash).replace(/\{\{branchName\}\}/g, params.branchName);
13479
+ }
13480
+ function buildPRPrompt(params, config) {
13481
+ const defaultPrompt = getDefaultPRPrompt(params);
13482
+ if (!config?.customTemplate) {
13483
+ return defaultPrompt;
13484
+ }
13485
+ const customPrompt = applyTemplateVariables(config.customTemplate, params);
13486
+ return config.mode === "replace" ? customPrompt : defaultPrompt + "\n\n" + customPrompt;
13487
+ }
13488
+
13286
13489
  function executeCommandStreaming(command, cwd, callbacks, timeoutMs = 6e4) {
13287
13490
  return new Promise((resolve) => {
13288
13491
  const toolUseId = `shell_${randomUUID$1().replace(/-/g, "")}`;
@@ -13400,6 +13603,7 @@ class MessageCoordinator {
13400
13603
  currentMessageId = null;
13401
13604
  messageIdCounter = 0;
13402
13605
  isStopped = false;
13606
+ runStartTime = null;
13403
13607
  constructor(config) {
13404
13608
  this.config = config;
13405
13609
  }
@@ -13519,11 +13723,11 @@ class MessageCoordinator {
13519
13723
  async processBashCommand(envelope) {
13520
13724
  this.log("info", "COORDINATOR", `Processing bash command: ${envelope.content}`);
13521
13725
  await this.waitForState("idle");
13522
- this.workerState = "executing-command";
13726
+ this.setWorkerState("running");
13523
13727
  try {
13524
13728
  await this.config.handlers.onBashCommand(envelope.content, envelope.originalMessage);
13525
13729
  } finally {
13526
- this.workerState = "idle";
13730
+ this.setWorkerState("idle");
13527
13731
  }
13528
13732
  }
13529
13733
  /**
@@ -13532,11 +13736,11 @@ class MessageCoordinator {
13532
13736
  async processMergeRequest(envelope) {
13533
13737
  this.log("info", "COORDINATOR", "Processing merge-request command");
13534
13738
  await this.waitForState("idle");
13535
- this.workerState = "executing-command";
13739
+ this.setWorkerState("running");
13536
13740
  try {
13537
13741
  await this.config.handlers.onMergeRequest(envelope.originalMessage);
13538
13742
  } finally {
13539
- this.workerState = "idle";
13743
+ this.setWorkerState("idle");
13540
13744
  }
13541
13745
  }
13542
13746
  async waitForState(targetState) {
@@ -13556,11 +13760,24 @@ class MessageCoordinator {
13556
13760
  }
13557
13761
  /**
13558
13762
  * Set the worker state (called by worker when state changes)
13763
+ * Automatically sends WebSocket events and tracks execution duration
13559
13764
  */
13560
13765
  setWorkerState(state) {
13561
- if (this.workerState !== state) {
13562
- this.log("info", "COORDINATOR", `Worker state: ${this.workerState} \u2192 ${state}`);
13563
- this.workerState = state;
13766
+ if (this.workerState === state) return;
13767
+ const prevState = this.workerState;
13768
+ this.log("info", "COORDINATOR", `Worker state: ${prevState} \u2192 ${state}`);
13769
+ this.workerState = state;
13770
+ if (state === "running" && prevState === "idle") {
13771
+ this.runStartTime = Date.now();
13772
+ this.config.workClient.sendWorkRunning();
13773
+ }
13774
+ if (state === "idle" && prevState === "running") {
13775
+ let duration;
13776
+ if (this.runStartTime) {
13777
+ duration = Date.now() - this.runStartTime;
13778
+ this.runStartTime = null;
13779
+ }
13780
+ this.config.workClient.sendWorkerReady(duration);
13564
13781
  }
13565
13782
  }
13566
13783
  /**
@@ -13600,7 +13817,7 @@ class ClaudeWorker {
13600
13817
  messageQueue = [];
13601
13818
  messageResolverRef = { current: null };
13602
13819
  abortController = new AbortController();
13603
- permissionAwaiter = /* @__PURE__ */ new Map();
13820
+ askUserAwaiter = /* @__PURE__ */ new Map();
13604
13821
  filteredToolUseIds = /* @__PURE__ */ new Set();
13605
13822
  timerManager;
13606
13823
  context;
@@ -13610,6 +13827,11 @@ class ClaudeWorker {
13610
13827
  dataEncryptionKey = null;
13611
13828
  coordinator;
13612
13829
  loadedHooks;
13830
+ loadedAgentConfig;
13831
+ // Pending permission requests: toolName -> Promise<'allow' | 'deny'> (to dedupe concurrent requests)
13832
+ pendingPermissions = /* @__PURE__ */ new Map();
13833
+ // Granted permissions cache: toolName -> true (to avoid repeated asks for same tool)
13834
+ grantedPermissions = /* @__PURE__ */ new Set();
13613
13835
  async start() {
13614
13836
  try {
13615
13837
  await this.initialize();
@@ -13630,18 +13852,69 @@ class ClaudeWorker {
13630
13852
  if (this.timerManager) {
13631
13853
  this.timerManager.clearIdleTimer();
13632
13854
  }
13855
+ if (this.logger) {
13856
+ await new Promise((resolve) => {
13857
+ this.logger.on("finish", resolve);
13858
+ this.logger.end();
13859
+ });
13860
+ }
13633
13861
  process.exit(0);
13634
13862
  }
13635
13863
  }
13636
13864
  async initialize() {
13637
13865
  const taskId = this.options.input.taskId;
13638
13866
  const userId = this.options.input.userId;
13639
- let workingDirectory = process.cwd();
13640
- let initialCommitHash;
13641
- this.logger = await this.createLogger({ type: "worker", taskId });
13867
+ this.logger = this.createWorkerLogger({ type: "worker", taskId });
13642
13868
  if (this.options.input.dataEncryptionKey && this.options.secretKey) {
13643
13869
  this.dataEncryptionKey = decryptWithEphemeralKey(decodeBase64(this.options.input.dataEncryptionKey), this.options.secretKey);
13644
13870
  }
13871
+ if (this.options.input.encryptedMessage && this.dataEncryptionKey) {
13872
+ this.options.input.message = decryptSdkMessage(this.options.input.encryptedMessage, this.dataEncryptionKey) || void 0;
13873
+ }
13874
+ let workingDirectory = this.options.input.cwd ? this.options.input.cwd.replace(/^~/, homedir()) : process.cwd();
13875
+ const idleTimeoutMs = Math.max(0, this.options.idleTimeoutSecond ?? 0) * 1e3;
13876
+ this.timerManager = this.createIdleTimerManager(idleTimeoutMs, taskId);
13877
+ const workerConfig = this.createWorkerClientConfig(userId, taskId, workingDirectory);
13878
+ const workClient = new WorkerClient(workerConfig.config, workerConfig.handlers);
13879
+ await workClient.connect();
13880
+ workClient.sendWorkerInitializing();
13881
+ this.context = {
13882
+ credentials: this.credentials,
13883
+ options: this.options,
13884
+ workClient,
13885
+ workingDirectory,
13886
+ initialCommitHash: "",
13887
+ // Will be set after setupWorkspace
13888
+ logger: this.logger
13889
+ };
13890
+ this.coordinator = new MessageCoordinator({
13891
+ workerType: "claude",
13892
+ workClient,
13893
+ handlers: {
13894
+ onNormalMessage: async (message) => {
13895
+ await this.enqueueMessage(message);
13896
+ },
13897
+ onBashCommand: async (command, _originalMessage) => {
13898
+ await this.executeBashCommand(command);
13899
+ },
13900
+ onMergeRequest: async (_originalMessage) => {
13901
+ await this.executeMergeRequest();
13902
+ }
13903
+ },
13904
+ logger: (level, category, message) => {
13905
+ const validLevel = level;
13906
+ this.log(validLevel, category, message);
13907
+ }
13908
+ });
13909
+ if (!this.options.input.gitUrl) {
13910
+ const hasChanges = await checkUncommittedChanges(workingDirectory);
13911
+ if (hasChanges) {
13912
+ this.log("info", "GIT", "Detected uncommitted changes, asking user for action");
13913
+ const action = await this.askUncommittedChangesAction();
13914
+ await handleUncommittedChanges(workingDirectory, action);
13915
+ }
13916
+ }
13917
+ let initialCommitHash;
13645
13918
  try {
13646
13919
  const hooks = await this.loadAgentHooks();
13647
13920
  const workspaceResult = await setupWorkspace({
@@ -13653,6 +13926,8 @@ class ClaudeWorker {
13653
13926
  }, hooks);
13654
13927
  workingDirectory = workspaceResult.workingDirectory;
13655
13928
  initialCommitHash = workspaceResult.initialCommitHash;
13929
+ this.context.workingDirectory = workingDirectory;
13930
+ this.context.initialCommitHash = initialCommitHash;
13656
13931
  await machine.writeInitialCommitHash(userId, taskId, initialCommitHash);
13657
13932
  this.log("info", "GIT", `Initial commit: ${initialCommitHash}`);
13658
13933
  this.initialCommitHashForPR = initialCommitHash;
@@ -13660,20 +13935,10 @@ class ClaudeWorker {
13660
13935
  this.logGitStateResult(gitStateResult, "start");
13661
13936
  } catch (error) {
13662
13937
  this.log("error", "GIT", "Failed to setup workspace:", error);
13663
- const basicConfig = this.createBasicWorkerConfig(userId, taskId, workingDirectory);
13664
13938
  const errorMessage = error instanceof Error ? error.message : String(error);
13665
- await WorkerClient.sendErrorAndExit(
13666
- basicConfig,
13667
- `Failed to setup workspace: ${errorMessage}`
13668
- );
13939
+ await workClient.sendErrorMessageAndExit(`Failed to setup workspace: ${errorMessage}`);
13669
13940
  process.exit(1);
13670
13941
  }
13671
- const idleTimeoutMs = Math.max(0, this.options.idleTimeoutSecond ?? 0) * 1e3;
13672
- this.timerManager = this.createIdleTimerManager(idleTimeoutMs, taskId);
13673
- const workerConfig = this.createWorkerClientConfig(userId, taskId, workingDirectory);
13674
- const workClient = new WorkerClient(workerConfig.config, workerConfig.handlers);
13675
- await workClient.connect();
13676
- workClient.sendWorkerInitializing();
13677
13942
  try {
13678
13943
  const metadata = {
13679
13944
  cwd: workingDirectory,
@@ -13690,35 +13955,6 @@ class ClaudeWorker {
13690
13955
  } catch (error) {
13691
13956
  this.log("warn", "DAEMON", "Failed to report session:", error);
13692
13957
  }
13693
- if (this.options.input.encryptedMessage && this.dataEncryptionKey) {
13694
- this.options.input.message = decryptSdkMessage(this.options.input.encryptedMessage, this.dataEncryptionKey) || void 0;
13695
- }
13696
- this.context = {
13697
- credentials: this.credentials,
13698
- options: this.options,
13699
- workClient,
13700
- workingDirectory,
13701
- initialCommitHash,
13702
- logger: this.logger
13703
- };
13704
- this.coordinator = new MessageCoordinator({
13705
- workerType: "claude",
13706
- handlers: {
13707
- onNormalMessage: async (message) => {
13708
- await this.enqueueMessage(message);
13709
- },
13710
- onBashCommand: async (command, _originalMessage) => {
13711
- await this.executeBashCommand(command);
13712
- },
13713
- onMergeRequest: async (_originalMessage) => {
13714
- await this.executeMergeRequest();
13715
- }
13716
- },
13717
- logger: (level, category, message) => {
13718
- const validLevel = level;
13719
- this.log(validLevel, category, message);
13720
- }
13721
- });
13722
13958
  }
13723
13959
  async handleEvent() {
13724
13960
  if (this.options.input.message) {
@@ -13736,15 +13972,30 @@ class ClaudeWorker {
13736
13972
  }
13737
13973
  const hasChanges = await hasUncommittedChanges(this.context.workingDirectory);
13738
13974
  if (hasChanges) {
13739
- await autoCommit(this.context.workingDirectory, "Update task changes");
13975
+ await autoCommit(this.context.workingDirectory, "Checkpoint for PR generation");
13740
13976
  this.log("info", "MERGE", "Auto-committed changes");
13741
13977
  }
13978
+ const currentHash = await getCurrentCommitHash(this.context.workingDirectory);
13979
+ const diffStats = await getDiffStats(this.context.workingDirectory, this.initialCommitHashForPR, currentHash);
13980
+ if (diffStats.files.length === 0) {
13981
+ const errorMessage = "No changes to create PR: no files changed since task started";
13982
+ this.log("error", "MERGE", errorMessage);
13983
+ this.context.workClient.sendSystemErrorMessage(errorMessage);
13984
+ return;
13985
+ }
13986
+ this.log("info", "MERGE", `Found ${diffStats.files.length} files changed`);
13742
13987
  const branchName = await getCurrentBranch(this.context.workingDirectory);
13743
13988
  this.log("info", "MERGE", `Pushing branch ${branchName} to remote`);
13744
13989
  await gitPush(this.context.workingDirectory, branchName, false);
13745
13990
  this.log("info", "MERGE", "Successfully pushed branch to remote");
13991
+ const prPrompt = buildPRPrompt(
13992
+ { initialCommitHash: this.initialCommitHashForPR, currentCommitHash: currentHash, branchName },
13993
+ this.loadedAgentConfig?.customPRPromptTemplate ? {
13994
+ customTemplate: this.loadedAgentConfig.customPRPromptTemplate,
13995
+ mode: this.loadedAgentConfig.prPromptMode
13996
+ } : void 0
13997
+ );
13746
13998
  this.inMergeRequest = true;
13747
- const prPrompt = await this.buildCreatePRPrompt();
13748
13999
  await this.enqueueMessage({
13749
14000
  type: "user",
13750
14001
  message: {
@@ -13773,53 +14024,8 @@ class ClaudeWorker {
13773
14024
  }
13774
14025
  );
13775
14026
  this.timerManager.startIdleTimer();
13776
- this.context.workClient.sendWorkerReady();
13777
14027
  this.log("info", "BASH", `Worker ready after command execution (exit code: ${exitCode})`);
13778
14028
  }
13779
- async buildCreatePRPrompt() {
13780
- if (!this.initialCommitHashForPR) {
13781
- 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").';
13782
- }
13783
- try {
13784
- const currentHash = await autoCommit(this.context.workingDirectory, "Checkpoint for PR generation");
13785
- const stats = await getDiffStats(
13786
- this.context.workingDirectory,
13787
- this.initialCommitHashForPR,
13788
- currentHash
13789
- );
13790
- const diff = await generateDiffPatch(
13791
- this.context.workingDirectory,
13792
- this.initialCommitHashForPR,
13793
- currentHash
13794
- );
13795
- const statsText = `Files changed: ${stats.files.length}, +${stats.totalInsertions}/-${stats.totalDeletions}
13796
-
13797
- Detailed changes:
13798
- ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("\n")}`;
13799
- return `All the changed has been successfully pushed to the git branch. Please create a Pull Request by analyzing the changes below.
13800
-
13801
- Changes made:
13802
- ${statsText}
13803
-
13804
- Diff (first 5000 chars):
13805
- \`\`\`
13806
- ${diff.substring(0, 5e3)}
13807
- \`\`\`
13808
-
13809
- Requirements:
13810
- - Title: Use conventional commits format (feat/fix/docs/refactor/test/chore: description), maximum 50 characters
13811
- - Description: Provide a clear, detailed explanation of:
13812
- * What changed (the actual modifications made)
13813
- * Why these changes were necessary (the problem being solved)
13814
- * Any important technical decisions or trade-offs
13815
- * Impact on existing functionality
13816
-
13817
- 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.`;
13818
- } catch (error) {
13819
- this.log("warn", "GIT", "Failed to generate diff for PR prompt:", error);
13820
- 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").';
13821
- }
13822
- }
13823
14029
  async runClaude() {
13824
14030
  this.log("info", "AGENT", `Starting Claude agent for task ${this.taskId}`);
13825
14031
  const agentSessionId = "agentSessionId" in this.options.input ? this.options.input.agentSessionId : void 0;
@@ -13830,12 +14036,12 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13830
14036
  );
13831
14037
  const sdkMcpServer = this.createAgentrixMcpServer();
13832
14038
  const mcpServers = {
13833
- agentrix: sdkMcpServer,
13834
- ...agentConfig.customMcpServers
14039
+ agentrix: sdkMcpServer
13835
14040
  };
13836
14041
  const allowedTools = [
13837
14042
  "mcp__agentrix__change_task_title",
13838
14043
  "mcp__agentrix__create_pr",
14044
+ "mcp__agentrix__ask_user",
13839
14045
  ...agentConfig.customAllowedTools
13840
14046
  ];
13841
14047
  const messageConsumer = this.createMessageConsumer();
@@ -13855,6 +14061,7 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13855
14061
  systemPrompt: finalSystemPrompt,
13856
14062
  mcpServers,
13857
14063
  allowedTools,
14064
+ plugins: agentConfig.customPlugins,
13858
14065
  abortController: this.abortController,
13859
14066
  env: this.buildEnvironmentOverrides(),
13860
14067
  maxTurns: agentConfig.customMaxTurns ?? this.options.input.maxTurns ?? 50,
@@ -13864,18 +14071,16 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13864
14071
  }
13865
14072
  });
13866
14073
  if (this.messageQueue.length > 0) {
13867
- this.context.workClient.sendWorkRunning();
13868
- this.coordinator.setWorkerState("processing-sdk");
14074
+ this.coordinator.setWorkerState("running");
13869
14075
  } else {
13870
14076
  this.timerManager.startIdleTimer();
13871
- this.context.workClient.sendWorkerReady();
13872
14077
  }
13873
14078
  for await (const message of response) {
13874
14079
  this.timerManager.clearIdleTimer();
13875
14080
  this.context.logger.debug(`sdk message: ${JSON.stringify(message)}`);
13876
14081
  if (message.type === "system" && message.subtype === "init") {
13877
14082
  this.context.workClient.sendUpdateTaskAgentSessionId(message.session_id);
13878
- this.context.workClient.sendWorkRunning();
14083
+ this.coordinator.setWorkerState("running");
13879
14084
  continue;
13880
14085
  }
13881
14086
  const filteredMessage = this.filterMessages(message);
@@ -13885,9 +14090,8 @@ Please must use the mcp__agentrix__create_pr tool to create the pull request wit
13885
14090
  if (message.type === "result") {
13886
14091
  this.coordinator.setWorkerState("idle");
13887
14092
  this.timerManager.startIdleTimer();
13888
- this.context.workClient.sendWorkerReady();
13889
14093
  } else {
13890
- this.coordinator.setWorkerState("processing-sdk");
14094
+ this.coordinator.setWorkerState("running");
13891
14095
  }
13892
14096
  }
13893
14097
  this.log("info", "AGENT", `Claude agent finished for task ${this.taskId}`);
@@ -14047,33 +14251,117 @@ Type: ${mimeType}`
14047
14251
  }
14048
14252
  createPermissionHandler() {
14049
14253
  return async (toolName, input) => {
14050
- const eventId = this.context.workClient.sendRequirePermission(toolName, input);
14051
- const timeoutMs = 3e4;
14052
- const permissionResponse = await new Promise(
14053
- (resolve, reject) => {
14054
- const timeout = setTimeout(() => {
14055
- this.permissionAwaiter.delete(eventId);
14056
- reject(new Error(`Permission request timeout for tool ${toolName}`));
14057
- }, timeoutMs);
14058
- this.permissionAwaiter.set(eventId, (response) => {
14059
- clearTimeout(timeout);
14060
- resolve(response);
14061
- });
14254
+ if (this.grantedPermissions.has(toolName)) {
14255
+ this.log("info", "PERMISSION", `Tool "${toolName}" already granted, skipping`);
14256
+ return { behavior: "allow", updatedInput: input };
14257
+ }
14258
+ const pending = this.pendingPermissions.get(toolName);
14259
+ if (pending) {
14260
+ this.log("info", "PERMISSION", `Tool "${toolName}" has pending request, waiting...`);
14261
+ const decision = await pending;
14262
+ if (decision === "allow") {
14263
+ return { behavior: "allow", updatedInput: input };
14264
+ } else {
14265
+ return { behavior: "deny", message: "Permission denied by user" };
14062
14266
  }
14063
- );
14064
- if (permissionResponse.behavior === "allow") {
14065
- return {
14066
- behavior: "allow",
14067
- updatedInput: input
14068
- };
14069
- } else {
14070
- return {
14071
- behavior: "deny",
14072
- message: permissionResponse.message || "Permission denied"
14073
- };
14267
+ }
14268
+ this.log("info", "PERMISSION", `Requesting permission for "${toolName}"`);
14269
+ let resolveDecision;
14270
+ const permissionPromise = new Promise((resolve) => {
14271
+ resolveDecision = resolve;
14272
+ });
14273
+ this.pendingPermissions.set(toolName, permissionPromise);
14274
+ try {
14275
+ const decision = await this.requestToolPermission(toolName);
14276
+ resolveDecision(decision);
14277
+ if (decision === "allow") {
14278
+ this.grantedPermissions.add(toolName);
14279
+ return { behavior: "allow", updatedInput: input };
14280
+ } else {
14281
+ return { behavior: "deny", message: "Permission denied by user" };
14282
+ }
14283
+ } catch (error) {
14284
+ resolveDecision("deny");
14285
+ return { behavior: "deny", message: "Permission request failed" };
14286
+ } finally {
14287
+ this.pendingPermissions.delete(toolName);
14074
14288
  }
14075
14289
  };
14076
14290
  }
14291
+ async requestToolPermission(toolName) {
14292
+ const questions = [{
14293
+ question: `Tool "${toolName}" is requesting permission to execute. Allow this operation?`,
14294
+ header: "Permission",
14295
+ multiSelect: false,
14296
+ options: [
14297
+ { label: "Allow", description: "Allow this tool to execute" },
14298
+ { label: "Deny", description: "Deny this tool execution" }
14299
+ ]
14300
+ }];
14301
+ try {
14302
+ const response = await this.askUser(questions);
14303
+ const answer = response.answers[0];
14304
+ return answer === "Allow" ? "allow" : "deny";
14305
+ } catch (error) {
14306
+ this.log("warn", "PERMISSION", `Permission request failed: ${error}`);
14307
+ return "deny";
14308
+ }
14309
+ }
14310
+ /**
14311
+ * Ask user questions and wait for response
14312
+ * Sends ask_user message via task-message and waits for ask_user_response
14313
+ * @param questions - Array of questions (1-4)
14314
+ * @returns Promise resolving to user's response
14315
+ */
14316
+ async askUser(questions) {
14317
+ const eventId = this.context.workClient.sendAskUser(questions);
14318
+ const timeoutMs = 3e5;
14319
+ return new Promise((resolve, reject) => {
14320
+ const timeout = setTimeout(() => {
14321
+ this.askUserAwaiter.delete(eventId);
14322
+ reject(new Error("Ask user request timed out"));
14323
+ }, timeoutMs);
14324
+ this.askUserAwaiter.set(eventId, (response) => {
14325
+ clearTimeout(timeout);
14326
+ resolve(response);
14327
+ });
14328
+ });
14329
+ }
14330
+ /**
14331
+ * Ask user how to handle uncommitted changes
14332
+ * @returns The action to take: ignore, commit, stash, or abort
14333
+ */
14334
+ async askUncommittedChangesAction() {
14335
+ const questions = [{
14336
+ question: "Uncommitted changes detected in the working directory. How would you like to proceed?",
14337
+ header: "Git Status",
14338
+ multiSelect: false,
14339
+ options: [
14340
+ { label: "Ignore", description: "Keep changes and continue with the task" },
14341
+ { label: "Commit", description: "Commit current changes before starting" },
14342
+ { label: "Stash", description: "Stash changes (git stash) before starting" },
14343
+ { label: "Abort", description: "Cancel the task, do nothing" }
14344
+ ]
14345
+ }];
14346
+ try {
14347
+ const response = await this.askUser(questions);
14348
+ const answer = response.answers[0];
14349
+ if (answer.startsWith("other:")) {
14350
+ this.log("info", "GIT", `User provided custom input: ${answer}, defaulting to Abort`);
14351
+ return "Abort";
14352
+ }
14353
+ const labelToAction = {
14354
+ "Ignore": "Ignore",
14355
+ "Commit": "Commit",
14356
+ "Stash": "Stash",
14357
+ "Abort": "Abort"
14358
+ };
14359
+ return labelToAction[answer] || "Abort";
14360
+ } catch (error) {
14361
+ this.log("warn", "GIT", `Failed to get user response for uncommitted changes: ${error}`);
14362
+ return "Abort";
14363
+ }
14364
+ }
14077
14365
  createAgentrixMcpServer() {
14078
14366
  return createSdkMcpServer({
14079
14367
  name: "agentrix",
@@ -14134,6 +14422,50 @@ URL: ${result.pullRequestUrl}`
14134
14422
  };
14135
14423
  }
14136
14424
  }
14425
+ ),
14426
+ tool(
14427
+ "ask_user",
14428
+ '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.',
14429
+ {
14430
+ questions: z.array(z.object({
14431
+ question: z.string().describe("The complete question to ask the user"),
14432
+ header: z.string().max(12).describe("Short label displayed as a chip/tag (max 12 chars)"),
14433
+ multiSelect: z.boolean().describe("Set to true to allow multiple option selections"),
14434
+ options: z.array(z.object({
14435
+ label: z.string().describe("Option label (1-5 words)"),
14436
+ description: z.string().describe("Explanation of what this option means")
14437
+ })).min(2).max(4).describe("Available choices (2-4 options)")
14438
+ })).min(1).max(4).describe("Questions to ask (1-4 questions)")
14439
+ },
14440
+ async (args) => {
14441
+ try {
14442
+ const questionsWithOther = args.questions.map((q) => ({
14443
+ ...q,
14444
+ options: [...q.options, { label: "Other", description: "" }]
14445
+ }));
14446
+ const result = await this.askUser(questionsWithOther);
14447
+ const answerText = result.answers.map((answer) => {
14448
+ if (answer.startsWith("other:")) {
14449
+ return `Other: "${answer.slice(6)}"`;
14450
+ }
14451
+ return answer;
14452
+ }).join("\n");
14453
+ return {
14454
+ content: [{ type: "text", text: `User answers:
14455
+ ${answerText}` }]
14456
+ };
14457
+ } catch (error) {
14458
+ this.log("error", "ASK_USER", "Failed to get user response:", error);
14459
+ return {
14460
+ content: [
14461
+ {
14462
+ type: "text",
14463
+ text: `Failed to get user response: ${error instanceof Error ? error.message : "Unknown error"}`
14464
+ }
14465
+ ]
14466
+ };
14467
+ }
14468
+ }
14137
14469
  )
14138
14470
  ]
14139
14471
  });
@@ -14149,7 +14481,7 @@ URL: ${result.pullRequestUrl}`
14149
14481
  } catch (error) {
14150
14482
  this.log("warn", "GIT", "Failed to handle git state on worker stop:", error);
14151
14483
  }
14152
- this.context.workClient.sendWorkerReady();
14484
+ this.coordinator.setWorkerState("idle");
14153
14485
  }
14154
14486
  filterMessages(message) {
14155
14487
  const msg = message;
@@ -14190,38 +14522,45 @@ URL: ${result.pullRequestUrl}`
14190
14522
  return this.createDefaultAgentConfig();
14191
14523
  }
14192
14524
  const claudeConfig = agentConfig.claude;
14193
- const customMcpServers = Object.fromEntries(
14194
- Object.entries(claudeConfig.mcpServers).map(([name, server]) => [name, server.instance])
14195
- );
14196
- this.log("info", "AGENT", `Agent ${this.options.input.agentId} loaded successfully`);
14197
- return {
14525
+ const customPlugins = claudeConfig.plugins.map((path) => ({
14526
+ type: "local",
14527
+ path
14528
+ }));
14529
+ this.log("info", "AGENT", `Agent ${this.options.input.agentId} loaded successfully (${customPlugins.length} plugins)`);
14530
+ const config = {
14198
14531
  customSystemPrompt: claudeConfig.systemPrompt,
14199
- // Loaded string content
14200
14532
  customModel: claudeConfig.config.model,
14201
14533
  customFallbackModel: claudeConfig.config.fallbackModel,
14202
14534
  customMaxTurns: claudeConfig.config.maxTurns,
14203
14535
  customExtraArgs: claudeConfig.config.extraArgs,
14204
14536
  customPermissionMode: claudeConfig.config.settings?.permissionMode,
14205
- customMcpServers,
14206
14537
  customAllowedTools: claudeConfig.config.settings?.allowedTools || [],
14538
+ customPlugins,
14207
14539
  systemPromptMode: claudeConfig.config.systemPrompt?.mode ?? "append",
14208
- hooks: this.loadedHooks
14209
- // Use cached hooks
14540
+ hooks: this.loadedHooks,
14541
+ customPRPromptTemplate: claudeConfig.prPromptTemplate,
14542
+ prPromptMode: claudeConfig.config.pullRequestPrompt?.mode ?? "append"
14210
14543
  };
14544
+ this.loadedAgentConfig = config;
14545
+ return config;
14211
14546
  } catch (error) {
14212
14547
  this.log("error", "AGENT", `Failed to load agent: ${error instanceof Error ? error.message : String(error)}`);
14213
14548
  return this.createDefaultAgentConfig();
14214
14549
  }
14215
14550
  }
14216
14551
  createDefaultAgentConfig() {
14217
- return {
14552
+ const config = {
14218
14553
  customSystemPrompt: void 0,
14219
14554
  customModel: void 0,
14220
14555
  customMaxTurns: void 0,
14221
- customMcpServers: {},
14222
14556
  customAllowedTools: [],
14223
- systemPromptMode: "append"
14557
+ customPlugins: [],
14558
+ systemPromptMode: "append",
14559
+ customPRPromptTemplate: void 0,
14560
+ prPromptMode: "append"
14224
14561
  };
14562
+ this.loadedAgentConfig = config;
14563
+ return config;
14225
14564
  }
14226
14565
  buildEnvironmentOverrides() {
14227
14566
  return {
@@ -14329,18 +14668,20 @@ URL: ${result.pullRequestUrl}`
14329
14668
  this.timerManager.stopTask("event");
14330
14669
  },
14331
14670
  onTaskMessage: async (message) => {
14332
- if (message.type === "user") {
14671
+ if (isAskUserResponseMessage(message)) {
14672
+ const [eventId, awaiter] = this.askUserAwaiter.entries().next().value || [];
14673
+ if (eventId && awaiter) {
14674
+ this.askUserAwaiter.delete(eventId);
14675
+ awaiter(message);
14676
+ }
14677
+ this.timerManager.clearIdleTimer();
14678
+ return;
14679
+ }
14680
+ if ("type" in message && message.type === "user") {
14333
14681
  const userMessage = message;
14334
14682
  await this.coordinator.enqueue(userMessage);
14335
14683
  this.timerManager.clearIdleTimer();
14336
14684
  }
14337
- },
14338
- onPermissionResponse: async (response) => {
14339
- const awaiter = this.permissionAwaiter.get(response.opCode);
14340
- if (awaiter) {
14341
- this.permissionAwaiter.delete(response.opCode);
14342
- awaiter(response);
14343
- }
14344
14685
  }
14345
14686
  }
14346
14687
  };
@@ -14379,8 +14720,7 @@ URL: ${result.pullRequestUrl}`
14379
14720
  this.log("info", "GIT", `Patch: ${gitStateResult.patchPath}`);
14380
14721
  }
14381
14722
  }
14382
- async createLogger(options) {
14383
- const { createLogger } = await import('./logger-D-ioMWe6.mjs').then(function (n) { return n.b; });
14723
+ createWorkerLogger(options) {
14384
14724
  return createLogger(options);
14385
14725
  }
14386
14726
  log(level, category, message, ...args) {
@@ -16119,6 +16459,7 @@ class CodexWorker {
16119
16459
  };
16120
16460
  this.coordinator = new MessageCoordinator({
16121
16461
  workerType: "codex",
16462
+ workClient,
16122
16463
  handlers: {
16123
16464
  onNormalMessage: async (message) => {
16124
16465
  const input = await this.convertSDKMessageToCodexInput(message);
@@ -16191,7 +16532,6 @@ class CodexWorker {
16191
16532
  }
16192
16533
  );
16193
16534
  this.timerManager.startIdleTimer();
16194
- this.context.workClient.sendWorkerReady();
16195
16535
  this.log("info", "BASH", `Worker ready after command execution (exit code: ${exitCode})`);
16196
16536
  }
16197
16537
  async buildCreatePRPrompt() {
@@ -16248,8 +16588,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16248
16588
  skipGitRepoCheck: true
16249
16589
  });
16250
16590
  }
16251
- this.context.workClient.sendWorkRunning();
16252
- this.coordinator.setWorkerState("processing-sdk");
16591
+ this.coordinator.setWorkerState("running");
16253
16592
  while (!this.isStopping) {
16254
16593
  this.log("debug", "AGENT", `Loop iteration: turnCount=${this.turnCount}, queueLength=${this.messageQueue.length}`);
16255
16594
  let userInput = null;
@@ -16261,7 +16600,6 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16261
16600
  await this.reportGitState("idle");
16262
16601
  this.coordinator.setWorkerState("idle");
16263
16602
  this.timerManager.startIdleTimer();
16264
- this.context.workClient.sendWorkerReady();
16265
16603
  this.log("info", "AGENT", "Sent worker-ready, waiting for next message");
16266
16604
  userInput = await this.waitForNextMessage();
16267
16605
  }
@@ -16271,7 +16609,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16271
16609
  }
16272
16610
  try {
16273
16611
  this.timerManager.clearIdleTimer();
16274
- this.coordinator.setWorkerState("processing-sdk");
16612
+ this.coordinator.setWorkerState("running");
16275
16613
  await this.processUserInput(userInput);
16276
16614
  this.turnCount++;
16277
16615
  this.log("debug", "AGENT", `Message processed, turnCount now ${this.turnCount}`);
@@ -16307,7 +16645,7 @@ ${stats.files.map((f) => ` ${f.path}: +${f.insertions}/-${f.deletions}`).join("
16307
16645
  const turnOptions = this.inMergeRequest ? { outputSchema: getPROutputSchema() } : {};
16308
16646
  this.log("debug", "AGENT", "Calling thread.runStreamed");
16309
16647
  const { events } = await this.thread.runStreamed(input, turnOptions);
16310
- this.context.workClient.sendWorkRunning();
16648
+ this.coordinator.setWorkerState("running");
16311
16649
  this.log("debug", "AGENT", "Starting to process events");
16312
16650
  for await (const event of events) {
16313
16651
  if (this.isStopping) {
@@ -16598,7 +16936,7 @@ Type: ${mimeType}`
16598
16936
  }
16599
16937
  async handleStopHook() {
16600
16938
  await this.reportGitState("stop");
16601
- this.context.workClient.sendWorkerReady();
16939
+ this.coordinator.setWorkerState("idle");
16602
16940
  }
16603
16941
  async loadAgentConfiguration() {
16604
16942
  if (!this.options.input.agentId || this.options.input.agentId === "default") {
@@ -16690,14 +17028,15 @@ Type: ${mimeType}`
16690
17028
  this.timerManager.stopTask("event");
16691
17029
  },
16692
17030
  onTaskMessage: async (message) => {
16693
- if (message.type === "user") {
17031
+ if (isAskUserResponseMessage(message)) {
17032
+ this.log("debug", "AGENT", "Received ask_user_response (not used by Codex)");
17033
+ return;
17034
+ }
17035
+ if ("type" in message && message.type === "user") {
16694
17036
  const userMessage = message;
16695
17037
  await this.coordinator.enqueue(userMessage);
16696
17038
  this.timerManager.clearIdleTimer();
16697
17039
  }
16698
- },
16699
- onPermissionResponse: async (response) => {
16700
- this.log("debug", "AGENT", `Permission response received: ${response.behavior}`);
16701
17040
  }
16702
17041
  }
16703
17042
  };
@@ -17052,7 +17391,7 @@ cli.command("upgrade", "Upgrade CLI to the latest version", {}, async (argv) =>
17052
17391
  }
17053
17392
  }
17054
17393
  try {
17055
- const { version } = await import('./logger-D-ioMWe6.mjs').then(function (n) { return n._; });
17394
+ const { version } = await import('./logger-BzpMLIL-.mjs').then(function (n) { return n._; });
17056
17395
  console.log(chalk.green(`
17057
17396
  \u2713 Now running version: ${version}`));
17058
17397
  } catch {
@@ -17234,7 +17573,7 @@ cli.command(
17234
17573
  },
17235
17574
  async (argv) => {
17236
17575
  try {
17237
- const { testCommand } = await import('./index-D1ipaIV_.mjs');
17576
+ const { testCommand } = await import('./index-DNDJMHW8.mjs');
17238
17577
  await testCommand(argv);
17239
17578
  } catch (error) {
17240
17579
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Test mode failed");