@locusai/cli 0.10.2 → 0.10.5

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.
@@ -14963,10 +14963,12 @@ class ClaudeRunner {
14963
14963
  currentToolName;
14964
14964
  activeTools = new Map;
14965
14965
  activeProcess = null;
14966
- constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
14966
+ timeoutMs;
14967
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log, timeoutMs) {
14967
14968
  this.model = model;
14968
14969
  this.log = log;
14969
14970
  this.projectPath = resolve(projectPath);
14971
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
14970
14972
  }
14971
14973
  setEventEmitter(emitter) {
14972
14974
  this.eventEmitter = emitter;
@@ -14982,11 +14984,14 @@ class ClaudeRunner {
14982
14984
  let lastError = null;
14983
14985
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
14984
14986
  try {
14985
- return await this.executeRun(prompt);
14987
+ return await this.withTimeout(this.executeRun(prompt));
14986
14988
  } catch (error48) {
14987
14989
  const err = error48;
14988
14990
  lastError = err;
14989
14991
  const isLastAttempt = attempt === maxRetries;
14992
+ if (err.message.includes("timed out")) {
14993
+ throw err;
14994
+ }
14990
14995
  if (!isLastAttempt) {
14991
14996
  const delay = Math.pow(2, attempt) * 1000;
14992
14997
  console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
@@ -14996,6 +15001,23 @@ class ClaudeRunner {
14996
15001
  }
14997
15002
  throw lastError || new Error("Claude CLI failed after multiple attempts");
14998
15003
  }
15004
+ withTimeout(promise2) {
15005
+ if (this.timeoutMs <= 0)
15006
+ return promise2;
15007
+ return new Promise((resolve2, reject) => {
15008
+ const timer = setTimeout(() => {
15009
+ this.abort();
15010
+ reject(new Error(`Claude CLI execution timed out after ${Math.round(this.timeoutMs / 60000)} minutes`));
15011
+ }, this.timeoutMs);
15012
+ promise2.then((value) => {
15013
+ clearTimeout(timer);
15014
+ resolve2(value);
15015
+ }, (err) => {
15016
+ clearTimeout(timer);
15017
+ reject(err);
15018
+ });
15019
+ });
15020
+ }
14999
15021
  async* runStream(prompt) {
15000
15022
  const args = [
15001
15023
  "--dangerously-skip-permissions",
@@ -15360,18 +15382,22 @@ ${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
15360
15382
  return new Error(message);
15361
15383
  }
15362
15384
  }
15363
- var SANDBOX_SETTINGS;
15385
+ var SANDBOX_SETTINGS, DEFAULT_TIMEOUT_MS;
15364
15386
  var init_claude_runner = __esm(() => {
15365
15387
  init_config();
15366
15388
  init_colors();
15367
15389
  init_resolve_bin();
15368
15390
  SANDBOX_SETTINGS = JSON.stringify({
15391
+ permissions: {
15392
+ deny: ["Read(../**)", "Edit(../**)"]
15393
+ },
15369
15394
  sandbox: {
15370
15395
  enabled: true,
15371
15396
  autoAllow: true,
15372
15397
  allowUnsandboxedCommands: false
15373
15398
  }
15374
15399
  });
15400
+ DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
15375
15401
  });
15376
15402
 
15377
15403
  // ../sdk/src/ai/codex-runner.ts
@@ -15386,10 +15412,17 @@ class CodexRunner {
15386
15412
  model;
15387
15413
  log;
15388
15414
  activeProcess = null;
15389
- constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
15415
+ eventEmitter;
15416
+ currentToolName;
15417
+ timeoutMs;
15418
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log, timeoutMs) {
15390
15419
  this.projectPath = projectPath;
15391
15420
  this.model = model;
15392
15421
  this.log = log;
15422
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS2;
15423
+ }
15424
+ setEventEmitter(emitter) {
15425
+ this.eventEmitter = emitter;
15393
15426
  }
15394
15427
  abort() {
15395
15428
  if (this.activeProcess && !this.activeProcess.killed) {
@@ -15402,9 +15435,12 @@ class CodexRunner {
15402
15435
  let lastError = null;
15403
15436
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
15404
15437
  try {
15405
- return await this.executeRun(prompt);
15438
+ return await this.withTimeout(this.executeRun(prompt));
15406
15439
  } catch (error48) {
15407
15440
  lastError = error48;
15441
+ if (lastError.message.includes("timed out")) {
15442
+ throw lastError;
15443
+ }
15408
15444
  if (attempt < maxRetries) {
15409
15445
  const delay = Math.pow(2, attempt) * 1000;
15410
15446
  console.warn(`Codex CLI attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
@@ -15414,9 +15450,31 @@ class CodexRunner {
15414
15450
  }
15415
15451
  throw lastError || new Error("Codex CLI failed after multiple attempts");
15416
15452
  }
15453
+ withTimeout(promise2) {
15454
+ if (this.timeoutMs <= 0)
15455
+ return promise2;
15456
+ return new Promise((resolve2, reject) => {
15457
+ const timer = setTimeout(() => {
15458
+ this.abort();
15459
+ reject(new Error(`Codex CLI execution timed out after ${Math.round(this.timeoutMs / 60000)} minutes`));
15460
+ }, this.timeoutMs);
15461
+ promise2.then((value) => {
15462
+ clearTimeout(timer);
15463
+ resolve2(value);
15464
+ }, (err) => {
15465
+ clearTimeout(timer);
15466
+ reject(err);
15467
+ });
15468
+ });
15469
+ }
15417
15470
  async* runStream(prompt) {
15418
15471
  const outputPath = join3(tmpdir(), `locus-codex-${randomUUID()}.txt`);
15419
15472
  const args = this.buildArgs(outputPath);
15473
+ this.eventEmitter?.emitSessionStarted({
15474
+ model: this.model,
15475
+ provider: "codex"
15476
+ });
15477
+ this.eventEmitter?.emitPromptSubmitted(prompt, prompt.length > 500);
15420
15478
  const codex = spawn2("codex", args, {
15421
15479
  cwd: this.projectPath,
15422
15480
  stdio: ["pipe", "pipe", "pipe"],
@@ -15429,7 +15487,21 @@ class CodexRunner {
15429
15487
  let processEnded = false;
15430
15488
  let errorMessage = "";
15431
15489
  let finalOutput = "";
15490
+ let finalContent = "";
15491
+ let isThinking = false;
15432
15492
  const enqueueChunk = (chunk) => {
15493
+ this.emitEventForChunk(chunk, isThinking);
15494
+ if (chunk.type === "thinking") {
15495
+ isThinking = true;
15496
+ } else if (chunk.type === "text_delta" || chunk.type === "tool_use") {
15497
+ if (isThinking) {
15498
+ this.eventEmitter?.emitThinkingStoped();
15499
+ isThinking = false;
15500
+ }
15501
+ }
15502
+ if (chunk.type === "text_delta") {
15503
+ finalContent += chunk.content;
15504
+ }
15433
15505
  if (resolveChunk) {
15434
15506
  const resolve2 = resolveChunk;
15435
15507
  resolveChunk = null;
@@ -15470,16 +15542,21 @@ class CodexRunner {
15470
15542
  codex.stderr.on("data", processOutput);
15471
15543
  codex.on("error", (err) => {
15472
15544
  errorMessage = `Failed to start Codex CLI: ${err.message}. Ensure 'codex' is installed and available in PATH.`;
15545
+ this.eventEmitter?.emitErrorOccurred(errorMessage, "SPAWN_ERROR");
15473
15546
  signalEnd();
15474
15547
  });
15475
15548
  codex.on("close", (code) => {
15476
15549
  this.activeProcess = null;
15477
- this.cleanupTempFile(outputPath);
15478
15550
  if (code === 0) {
15479
15551
  const result = this.readOutput(outputPath, finalOutput);
15552
+ this.cleanupTempFile(outputPath);
15480
15553
  enqueueChunk({ type: "result", content: result });
15481
- } else if (!errorMessage) {
15482
- errorMessage = this.createErrorFromOutput(code, finalOutput).message;
15554
+ } else {
15555
+ this.cleanupTempFile(outputPath);
15556
+ if (!errorMessage) {
15557
+ errorMessage = this.createErrorFromOutput(code, finalOutput).message;
15558
+ this.eventEmitter?.emitErrorOccurred(errorMessage, `EXIT_${code}`);
15559
+ }
15483
15560
  }
15484
15561
  signalEnd();
15485
15562
  });
@@ -15493,6 +15570,12 @@ class CodexRunner {
15493
15570
  } else if (processEnded) {
15494
15571
  if (errorMessage) {
15495
15572
  yield { type: "error", error: errorMessage };
15573
+ this.eventEmitter?.emitSessionEnded(false);
15574
+ } else {
15575
+ if (finalContent) {
15576
+ this.eventEmitter?.emitResponseCompleted(finalContent);
15577
+ }
15578
+ this.eventEmitter?.emitSessionEnded(true);
15496
15579
  }
15497
15580
  break;
15498
15581
  } else {
@@ -15502,6 +15585,12 @@ class CodexRunner {
15502
15585
  if (chunk === null) {
15503
15586
  if (errorMessage) {
15504
15587
  yield { type: "error", error: errorMessage };
15588
+ this.eventEmitter?.emitSessionEnded(false);
15589
+ } else {
15590
+ if (finalContent) {
15591
+ this.eventEmitter?.emitResponseCompleted(finalContent);
15592
+ }
15593
+ this.eventEmitter?.emitSessionEnded(true);
15505
15594
  }
15506
15595
  break;
15507
15596
  }
@@ -15509,6 +15598,36 @@ class CodexRunner {
15509
15598
  }
15510
15599
  }
15511
15600
  }
15601
+ emitEventForChunk(chunk, isThinking) {
15602
+ if (!this.eventEmitter)
15603
+ return;
15604
+ switch (chunk.type) {
15605
+ case "text_delta":
15606
+ this.eventEmitter.emitTextDelta(chunk.content);
15607
+ break;
15608
+ case "tool_use":
15609
+ if (this.currentToolName) {
15610
+ this.eventEmitter.emitToolCompleted(this.currentToolName);
15611
+ }
15612
+ this.currentToolName = chunk.tool;
15613
+ this.eventEmitter.emitToolStarted(chunk.tool);
15614
+ break;
15615
+ case "thinking":
15616
+ if (!isThinking) {
15617
+ this.eventEmitter.emitThinkingStarted(chunk.content);
15618
+ }
15619
+ break;
15620
+ case "result":
15621
+ if (this.currentToolName) {
15622
+ this.eventEmitter.emitToolCompleted(this.currentToolName);
15623
+ this.currentToolName = undefined;
15624
+ }
15625
+ break;
15626
+ case "error":
15627
+ this.eventEmitter.emitErrorOccurred(chunk.error);
15628
+ break;
15629
+ }
15630
+ }
15512
15631
  executeRun(prompt) {
15513
15632
  return new Promise((resolve2, reject) => {
15514
15633
  const outputPath = join3(tmpdir(), `locus-codex-${randomUUID()}.txt`);
@@ -15538,10 +15657,12 @@ class CodexRunner {
15538
15657
  });
15539
15658
  codex.on("close", (code) => {
15540
15659
  this.activeProcess = null;
15541
- this.cleanupTempFile(outputPath);
15542
15660
  if (code === 0) {
15543
- resolve2(this.readOutput(outputPath, output));
15661
+ const result = this.readOutput(outputPath, output);
15662
+ this.cleanupTempFile(outputPath);
15663
+ resolve2(result);
15544
15664
  } else {
15665
+ this.cleanupTempFile(outputPath);
15545
15666
  reject(this.createErrorFromOutput(code, errorOutput));
15546
15667
  }
15547
15668
  });
@@ -15551,12 +15672,18 @@ class CodexRunner {
15551
15672
  }
15552
15673
  buildArgs(outputPath) {
15553
15674
  const args = [
15675
+ "--ask-for-approval",
15676
+ "never",
15554
15677
  "exec",
15555
15678
  "--sandbox",
15556
15679
  "workspace-write",
15557
15680
  "--skip-git-repo-check",
15558
15681
  "--output-last-message",
15559
- outputPath
15682
+ outputPath,
15683
+ "-c",
15684
+ "sandbox_workspace_write.network_access=true",
15685
+ "-c",
15686
+ 'sandbox.excludedCommands=["git", "gh"]'
15560
15687
  ];
15561
15688
  if (this.model) {
15562
15689
  args.push("--model", this.model);
@@ -15607,9 +15734,11 @@ class CodexRunner {
15607
15734
  return new Promise((resolve2) => setTimeout(resolve2, ms));
15608
15735
  }
15609
15736
  }
15737
+ var DEFAULT_TIMEOUT_MS2;
15610
15738
  var init_codex_runner = __esm(() => {
15611
15739
  init_config();
15612
15740
  init_resolve_bin();
15741
+ DEFAULT_TIMEOUT_MS2 = 60 * 60 * 1000;
15613
15742
  });
15614
15743
 
15615
15744
  // ../sdk/src/ai/factory.ts
@@ -15618,9 +15747,9 @@ function createAiRunner(provider, config2) {
15618
15747
  const model = config2.model ?? DEFAULT_MODEL[resolvedProvider];
15619
15748
  switch (resolvedProvider) {
15620
15749
  case PROVIDER.CODEX:
15621
- return new CodexRunner(config2.projectPath, model, config2.log);
15750
+ return new CodexRunner(config2.projectPath, model, config2.log, config2.timeoutMs);
15622
15751
  default:
15623
- return new ClaudeRunner(config2.projectPath, model, config2.log);
15752
+ return new ClaudeRunner(config2.projectPath, model, config2.log, config2.timeoutMs);
15624
15753
  }
15625
15754
  }
15626
15755
  var init_factory = __esm(() => {
@@ -31580,8 +31709,9 @@ class WorktreeManager {
31580
31709
  this.ensureDirectory(this.rootPath, "Worktree root");
31581
31710
  addWorktree();
31582
31711
  }
31583
- this.log(`Worktree created at ${worktreePath}`, "success");
31584
- return { worktreePath, branch, baseBranch };
31712
+ const baseCommitHash = this.git("rev-parse HEAD", worktreePath).trim();
31713
+ this.log(`Worktree created at ${worktreePath} (base: ${baseCommitHash.slice(0, 8)})`, "success");
31714
+ return { worktreePath, branch, baseBranch, baseCommitHash };
31585
31715
  }
31586
31716
  list() {
31587
31717
  const output = this.git("worktree list --porcelain", this.projectPath);
@@ -31686,27 +31816,54 @@ class WorktreeManager {
31686
31816
  try {
31687
31817
  const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
31688
31818
  return Number.parseInt(count, 10) > 0;
31819
+ } catch (err) {
31820
+ this.log(`Could not compare HEAD against base branch "${baseBranch}": ${err instanceof Error ? err.message : String(err)}`, "warn");
31821
+ return false;
31822
+ }
31823
+ }
31824
+ hasCommitsAheadOfHash(worktreePath, baseHash) {
31825
+ try {
31826
+ const headHash = this.git("rev-parse HEAD", worktreePath).trim();
31827
+ return headHash !== baseHash;
31689
31828
  } catch {
31690
31829
  return false;
31691
31830
  }
31692
31831
  }
31693
- commitChanges(worktreePath, message, baseBranch) {
31832
+ commitChanges(worktreePath, message, baseBranch, baseCommitHash) {
31694
31833
  const hasUncommittedChanges = this.hasChanges(worktreePath);
31834
+ if (hasUncommittedChanges) {
31835
+ const statusOutput = this.git("status --porcelain", worktreePath).trim();
31836
+ this.log(`Detected uncommitted changes:
31837
+ ${statusOutput.split(`
31838
+ `).slice(0, 10).join(`
31839
+ `)}${statusOutput.split(`
31840
+ `).length > 10 ? `
31841
+ ... and ${statusOutput.split(`
31842
+ `).length - 10} more` : ""}`, "info");
31843
+ }
31695
31844
  if (!hasUncommittedChanges) {
31696
31845
  if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
31697
31846
  const hash3 = this.git("rev-parse HEAD", worktreePath).trim();
31698
31847
  this.log(`Agent already committed changes (${hash3.slice(0, 8)}); skipping additional commit`, "info");
31699
31848
  return hash3;
31700
31849
  }
31701
- this.log("No changes to commit", "info");
31850
+ if (baseCommitHash && this.hasCommitsAheadOfHash(worktreePath, baseCommitHash)) {
31851
+ const hash3 = this.git("rev-parse HEAD", worktreePath).trim();
31852
+ this.log(`Agent already committed changes (${hash3.slice(0, 8)}, detected via base commit hash); skipping additional commit`, "info");
31853
+ return hash3;
31854
+ }
31855
+ const branch = this.getBranch(worktreePath);
31856
+ this.log(`No changes detected in worktree (branch: ${branch}, baseBranch: ${baseBranch ?? "none"}, baseCommitHash: ${baseCommitHash?.slice(0, 8) ?? "none"})`, "warn");
31702
31857
  return null;
31703
31858
  }
31704
31859
  this.git("add -A", worktreePath);
31705
31860
  const staged = this.git("diff --cached --name-only", worktreePath).trim();
31706
31861
  if (!staged) {
31707
- this.log("No changes to commit", "info");
31862
+ this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
31708
31863
  return null;
31709
31864
  }
31865
+ this.log(`Staging ${staged.split(`
31866
+ `).length} file(s) for commit`, "info");
31710
31867
  this.gitExec(["commit", "-m", message], worktreePath);
31711
31868
  const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
31712
31869
  this.log(`Committed: ${hash2.slice(0, 8)}`, "success");
@@ -32124,15 +32281,27 @@ class TaskExecutor {
32124
32281
  const basePrompt = await this.promptBuilder.build(task2);
32125
32282
  try {
32126
32283
  this.deps.log("Starting Execution...", "info");
32127
- await this.deps.aiRunner.run(basePrompt);
32128
- return {
32129
- success: true,
32130
- summary: "Task completed by the agent"
32131
- };
32284
+ const output = await this.deps.aiRunner.run(basePrompt);
32285
+ const summary = this.extractSummary(output);
32286
+ return { success: true, summary };
32132
32287
  } catch (error48) {
32133
32288
  return { success: false, summary: `Error: ${error48}` };
32134
32289
  }
32135
32290
  }
32291
+ extractSummary(output) {
32292
+ if (!output || !output.trim()) {
32293
+ return "Task completed by the agent";
32294
+ }
32295
+ const paragraphs = output.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
32296
+ if (paragraphs.length === 0) {
32297
+ return "Task completed by the agent";
32298
+ }
32299
+ const last = paragraphs[paragraphs.length - 1];
32300
+ if (last.length > 500) {
32301
+ return `${last.slice(0, 497)}...`;
32302
+ }
32303
+ return last;
32304
+ }
32136
32305
  }
32137
32306
  var init_task_executor = __esm(() => {
32138
32307
  init_prompt_builder();
@@ -32150,7 +32319,7 @@ class GitWorkflow {
32150
32319
  this.log = log;
32151
32320
  this.ghUsername = ghUsername;
32152
32321
  const projectPath = config2.projectPath || process.cwd();
32153
- this.worktreeManager = config2.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }) : null;
32322
+ this.worktreeManager = config2.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }, log) : null;
32154
32323
  this.prService = config2.autoPush ? new PrService(projectPath, log) : null;
32155
32324
  }
32156
32325
  createTaskWorktree(task2, defaultExecutor) {
@@ -32158,6 +32327,7 @@ class GitWorkflow {
32158
32327
  return {
32159
32328
  worktreePath: null,
32160
32329
  baseBranch: null,
32330
+ baseCommitHash: null,
32161
32331
  executor: defaultExecutor
32162
32332
  };
32163
32333
  }
@@ -32183,10 +32353,11 @@ class GitWorkflow {
32183
32353
  return {
32184
32354
  worktreePath: result.worktreePath,
32185
32355
  baseBranch: result.baseBranch,
32356
+ baseCommitHash: result.baseCommitHash,
32186
32357
  executor: taskExecutor
32187
32358
  };
32188
32359
  }
32189
- commitAndPush(worktreePath, task2, baseBranch) {
32360
+ commitAndPush(worktreePath, task2, baseBranch, baseCommitHash) {
32190
32361
  if (!this.worktreeManager) {
32191
32362
  return { branch: null, pushed: false, pushFailed: false };
32192
32363
  }
@@ -32203,7 +32374,7 @@ class GitWorkflow {
32203
32374
 
32204
32375
  ${trailers.join(`
32205
32376
  `)}`;
32206
- const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch);
32377
+ const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch, baseCommitHash);
32207
32378
  if (!hash2) {
32208
32379
  this.log("No changes to commit for this task", "info");
32209
32380
  return {
@@ -32243,7 +32414,12 @@ ${trailers.join(`
32243
32414
  } catch (err) {
32244
32415
  const errorMessage = err instanceof Error ? err.message : String(err);
32245
32416
  this.log(`Git commit failed: ${errorMessage}`, "error");
32246
- return { branch: null, pushed: false, pushFailed: false };
32417
+ return {
32418
+ branch: null,
32419
+ pushed: false,
32420
+ pushFailed: true,
32421
+ pushError: `Git commit/push failed: ${errorMessage}`
32422
+ };
32247
32423
  }
32248
32424
  }
32249
32425
  createPullRequest(task2, branch, summary, baseBranch) {
@@ -32469,7 +32645,7 @@ class AgentWorker {
32469
32645
  }
32470
32646
  async executeTask(task2) {
32471
32647
  const fullTask = await this.client.tasks.getById(task2.id, this.config.workspaceId);
32472
- const { worktreePath, baseBranch, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
32648
+ const { worktreePath, baseBranch, baseCommitHash, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
32473
32649
  this.currentWorktreePath = worktreePath;
32474
32650
  let branchPushed = false;
32475
32651
  let keepBranch = false;
@@ -32481,7 +32657,7 @@ class AgentWorker {
32481
32657
  let prError = null;
32482
32658
  let noChanges = false;
32483
32659
  if (result.success && worktreePath) {
32484
- const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined);
32660
+ const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined, baseCommitHash ?? undefined);
32485
32661
  taskBranch = commitResult.branch;
32486
32662
  branchPushed = commitResult.pushed;
32487
32663
  keepBranch = taskBranch !== null;
package/bin/locus.js CHANGED
@@ -6663,10 +6663,12 @@ class ClaudeRunner {
6663
6663
  currentToolName;
6664
6664
  activeTools = new Map;
6665
6665
  activeProcess = null;
6666
- constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
6666
+ timeoutMs;
6667
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log, timeoutMs) {
6667
6668
  this.model = model;
6668
6669
  this.log = log;
6669
6670
  this.projectPath = resolve(projectPath);
6671
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
6670
6672
  }
6671
6673
  setEventEmitter(emitter) {
6672
6674
  this.eventEmitter = emitter;
@@ -6682,11 +6684,14 @@ class ClaudeRunner {
6682
6684
  let lastError = null;
6683
6685
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
6684
6686
  try {
6685
- return await this.executeRun(prompt);
6687
+ return await this.withTimeout(this.executeRun(prompt));
6686
6688
  } catch (error) {
6687
6689
  const err = error;
6688
6690
  lastError = err;
6689
6691
  const isLastAttempt = attempt === maxRetries;
6692
+ if (err.message.includes("timed out")) {
6693
+ throw err;
6694
+ }
6690
6695
  if (!isLastAttempt) {
6691
6696
  const delay = Math.pow(2, attempt) * 1000;
6692
6697
  console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
@@ -6696,6 +6701,23 @@ class ClaudeRunner {
6696
6701
  }
6697
6702
  throw lastError || new Error("Claude CLI failed after multiple attempts");
6698
6703
  }
6704
+ withTimeout(promise) {
6705
+ if (this.timeoutMs <= 0)
6706
+ return promise;
6707
+ return new Promise((resolve2, reject) => {
6708
+ const timer = setTimeout(() => {
6709
+ this.abort();
6710
+ reject(new Error(`Claude CLI execution timed out after ${Math.round(this.timeoutMs / 60000)} minutes`));
6711
+ }, this.timeoutMs);
6712
+ promise.then((value) => {
6713
+ clearTimeout(timer);
6714
+ resolve2(value);
6715
+ }, (err) => {
6716
+ clearTimeout(timer);
6717
+ reject(err);
6718
+ });
6719
+ });
6720
+ }
6699
6721
  async* runStream(prompt) {
6700
6722
  const args = [
6701
6723
  "--dangerously-skip-permissions",
@@ -7060,18 +7082,22 @@ ${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
7060
7082
  return new Error(message);
7061
7083
  }
7062
7084
  }
7063
- var SANDBOX_SETTINGS;
7085
+ var SANDBOX_SETTINGS, DEFAULT_TIMEOUT_MS;
7064
7086
  var init_claude_runner = __esm(() => {
7065
7087
  init_config();
7066
7088
  init_colors();
7067
7089
  init_resolve_bin();
7068
7090
  SANDBOX_SETTINGS = JSON.stringify({
7091
+ permissions: {
7092
+ deny: ["Read(../**)", "Edit(../**)"]
7093
+ },
7069
7094
  sandbox: {
7070
7095
  enabled: true,
7071
7096
  autoAllow: true,
7072
7097
  allowUnsandboxedCommands: false
7073
7098
  }
7074
7099
  });
7100
+ DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
7075
7101
  });
7076
7102
 
7077
7103
  // ../sdk/src/ai/codex-runner.ts
@@ -7086,10 +7112,17 @@ class CodexRunner {
7086
7112
  model;
7087
7113
  log;
7088
7114
  activeProcess = null;
7089
- constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
7115
+ eventEmitter;
7116
+ currentToolName;
7117
+ timeoutMs;
7118
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log, timeoutMs) {
7090
7119
  this.projectPath = projectPath;
7091
7120
  this.model = model;
7092
7121
  this.log = log;
7122
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS2;
7123
+ }
7124
+ setEventEmitter(emitter) {
7125
+ this.eventEmitter = emitter;
7093
7126
  }
7094
7127
  abort() {
7095
7128
  if (this.activeProcess && !this.activeProcess.killed) {
@@ -7102,9 +7135,12 @@ class CodexRunner {
7102
7135
  let lastError = null;
7103
7136
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
7104
7137
  try {
7105
- return await this.executeRun(prompt);
7138
+ return await this.withTimeout(this.executeRun(prompt));
7106
7139
  } catch (error) {
7107
7140
  lastError = error;
7141
+ if (lastError.message.includes("timed out")) {
7142
+ throw lastError;
7143
+ }
7108
7144
  if (attempt < maxRetries) {
7109
7145
  const delay = Math.pow(2, attempt) * 1000;
7110
7146
  console.warn(`Codex CLI attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
@@ -7114,9 +7150,31 @@ class CodexRunner {
7114
7150
  }
7115
7151
  throw lastError || new Error("Codex CLI failed after multiple attempts");
7116
7152
  }
7153
+ withTimeout(promise) {
7154
+ if (this.timeoutMs <= 0)
7155
+ return promise;
7156
+ return new Promise((resolve2, reject) => {
7157
+ const timer = setTimeout(() => {
7158
+ this.abort();
7159
+ reject(new Error(`Codex CLI execution timed out after ${Math.round(this.timeoutMs / 60000)} minutes`));
7160
+ }, this.timeoutMs);
7161
+ promise.then((value) => {
7162
+ clearTimeout(timer);
7163
+ resolve2(value);
7164
+ }, (err) => {
7165
+ clearTimeout(timer);
7166
+ reject(err);
7167
+ });
7168
+ });
7169
+ }
7117
7170
  async* runStream(prompt) {
7118
7171
  const outputPath = join5(tmpdir(), `locus-codex-${randomUUID()}.txt`);
7119
7172
  const args = this.buildArgs(outputPath);
7173
+ this.eventEmitter?.emitSessionStarted({
7174
+ model: this.model,
7175
+ provider: "codex"
7176
+ });
7177
+ this.eventEmitter?.emitPromptSubmitted(prompt, prompt.length > 500);
7120
7178
  const codex = spawn2("codex", args, {
7121
7179
  cwd: this.projectPath,
7122
7180
  stdio: ["pipe", "pipe", "pipe"],
@@ -7129,7 +7187,21 @@ class CodexRunner {
7129
7187
  let processEnded = false;
7130
7188
  let errorMessage = "";
7131
7189
  let finalOutput = "";
7190
+ let finalContent = "";
7191
+ let isThinking = false;
7132
7192
  const enqueueChunk = (chunk) => {
7193
+ this.emitEventForChunk(chunk, isThinking);
7194
+ if (chunk.type === "thinking") {
7195
+ isThinking = true;
7196
+ } else if (chunk.type === "text_delta" || chunk.type === "tool_use") {
7197
+ if (isThinking) {
7198
+ this.eventEmitter?.emitThinkingStoped();
7199
+ isThinking = false;
7200
+ }
7201
+ }
7202
+ if (chunk.type === "text_delta") {
7203
+ finalContent += chunk.content;
7204
+ }
7133
7205
  if (resolveChunk) {
7134
7206
  const resolve2 = resolveChunk;
7135
7207
  resolveChunk = null;
@@ -7170,16 +7242,21 @@ class CodexRunner {
7170
7242
  codex.stderr.on("data", processOutput);
7171
7243
  codex.on("error", (err) => {
7172
7244
  errorMessage = `Failed to start Codex CLI: ${err.message}. Ensure 'codex' is installed and available in PATH.`;
7245
+ this.eventEmitter?.emitErrorOccurred(errorMessage, "SPAWN_ERROR");
7173
7246
  signalEnd();
7174
7247
  });
7175
7248
  codex.on("close", (code) => {
7176
7249
  this.activeProcess = null;
7177
- this.cleanupTempFile(outputPath);
7178
7250
  if (code === 0) {
7179
7251
  const result = this.readOutput(outputPath, finalOutput);
7252
+ this.cleanupTempFile(outputPath);
7180
7253
  enqueueChunk({ type: "result", content: result });
7181
- } else if (!errorMessage) {
7182
- errorMessage = this.createErrorFromOutput(code, finalOutput).message;
7254
+ } else {
7255
+ this.cleanupTempFile(outputPath);
7256
+ if (!errorMessage) {
7257
+ errorMessage = this.createErrorFromOutput(code, finalOutput).message;
7258
+ this.eventEmitter?.emitErrorOccurred(errorMessage, `EXIT_${code}`);
7259
+ }
7183
7260
  }
7184
7261
  signalEnd();
7185
7262
  });
@@ -7193,6 +7270,12 @@ class CodexRunner {
7193
7270
  } else if (processEnded) {
7194
7271
  if (errorMessage) {
7195
7272
  yield { type: "error", error: errorMessage };
7273
+ this.eventEmitter?.emitSessionEnded(false);
7274
+ } else {
7275
+ if (finalContent) {
7276
+ this.eventEmitter?.emitResponseCompleted(finalContent);
7277
+ }
7278
+ this.eventEmitter?.emitSessionEnded(true);
7196
7279
  }
7197
7280
  break;
7198
7281
  } else {
@@ -7202,6 +7285,12 @@ class CodexRunner {
7202
7285
  if (chunk === null) {
7203
7286
  if (errorMessage) {
7204
7287
  yield { type: "error", error: errorMessage };
7288
+ this.eventEmitter?.emitSessionEnded(false);
7289
+ } else {
7290
+ if (finalContent) {
7291
+ this.eventEmitter?.emitResponseCompleted(finalContent);
7292
+ }
7293
+ this.eventEmitter?.emitSessionEnded(true);
7205
7294
  }
7206
7295
  break;
7207
7296
  }
@@ -7209,6 +7298,36 @@ class CodexRunner {
7209
7298
  }
7210
7299
  }
7211
7300
  }
7301
+ emitEventForChunk(chunk, isThinking) {
7302
+ if (!this.eventEmitter)
7303
+ return;
7304
+ switch (chunk.type) {
7305
+ case "text_delta":
7306
+ this.eventEmitter.emitTextDelta(chunk.content);
7307
+ break;
7308
+ case "tool_use":
7309
+ if (this.currentToolName) {
7310
+ this.eventEmitter.emitToolCompleted(this.currentToolName);
7311
+ }
7312
+ this.currentToolName = chunk.tool;
7313
+ this.eventEmitter.emitToolStarted(chunk.tool);
7314
+ break;
7315
+ case "thinking":
7316
+ if (!isThinking) {
7317
+ this.eventEmitter.emitThinkingStarted(chunk.content);
7318
+ }
7319
+ break;
7320
+ case "result":
7321
+ if (this.currentToolName) {
7322
+ this.eventEmitter.emitToolCompleted(this.currentToolName);
7323
+ this.currentToolName = undefined;
7324
+ }
7325
+ break;
7326
+ case "error":
7327
+ this.eventEmitter.emitErrorOccurred(chunk.error);
7328
+ break;
7329
+ }
7330
+ }
7212
7331
  executeRun(prompt) {
7213
7332
  return new Promise((resolve2, reject) => {
7214
7333
  const outputPath = join5(tmpdir(), `locus-codex-${randomUUID()}.txt`);
@@ -7238,10 +7357,12 @@ class CodexRunner {
7238
7357
  });
7239
7358
  codex.on("close", (code) => {
7240
7359
  this.activeProcess = null;
7241
- this.cleanupTempFile(outputPath);
7242
7360
  if (code === 0) {
7243
- resolve2(this.readOutput(outputPath, output));
7361
+ const result = this.readOutput(outputPath, output);
7362
+ this.cleanupTempFile(outputPath);
7363
+ resolve2(result);
7244
7364
  } else {
7365
+ this.cleanupTempFile(outputPath);
7245
7366
  reject(this.createErrorFromOutput(code, errorOutput));
7246
7367
  }
7247
7368
  });
@@ -7251,12 +7372,18 @@ class CodexRunner {
7251
7372
  }
7252
7373
  buildArgs(outputPath) {
7253
7374
  const args = [
7375
+ "--ask-for-approval",
7376
+ "never",
7254
7377
  "exec",
7255
7378
  "--sandbox",
7256
7379
  "workspace-write",
7257
7380
  "--skip-git-repo-check",
7258
7381
  "--output-last-message",
7259
- outputPath
7382
+ outputPath,
7383
+ "-c",
7384
+ "sandbox_workspace_write.network_access=true",
7385
+ "-c",
7386
+ 'sandbox.excludedCommands=["git", "gh"]'
7260
7387
  ];
7261
7388
  if (this.model) {
7262
7389
  args.push("--model", this.model);
@@ -7307,9 +7434,11 @@ class CodexRunner {
7307
7434
  return new Promise((resolve2) => setTimeout(resolve2, ms));
7308
7435
  }
7309
7436
  }
7437
+ var DEFAULT_TIMEOUT_MS2;
7310
7438
  var init_codex_runner = __esm(() => {
7311
7439
  init_config();
7312
7440
  init_resolve_bin();
7441
+ DEFAULT_TIMEOUT_MS2 = 60 * 60 * 1000;
7313
7442
  });
7314
7443
 
7315
7444
  // ../sdk/src/ai/factory.ts
@@ -7318,9 +7447,9 @@ function createAiRunner(provider, config) {
7318
7447
  const model = config.model ?? DEFAULT_MODEL[resolvedProvider];
7319
7448
  switch (resolvedProvider) {
7320
7449
  case PROVIDER.CODEX:
7321
- return new CodexRunner(config.projectPath, model, config.log);
7450
+ return new CodexRunner(config.projectPath, model, config.log, config.timeoutMs);
7322
7451
  default:
7323
- return new ClaudeRunner(config.projectPath, model, config.log);
7452
+ return new ClaudeRunner(config.projectPath, model, config.log, config.timeoutMs);
7324
7453
  }
7325
7454
  }
7326
7455
  var init_factory = __esm(() => {
@@ -7747,8 +7876,9 @@ class WorktreeManager {
7747
7876
  this.ensureDirectory(this.rootPath, "Worktree root");
7748
7877
  addWorktree();
7749
7878
  }
7750
- this.log(`Worktree created at ${worktreePath}`, "success");
7751
- return { worktreePath, branch, baseBranch };
7879
+ const baseCommitHash = this.git("rev-parse HEAD", worktreePath).trim();
7880
+ this.log(`Worktree created at ${worktreePath} (base: ${baseCommitHash.slice(0, 8)})`, "success");
7881
+ return { worktreePath, branch, baseBranch, baseCommitHash };
7752
7882
  }
7753
7883
  list() {
7754
7884
  const output = this.git("worktree list --porcelain", this.projectPath);
@@ -7853,27 +7983,54 @@ class WorktreeManager {
7853
7983
  try {
7854
7984
  const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
7855
7985
  return Number.parseInt(count, 10) > 0;
7986
+ } catch (err) {
7987
+ this.log(`Could not compare HEAD against base branch "${baseBranch}": ${err instanceof Error ? err.message : String(err)}`, "warn");
7988
+ return false;
7989
+ }
7990
+ }
7991
+ hasCommitsAheadOfHash(worktreePath, baseHash) {
7992
+ try {
7993
+ const headHash = this.git("rev-parse HEAD", worktreePath).trim();
7994
+ return headHash !== baseHash;
7856
7995
  } catch {
7857
7996
  return false;
7858
7997
  }
7859
7998
  }
7860
- commitChanges(worktreePath, message, baseBranch) {
7999
+ commitChanges(worktreePath, message, baseBranch, baseCommitHash) {
7861
8000
  const hasUncommittedChanges = this.hasChanges(worktreePath);
8001
+ if (hasUncommittedChanges) {
8002
+ const statusOutput = this.git("status --porcelain", worktreePath).trim();
8003
+ this.log(`Detected uncommitted changes:
8004
+ ${statusOutput.split(`
8005
+ `).slice(0, 10).join(`
8006
+ `)}${statusOutput.split(`
8007
+ `).length > 10 ? `
8008
+ ... and ${statusOutput.split(`
8009
+ `).length - 10} more` : ""}`, "info");
8010
+ }
7862
8011
  if (!hasUncommittedChanges) {
7863
8012
  if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
7864
8013
  const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
7865
8014
  this.log(`Agent already committed changes (${hash2.slice(0, 8)}); skipping additional commit`, "info");
7866
8015
  return hash2;
7867
8016
  }
7868
- this.log("No changes to commit", "info");
8017
+ if (baseCommitHash && this.hasCommitsAheadOfHash(worktreePath, baseCommitHash)) {
8018
+ const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
8019
+ this.log(`Agent already committed changes (${hash2.slice(0, 8)}, detected via base commit hash); skipping additional commit`, "info");
8020
+ return hash2;
8021
+ }
8022
+ const branch = this.getBranch(worktreePath);
8023
+ this.log(`No changes detected in worktree (branch: ${branch}, baseBranch: ${baseBranch ?? "none"}, baseCommitHash: ${baseCommitHash?.slice(0, 8) ?? "none"})`, "warn");
7869
8024
  return null;
7870
8025
  }
7871
8026
  this.git("add -A", worktreePath);
7872
8027
  const staged = this.git("diff --cached --name-only", worktreePath).trim();
7873
8028
  if (!staged) {
7874
- this.log("No changes to commit", "info");
8029
+ this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
7875
8030
  return null;
7876
8031
  }
8032
+ this.log(`Staging ${staged.split(`
8033
+ `).length} file(s) for commit`, "info");
7877
8034
  this.gitExec(["commit", "-m", message], worktreePath);
7878
8035
  const hash = this.git("rev-parse HEAD", worktreePath).trim();
7879
8036
  this.log(`Committed: ${hash.slice(0, 8)}`, "success");
@@ -23004,15 +23161,27 @@ class TaskExecutor {
23004
23161
  const basePrompt = await this.promptBuilder.build(task2);
23005
23162
  try {
23006
23163
  this.deps.log("Starting Execution...", "info");
23007
- await this.deps.aiRunner.run(basePrompt);
23008
- return {
23009
- success: true,
23010
- summary: "Task completed by the agent"
23011
- };
23164
+ const output = await this.deps.aiRunner.run(basePrompt);
23165
+ const summary = this.extractSummary(output);
23166
+ return { success: true, summary };
23012
23167
  } catch (error48) {
23013
23168
  return { success: false, summary: `Error: ${error48}` };
23014
23169
  }
23015
23170
  }
23171
+ extractSummary(output) {
23172
+ if (!output || !output.trim()) {
23173
+ return "Task completed by the agent";
23174
+ }
23175
+ const paragraphs = output.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
23176
+ if (paragraphs.length === 0) {
23177
+ return "Task completed by the agent";
23178
+ }
23179
+ const last = paragraphs[paragraphs.length - 1];
23180
+ if (last.length > 500) {
23181
+ return `${last.slice(0, 497)}...`;
23182
+ }
23183
+ return last;
23184
+ }
23016
23185
  }
23017
23186
  var init_task_executor = __esm(() => {
23018
23187
  init_prompt_builder();
@@ -23030,7 +23199,7 @@ class GitWorkflow {
23030
23199
  this.log = log;
23031
23200
  this.ghUsername = ghUsername;
23032
23201
  const projectPath = config2.projectPath || process.cwd();
23033
- this.worktreeManager = config2.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }) : null;
23202
+ this.worktreeManager = config2.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }, log) : null;
23034
23203
  this.prService = config2.autoPush ? new PrService(projectPath, log) : null;
23035
23204
  }
23036
23205
  createTaskWorktree(task2, defaultExecutor) {
@@ -23038,6 +23207,7 @@ class GitWorkflow {
23038
23207
  return {
23039
23208
  worktreePath: null,
23040
23209
  baseBranch: null,
23210
+ baseCommitHash: null,
23041
23211
  executor: defaultExecutor
23042
23212
  };
23043
23213
  }
@@ -23063,10 +23233,11 @@ class GitWorkflow {
23063
23233
  return {
23064
23234
  worktreePath: result.worktreePath,
23065
23235
  baseBranch: result.baseBranch,
23236
+ baseCommitHash: result.baseCommitHash,
23066
23237
  executor: taskExecutor
23067
23238
  };
23068
23239
  }
23069
- commitAndPush(worktreePath, task2, baseBranch) {
23240
+ commitAndPush(worktreePath, task2, baseBranch, baseCommitHash) {
23070
23241
  if (!this.worktreeManager) {
23071
23242
  return { branch: null, pushed: false, pushFailed: false };
23072
23243
  }
@@ -23083,7 +23254,7 @@ class GitWorkflow {
23083
23254
 
23084
23255
  ${trailers.join(`
23085
23256
  `)}`;
23086
- const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch);
23257
+ const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch, baseCommitHash);
23087
23258
  if (!hash2) {
23088
23259
  this.log("No changes to commit for this task", "info");
23089
23260
  return {
@@ -23123,7 +23294,12 @@ ${trailers.join(`
23123
23294
  } catch (err) {
23124
23295
  const errorMessage = err instanceof Error ? err.message : String(err);
23125
23296
  this.log(`Git commit failed: ${errorMessage}`, "error");
23126
- return { branch: null, pushed: false, pushFailed: false };
23297
+ return {
23298
+ branch: null,
23299
+ pushed: false,
23300
+ pushFailed: true,
23301
+ pushError: `Git commit/push failed: ${errorMessage}`
23302
+ };
23127
23303
  }
23128
23304
  }
23129
23305
  createPullRequest(task2, branch, summary, baseBranch) {
@@ -39169,7 +39345,7 @@ class AgentWorker {
39169
39345
  }
39170
39346
  async executeTask(task2) {
39171
39347
  const fullTask = await this.client.tasks.getById(task2.id, this.config.workspaceId);
39172
- const { worktreePath, baseBranch, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
39348
+ const { worktreePath, baseBranch, baseCommitHash, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
39173
39349
  this.currentWorktreePath = worktreePath;
39174
39350
  let branchPushed = false;
39175
39351
  let keepBranch = false;
@@ -39181,7 +39357,7 @@ class AgentWorker {
39181
39357
  let prError = null;
39182
39358
  let noChanges = false;
39183
39359
  if (result.success && worktreePath) {
39184
- const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined);
39360
+ const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined, baseCommitHash ?? undefined);
39185
39361
  taskBranch = commitResult.branch;
39186
39362
  branchPushed = commitResult.pushed;
39187
39363
  keepBranch = taskBranch !== null;
@@ -40622,6 +40798,9 @@ class TierMergeService {
40622
40798
  }
40623
40799
  }
40624
40800
  findTierTaskBranches(tier) {
40801
+ const tierTaskIds = this.tierTaskIds.get(tier);
40802
+ if (!tierTaskIds || tierTaskIds.length === 0)
40803
+ return [];
40625
40804
  try {
40626
40805
  const output = execSync3('git branch -r --list "origin/agent/*" --format="%(refname:short)"', { cwd: this.projectPath, encoding: "utf-8" }).trim();
40627
40806
  if (!output)
@@ -40629,13 +40808,13 @@ class TierMergeService {
40629
40808
  const remoteBranches = output.split(`
40630
40809
  `).map((b) => b.replace("origin/", ""));
40631
40810
  return remoteBranches.filter((branch) => {
40632
- const match = branch.match(/^agent\/([^-]+)/);
40633
- if (!match)
40811
+ const branchSuffix = branch.replace(/^agent\//, "");
40812
+ if (!branchSuffix)
40634
40813
  return false;
40635
- const taskIdPrefix = match[1];
40636
- return this.tierTaskIds.get(tier)?.some((id) => id.startsWith(taskIdPrefix) || taskIdPrefix.startsWith(id.slice(0, 8))) ?? false;
40814
+ return tierTaskIds.some((id) => branchSuffix.startsWith(`${id}-`) || branchSuffix === id || branchSuffix.startsWith(id));
40637
40815
  });
40638
- } catch {
40816
+ } catch (err) {
40817
+ console.log(c.dim(` Could not list remote branches for tier ${tier}: ${err instanceof Error ? err.message : String(err)}`));
40639
40818
  return [];
40640
40819
  }
40641
40820
  }
@@ -41210,6 +41389,7 @@ Review and refine the Tech Lead's breakdown:
41210
41389
  3. **Task Merging** — If two tasks are trivially small and related, merge them.
41211
41390
  4. **Complexity Scoring** — Rate each task 1-5 (1=trivial, 5=very complex).
41212
41391
  5. **Missing Tasks** — Add any tasks the Tech Lead missed (database migrations, configuration, testing, etc.).
41392
+ 6. **Description Quality** — Review and improve each task description to be a clear, actionable implementation guide. Each description must tell the executing agent exactly what to do, where to do it (specific files/modules), how to do it (patterns, utilities, data flow), and what is NOT in scope. Vague descriptions like "Add authentication" must be rewritten with specific file paths, implementation approach, and boundaries.
41213
41393
 
41214
41394
  ## CRITICAL: Task Isolation & Overlap Detection
41215
41395
 
@@ -41235,7 +41415,7 @@ Your entire response must be a single JSON object — no text before it, no text
41235
41415
  "tasks": [
41236
41416
  {
41237
41417
  "title": "string",
41238
- "description": "string (2-4 sentences)",
41418
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries)",
41239
41419
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
41240
41420
  "priority": "HIGH | MEDIUM | LOW | CRITICAL",
41241
41421
  "labels": ["string"],
@@ -41328,6 +41508,15 @@ Verify tier assignments are correct:
41328
41508
  ### 5. Merge Conflict Risk Zones
41329
41509
  Identify the highest-risk files (files that multiple same-tier tasks might touch) and ensure only ONE task per tier modifies each.
41330
41510
 
41511
+ ### 6. Description Quality Validation
41512
+ For each task, verify the description is a clear, actionable implementation guide. Each description must specify:
41513
+ - **What to do** — the specific goal and expected behavior/outcome
41514
+ - **Where to do it** — specific files, modules, or directories to modify or create
41515
+ - **How to do it** — implementation approach, patterns to follow, existing utilities to use
41516
+ - **Boundaries** — what is NOT in scope for this task
41517
+
41518
+ If any description is vague (e.g., "Add authentication", "Update the API", "Fix the frontend"), rewrite it with concrete implementation details. The executing agent receives ONLY the task title, description, and acceptance criteria as its instructions.
41519
+
41331
41520
  ## Output Format
41332
41521
 
41333
41522
  Your entire response must be a single JSON object — no text before it, no text after it, no markdown code blocks, no explanation. Start your response with the "{" character:
@@ -41336,7 +41525,7 @@ Your entire response must be a single JSON object — no text before it, no text
41336
41525
  "hasIssues": true | false,
41337
41526
  "issues": [
41338
41527
  {
41339
- "type": "file_overlap" | "duplicated_work" | "not_self_contained" | "merge_conflict_risk" | "wrong_tier",
41528
+ "type": "file_overlap" | "duplicated_work" | "not_self_contained" | "merge_conflict_risk" | "wrong_tier" | "vague_description",
41340
41529
  "description": "string describing the specific issue",
41341
41530
  "affectedTasks": ["Task Title 1", "Task Title 2"],
41342
41531
  "resolution": "string describing how to fix it (merge, move to different tier, consolidate)"
@@ -41349,7 +41538,7 @@ Your entire response must be a single JSON object — no text before it, no text
41349
41538
  "tasks": [
41350
41539
  {
41351
41540
  "title": "string",
41352
- "description": "string",
41541
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries)",
41353
41542
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
41354
41543
  "priority": "CRITICAL | HIGH | MEDIUM | LOW",
41355
41544
  "labels": ["string"],
@@ -41373,6 +41562,7 @@ IMPORTANT:
41373
41562
  - If hasIssues is false, the revisedPlan should be identical to the input plan (no changes needed)
41374
41563
  - The revisedPlan is ALWAYS required — it becomes the final plan
41375
41564
  - When merging tasks, combine their acceptance criteria and update descriptions to cover all consolidated work
41565
+ - Ensure every task description is a detailed implementation guide (what, where, how, boundaries) — rewrite vague descriptions
41376
41566
  - Prefer fewer, larger, self-contained tasks over many small conflicting ones
41377
41567
  - Every task MUST have a "tier" field (integer >= 0)
41378
41568
  - tier 0 = foundational (runs first), tier 1 = depends on tier 0, tier 2 = depends on tier 1, etc.
@@ -41411,6 +41601,7 @@ Produce the final sprint plan:
41411
41601
  4. **Tier Assignment** — Assign each task an execution tier (integer, starting at 0). Tasks within the same tier run IN PARALLEL on separate git branches. Tasks in tier N+1 only start AFTER all tier N tasks are complete and merged. Tier 0 = foundational tasks (config, schemas, shared code). Higher tiers build on lower tier outputs.
41412
41602
  5. **Duration Estimate** — How many days this sprint will take with 2-3 agents working in parallel
41413
41603
  6. **Final Task List** — Each task with all fields filled in, ordered by execution priority
41604
+ 7. **Description Quality Check** — Ensure every task description is a clear, actionable implementation guide. Each description must specify: what to do, where to do it (specific files/modules/directories), how to do it (implementation approach, patterns to follow, existing utilities to use), and what is NOT in scope. If any description is vague or generic, rewrite it with specifics. Remember: an independent agent will receive ONLY the task title, description, and acceptance criteria — the description is its primary instruction.
41414
41605
 
41415
41606
  Guidelines:
41416
41607
  - The order of tasks in the array determines execution order. Tasks are dispatched sequentially from first to last.
@@ -41420,6 +41611,7 @@ Guidelines:
41420
41611
  - Group related independent tasks in the same tier for maximum parallelism
41421
41612
  - Ensure acceptance criteria are specific and testable
41422
41613
  - Keep the sprint focused — if it's too large (>12 tasks), consider reducing scope
41614
+ - Ensure every task description reads as a standalone implementation brief — not a summary
41423
41615
 
41424
41616
  ## CRITICAL: Task Isolation Validation
41425
41617
 
@@ -41441,7 +41633,7 @@ Your entire response must be a single JSON object — no text before it, no text
41441
41633
  "tasks": [
41442
41634
  {
41443
41635
  "title": "string",
41444
- "description": "string",
41636
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries)",
41445
41637
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
41446
41638
  "priority": "CRITICAL | HIGH | MEDIUM | LOW",
41447
41639
  "labels": ["string"],
@@ -41497,7 +41689,7 @@ ${input.codebaseIndex || "No codebase index available."}
41497
41689
  Analyze the CEO's directive and produce a detailed task breakdown. For each task:
41498
41690
 
41499
41691
  1. **Title** — Clear, action-oriented (e.g., "Implement user registration API endpoint")
41500
- 2. **Description** — What needs to be done technically, which files/modules are involved
41692
+ 2. **Description** — A detailed, actionable implementation guide (see description requirements below)
41501
41693
  3. **Assignee Role** — Who should work on this: BACKEND, FRONTEND, QA, PM, or DESIGN
41502
41694
  4. **Priority** — HIGH, MEDIUM, or LOW based on business impact
41503
41695
  5. **Labels** — Relevant tags (e.g., "api", "database", "ui", "auth")
@@ -41509,6 +41701,19 @@ Think about:
41509
41701
  - What the right granularity is (not too big, not too small)
41510
41702
  - What risks or unknowns exist
41511
41703
 
41704
+ ## CRITICAL: Task Description Requirements
41705
+
41706
+ Each task description will be handed to an INDEPENDENT agent as its primary instruction. The agent will have access to the codebase but NO context about the planning meeting. Descriptions must be clear enough for the agent to execute the task without ambiguity.
41707
+
41708
+ Each description MUST include:
41709
+ 1. **What to do** — Clearly state the goal and what needs to be implemented, changed, or created. Be specific about the expected behavior or outcome.
41710
+ 2. **Where to do it** — List the specific files, modules, or directories that need to be modified or created. Reference existing code paths when extending functionality.
41711
+ 3. **How to do it** — Provide key implementation details: which patterns to follow, which existing utilities or services to use, what the data flow looks like.
41712
+ 4. **Boundaries** — Clarify what is NOT in scope for this task to prevent overlap with other tasks.
41713
+
41714
+ Bad example: "Add authentication to the API."
41715
+ Good example: "Implement JWT-based authentication middleware in src/middleware/auth.ts. Create a verifyToken middleware that extracts the Bearer token from the Authorization header, validates it using the existing JWT_SECRET from env config, and attaches the decoded user payload to req.user. Apply this middleware to all routes in src/routes/protected/. Add a POST /auth/login endpoint in src/routes/auth.ts that accepts {email, password}, validates credentials against the users table, and returns a signed JWT. This task does NOT include user registration or password reset — those are handled separately."
41716
+
41512
41717
  ## CRITICAL: Task Isolation Rules
41513
41718
 
41514
41719
  Tasks will be executed by INDEPENDENT agents on SEPARATE git branches that get merged together. Each agent has NO knowledge of what other agents are doing. Therefore:
@@ -41527,7 +41732,7 @@ Your entire response must be a single JSON object — no text before it, no text
41527
41732
  "tasks": [
41528
41733
  {
41529
41734
  "title": "string",
41530
- "description": "string (2-4 sentences)",
41735
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries — see description requirements above)",
41531
41736
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
41532
41737
  "priority": "HIGH | MEDIUM | LOW",
41533
41738
  "labels": ["string"],
@@ -44177,9 +44382,31 @@ async function initCommand() {
44177
44382
  // src/commands/plan.ts
44178
44383
  init_index_node();
44179
44384
  import { parseArgs as parseArgs5 } from "node:util";
44385
+ function normalizePlanIdArgs(args) {
44386
+ const planIdFlags = new Set([
44387
+ "--approve",
44388
+ "--reject",
44389
+ "--cancel",
44390
+ "--show"
44391
+ ]);
44392
+ const result = [];
44393
+ let i = 0;
44394
+ while (i < args.length) {
44395
+ if (planIdFlags.has(args[i]) && i + 2 < args.length && args[i + 1] === "plan" && /^-\d+$/.test(args[i + 2])) {
44396
+ result.push(args[i]);
44397
+ result.push(`plan${args[i + 2]}`);
44398
+ i += 3;
44399
+ } else {
44400
+ result.push(args[i]);
44401
+ i++;
44402
+ }
44403
+ }
44404
+ return result;
44405
+ }
44180
44406
  async function planCommand(args) {
44407
+ const normalizedArgs = normalizePlanIdArgs(args);
44181
44408
  const { values, positionals } = parseArgs5({
44182
- args,
44409
+ args: normalizedArgs,
44183
44410
  options: {
44184
44411
  approve: { type: "string" },
44185
44412
  reject: { type: "string" },
@@ -44735,10 +44962,7 @@ function showTelegramHelp() {
44735
44962
 
44736
44963
  ${c.header(" SUBCOMMANDS ")}
44737
44964
  ${c.success("run")} Start the Telegram bot
44738
- ${c.dim("--agents <N> Override agent count (1-5)")}
44739
44965
  ${c.success("setup")} Interactive Telegram bot setup (or pass flags below)
44740
- ${c.dim("--token <TOKEN> Bot token from @BotFather (required)")}
44741
- ${c.dim("--chat-id <ID> Your Telegram chat ID (required)")}
44742
44966
  ${c.success("config")} Show current Telegram configuration
44743
44967
  ${c.success("set")} Set a config value
44744
44968
  ${c.dim("locus telegram set <key> <value>")}
@@ -44747,7 +44971,6 @@ function showTelegramHelp() {
44747
44971
 
44748
44972
  ${c.header(" EXAMPLES ")}
44749
44973
  ${c.dim("$")} ${c.primary("locus telegram run")}
44750
- ${c.dim("$")} ${c.primary("locus telegram run --agents 3")}
44751
44974
  ${c.dim("$")} ${c.primary('locus telegram setup --token "123:ABC" --chat-id 987654')}
44752
44975
  ${c.dim("$")} ${c.primary("locus telegram config")}
44753
44976
  ${c.dim("$")} ${c.primary("locus telegram remove")}
@@ -44936,22 +45159,7 @@ function removeCommand2(projectPath) {
44936
45159
  ${c.success("✔")} ${c.bold("Telegram configuration removed.")}
44937
45160
  `);
44938
45161
  }
44939
- function runBotCommand(subArgs, projectPath) {
44940
- let agentCountOverride;
44941
- for (let i = 0;i < subArgs.length; i++) {
44942
- if (subArgs[i] === "--agents" && subArgs[i + 1]) {
44943
- agentCountOverride = subArgs[++i]?.trim();
44944
- }
44945
- }
44946
- if (agentCountOverride) {
44947
- const parsed = Number.parseInt(agentCountOverride, 10);
44948
- if (Number.isNaN(parsed) || parsed < 1 || parsed > 5) {
44949
- console.error(`
44950
- ${c.error("✖")} ${c.bold("Agent count must be a number between 1 and 5.")}
44951
- `);
44952
- process.exit(1);
44953
- }
44954
- }
45162
+ function runBotCommand(projectPath) {
44955
45163
  const manager = new SettingsManager(projectPath);
44956
45164
  const settings = manager.load();
44957
45165
  if (!settings.telegram?.botToken || !settings.telegram?.chatId) {
@@ -44973,9 +45181,6 @@ function runBotCommand(subArgs, projectPath) {
44973
45181
  args = [];
44974
45182
  }
44975
45183
  const env = { ...process.env };
44976
- if (agentCountOverride) {
44977
- env.LOCUS_AGENT_COUNT = agentCountOverride;
44978
- }
44979
45184
  const child = spawn4(cmd, args, {
44980
45185
  cwd: projectPath,
44981
45186
  stdio: "inherit",
@@ -45001,19 +45206,18 @@ function runBotCommand(subArgs, projectPath) {
45001
45206
  async function telegramCommand(args) {
45002
45207
  const projectPath = process.cwd();
45003
45208
  const subcommand = args[0];
45004
- const subArgs = args.slice(1);
45005
45209
  switch (subcommand) {
45006
45210
  case "run":
45007
- runBotCommand(subArgs, projectPath);
45211
+ runBotCommand(projectPath);
45008
45212
  break;
45009
45213
  case "setup":
45010
- await setupCommand2(subArgs, projectPath);
45214
+ await setupCommand2(args, projectPath);
45011
45215
  break;
45012
45216
  case "config":
45013
45217
  configCommand2(projectPath);
45014
45218
  break;
45015
45219
  case "set":
45016
- setCommand2(subArgs, projectPath);
45220
+ setCommand2(args, projectPath);
45017
45221
  break;
45018
45222
  case "remove":
45019
45223
  removeCommand2(projectPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locusai/cli",
3
- "version": "0.10.2",
3
+ "version": "0.10.5",
4
4
  "description": "CLI for Locus - AI-native project management platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  "author": "",
34
34
  "license": "MIT",
35
35
  "dependencies": {
36
- "@locusai/sdk": "^0.10.2"
36
+ "@locusai/sdk": "^0.10.5"
37
37
  },
38
38
  "devDependencies": {}
39
39
  }