@locusai/cli 0.10.6 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/agent/worker.js +246 -785
  2. package/bin/locus.js +22412 -23313
  3. package/package.json +2 -2
@@ -14611,8 +14611,7 @@ var init_task = __esm(() => {
14611
14611
  comments: exports_external.array(CommentSchema).optional(),
14612
14612
  activityLog: exports_external.array(EventSchema).optional(),
14613
14613
  docs: exports_external.array(DocSchema),
14614
- order: exports_external.number().optional(),
14615
- tier: exports_external.number().int().min(0).optional()
14614
+ order: exports_external.number().optional()
14616
14615
  });
14617
14616
  CreateTaskSchema = exports_external.object({
14618
14617
  title: exports_external.string().min(1, "Title is required").max(200),
@@ -14627,8 +14626,7 @@ var init_task = __esm(() => {
14627
14626
  sprintId: exports_external.string().nullable().optional(),
14628
14627
  acceptanceChecklist: exports_external.array(AcceptanceItemSchema).optional(),
14629
14628
  docIds: exports_external.array(exports_external.string()).optional(),
14630
- order: exports_external.number().optional(),
14631
- tier: exports_external.number().int().min(0).optional()
14629
+ order: exports_external.number().optional()
14632
14630
  });
14633
14631
  UpdateTaskSchema = TaskSchema.partial().omit({
14634
14632
  id: true,
@@ -31378,588 +31376,252 @@ var init_knowledge_base = __esm(() => {
31378
31376
  init_config();
31379
31377
  });
31380
31378
 
31381
- // ../sdk/src/git/pr-service.ts
31379
+ // ../sdk/src/agent/git-workflow.ts
31382
31380
  import { execFileSync as execFileSync2 } from "node:child_process";
31383
31381
 
31384
- class PrService {
31385
- projectPath;
31382
+ class GitWorkflow {
31383
+ config;
31386
31384
  log;
31387
- constructor(projectPath, log) {
31388
- this.projectPath = projectPath;
31385
+ projectPath;
31386
+ branchName = null;
31387
+ baseBranch = null;
31388
+ ghUsername;
31389
+ constructor(config2, log) {
31390
+ this.config = config2;
31389
31391
  this.log = log;
31390
- }
31391
- createPr(options) {
31392
- const {
31393
- task: task2,
31394
- branch,
31395
- baseBranch: requestedBaseBranch,
31396
- agentId,
31397
- summary
31398
- } = options;
31399
- const provider = detectRemoteProvider(this.projectPath);
31400
- if (provider !== "github") {
31401
- throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
31402
- }
31403
- if (!isGhAvailable(this.projectPath)) {
31404
- throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
31405
- }
31406
- const title = `[Locus] ${task2.title}`;
31407
- const body = this.buildPrBody(task2, agentId, summary);
31408
- const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
31409
- this.validateCreatePrInputs(baseBranch, branch);
31410
- this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
31411
- const output = execFileSync2("gh", [
31412
- "pr",
31413
- "create",
31414
- "--title",
31415
- title,
31416
- "--body",
31417
- body,
31418
- "--base",
31419
- baseBranch,
31420
- "--head",
31421
- branch
31422
- ], {
31423
- cwd: this.projectPath,
31424
- encoding: "utf-8",
31425
- stdio: ["pipe", "pipe", "pipe"]
31426
- }).trim();
31427
- const url3 = output;
31428
- const prNumber = this.extractPrNumber(url3);
31429
- this.log(`PR created: ${url3}`, "success");
31430
- return { url: url3, number: prNumber };
31431
- }
31432
- validateCreatePrInputs(baseBranch, headBranch) {
31433
- if (!this.hasRemoteBranch(baseBranch)) {
31434
- throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
31435
- }
31436
- if (!this.hasRemoteBranch(headBranch)) {
31437
- throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
31438
- }
31439
- const baseRef = this.resolveBranchRef(baseBranch);
31440
- const headRef = this.resolveBranchRef(headBranch);
31441
- if (!baseRef) {
31442
- throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
31443
- }
31444
- if (!headRef) {
31445
- throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
31446
- }
31447
- const commitsAhead = this.countCommitsAhead(baseRef, headRef);
31448
- if (commitsAhead <= 0) {
31449
- throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
31392
+ this.projectPath = config2.projectPath || process.cwd();
31393
+ this.ghUsername = getGhUsername();
31394
+ if (this.ghUsername) {
31395
+ this.log(`GitHub user: ${this.ghUsername}`, "info");
31450
31396
  }
31451
31397
  }
31452
- countCommitsAhead(baseRef, headRef) {
31453
- const output = execFileSync2("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
31454
- cwd: this.projectPath,
31455
- encoding: "utf-8",
31456
- stdio: ["pipe", "pipe", "pipe"]
31457
- }).trim();
31458
- const value = Number.parseInt(output || "0", 10);
31459
- return Number.isNaN(value) ? 0 : value;
31460
- }
31461
- resolveBranchRef(branch) {
31462
- if (this.hasLocalBranch(branch)) {
31463
- return branch;
31464
- }
31465
- if (this.hasRemoteTrackingBranch(branch)) {
31466
- return `origin/${branch}`;
31467
- }
31468
- return null;
31469
- }
31470
- hasLocalBranch(branch) {
31398
+ createBranch(sprintId) {
31399
+ const defaultBranch = getDefaultBranch(this.projectPath);
31400
+ this.baseBranch = defaultBranch;
31471
31401
  try {
31472
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
31473
- cwd: this.projectPath,
31474
- stdio: ["pipe", "pipe", "pipe"]
31475
- });
31476
- return true;
31402
+ this.gitExec(["checkout", defaultBranch]);
31403
+ this.gitExec(["pull", "origin", defaultBranch]);
31477
31404
  } catch {
31478
- return false;
31405
+ this.log(`Could not pull latest from ${defaultBranch}, continuing with current state`, "warn");
31479
31406
  }
31407
+ const suffix = sprintId ? sprintId.slice(0, 8) : Date.now().toString(36);
31408
+ this.branchName = `locus/${suffix}`;
31409
+ try {
31410
+ this.gitExec(["branch", "-D", this.branchName]);
31411
+ } catch {}
31412
+ this.gitExec(["checkout", "-b", this.branchName]);
31413
+ this.log(`Created branch: ${this.branchName} (from ${defaultBranch})`, "success");
31414
+ return this.branchName;
31480
31415
  }
31481
- hasRemoteTrackingBranch(branch) {
31416
+ commitAndPush(task2) {
31417
+ if (!this.branchName) {
31418
+ this.log("No branch created yet, skipping commit", "warn");
31419
+ return { branch: null, pushed: false, pushFailed: false };
31420
+ }
31482
31421
  try {
31483
- execFileSync2("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
31484
- cwd: this.projectPath,
31485
- stdio: ["pipe", "pipe", "pipe"]
31486
- });
31487
- return true;
31488
- } catch {
31489
- return false;
31422
+ const status = this.gitExec(["status", "--porcelain"]).trim();
31423
+ if (!status) {
31424
+ const baseBranchCommit = this.getBaseCommit();
31425
+ const headCommit = this.gitExec(["rev-parse", "HEAD"]).trim();
31426
+ if (baseBranchCommit && headCommit !== baseBranchCommit) {
31427
+ return this.pushBranch();
31428
+ }
31429
+ this.log("No changes to commit for this task", "info");
31430
+ return {
31431
+ branch: this.branchName,
31432
+ pushed: false,
31433
+ pushFailed: false,
31434
+ noChanges: true,
31435
+ skipReason: "No changes were made for this task."
31436
+ };
31437
+ }
31438
+ this.gitExec(["add", "-A"]);
31439
+ const staged = this.gitExec(["diff", "--cached", "--name-only"]).trim();
31440
+ if (!staged) {
31441
+ this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
31442
+ return {
31443
+ branch: this.branchName,
31444
+ pushed: false,
31445
+ pushFailed: false,
31446
+ noChanges: true,
31447
+ skipReason: "All changes were ignored by .gitignore."
31448
+ };
31449
+ }
31450
+ this.log(`Staging ${staged.split(`
31451
+ `).length} file(s) for commit`, "info");
31452
+ const trailers = [
31453
+ `Task-ID: ${task2.id}`,
31454
+ `Agent: ${this.config.agentId}`,
31455
+ "Co-authored-by: LocusAI <agent@locusai.team>"
31456
+ ];
31457
+ if (this.ghUsername) {
31458
+ trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
31459
+ }
31460
+ const commitMessage = `feat(agent): ${task2.title}
31461
+
31462
+ ${trailers.join(`
31463
+ `)}`;
31464
+ this.gitExec(["commit", "-m", commitMessage]);
31465
+ const hash2 = this.gitExec(["rev-parse", "HEAD"]).trim();
31466
+ this.log(`Committed: ${hash2.slice(0, 8)}`, "success");
31467
+ return this.pushBranch();
31468
+ } catch (err) {
31469
+ const errorMessage = err instanceof Error ? err.message : String(err);
31470
+ this.log(`Git commit failed: ${errorMessage}`, "error");
31471
+ return {
31472
+ branch: this.branchName,
31473
+ pushed: false,
31474
+ pushFailed: true,
31475
+ pushError: `Git commit/push failed: ${errorMessage}`
31476
+ };
31490
31477
  }
31491
31478
  }
31492
- hasRemoteBranch(branch) {
31479
+ pushBranch() {
31480
+ if (!this.branchName) {
31481
+ return { branch: null, pushed: false, pushFailed: false };
31482
+ }
31493
31483
  try {
31494
- execFileSync2("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
31495
- cwd: this.projectPath,
31496
- stdio: ["pipe", "pipe", "pipe"]
31497
- });
31498
- return true;
31499
- } catch {
31500
- return false;
31484
+ this.gitExec(["push", "-u", "origin", this.branchName]);
31485
+ this.log(`Pushed ${this.branchName} to origin`, "success");
31486
+ return { branch: this.branchName, pushed: true, pushFailed: false };
31487
+ } catch (error48) {
31488
+ const msg = error48 instanceof Error ? error48.message : String(error48);
31489
+ if (msg.includes("non-fast-forward") || msg.includes("[rejected]") || msg.includes("fetch first")) {
31490
+ this.log(`Push rejected (non-fast-forward). Retrying with --force-with-lease.`, "warn");
31491
+ try {
31492
+ this.gitExec([
31493
+ "push",
31494
+ "--force-with-lease",
31495
+ "-u",
31496
+ "origin",
31497
+ this.branchName
31498
+ ]);
31499
+ this.log(`Pushed ${this.branchName} to origin with --force-with-lease`, "success");
31500
+ return { branch: this.branchName, pushed: true, pushFailed: false };
31501
+ } catch (retryErr) {
31502
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
31503
+ this.log(`Git push retry failed: ${retryMsg}`, "error");
31504
+ return {
31505
+ branch: this.branchName,
31506
+ pushed: false,
31507
+ pushFailed: true,
31508
+ pushError: retryMsg
31509
+ };
31510
+ }
31511
+ }
31512
+ this.log(`Git push failed: ${msg}`, "error");
31513
+ return {
31514
+ branch: this.branchName,
31515
+ pushed: false,
31516
+ pushFailed: true,
31517
+ pushError: msg
31518
+ };
31501
31519
  }
31502
31520
  }
31503
- getPrDiff(branch) {
31504
- return execFileSync2("gh", ["pr", "diff", branch], {
31505
- cwd: this.projectPath,
31506
- encoding: "utf-8",
31507
- stdio: ["pipe", "pipe", "pipe"],
31508
- maxBuffer: 10 * 1024 * 1024
31509
- });
31510
- }
31511
- submitReview(prIdentifier, body, event) {
31521
+ createPullRequest(completedTasks, summaries) {
31522
+ if (!this.branchName || !this.baseBranch) {
31523
+ return { url: null, error: "No branch or base branch available." };
31524
+ }
31525
+ const provider = detectRemoteProvider(this.projectPath);
31526
+ if (provider !== "github") {
31527
+ return {
31528
+ url: null,
31529
+ error: `PR creation is only supported for GitHub repositories (detected: ${provider})`
31530
+ };
31531
+ }
31532
+ if (!isGhAvailable(this.projectPath)) {
31533
+ return {
31534
+ url: null,
31535
+ error: "GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/"
31536
+ };
31537
+ }
31538
+ const title = `[Locus] Sprint tasks (${completedTasks.length} task${completedTasks.length !== 1 ? "s" : ""})`;
31539
+ const body = this.buildPrBody(completedTasks, summaries);
31540
+ this.log(`Creating PR: ${title} (${this.branchName} → ${this.baseBranch})`, "info");
31512
31541
  try {
31513
- execFileSync2("gh", [
31542
+ const output = execFileSync2("gh", [
31514
31543
  "pr",
31515
- "review",
31516
- prIdentifier,
31544
+ "create",
31545
+ "--title",
31546
+ title,
31517
31547
  "--body",
31518
31548
  body,
31519
- `--${event.toLowerCase().replace("_", "-")}`
31549
+ "--base",
31550
+ this.baseBranch,
31551
+ "--head",
31552
+ this.branchName
31520
31553
  ], {
31521
31554
  cwd: this.projectPath,
31522
31555
  encoding: "utf-8",
31523
31556
  stdio: ["pipe", "pipe", "pipe"]
31524
- });
31557
+ }).trim();
31558
+ this.log(`PR created: ${output}`, "success");
31559
+ return { url: output };
31525
31560
  } catch (err) {
31526
- const msg = err instanceof Error ? err.message : String(err);
31527
- if (event === "REQUEST_CHANGES" && msg.includes("own pull request")) {
31528
- execFileSync2("gh", ["pr", "review", prIdentifier, "--body", body, "--comment"], {
31529
- cwd: this.projectPath,
31530
- encoding: "utf-8",
31531
- stdio: ["pipe", "pipe", "pipe"]
31532
- });
31533
- return;
31534
- }
31535
- throw err;
31561
+ const errorMessage = err instanceof Error ? err.message : String(err);
31562
+ this.log(`PR creation failed: ${errorMessage}`, "error");
31563
+ return { url: null, error: errorMessage };
31536
31564
  }
31537
31565
  }
31538
- listLocusPrs() {
31566
+ checkoutBaseBranch() {
31567
+ if (!this.baseBranch)
31568
+ return;
31539
31569
  try {
31540
- const output = execFileSync2("gh", [
31541
- "pr",
31542
- "list",
31543
- "--search",
31544
- "[Locus] in:title",
31545
- "--state",
31546
- "open",
31547
- "--json",
31548
- "number,title,url,headRefName"
31549
- ], {
31550
- cwd: this.projectPath,
31551
- encoding: "utf-8",
31552
- stdio: ["pipe", "pipe", "pipe"]
31553
- }).trim();
31554
- const prs = JSON.parse(output || "[]");
31555
- return prs.map((pr) => ({
31556
- number: pr.number,
31557
- title: pr.title,
31558
- url: pr.url,
31559
- branch: pr.headRefName
31560
- }));
31561
- } catch {
31562
- this.log("Failed to list Locus PRs", "warn");
31563
- return [];
31570
+ this.gitExec(["checkout", this.baseBranch]);
31571
+ this.log(`Checked out base branch: ${this.baseBranch}`, "info");
31572
+ } catch (err) {
31573
+ this.log(`Could not checkout base branch: ${err instanceof Error ? err.message : String(err)}`, "warn");
31564
31574
  }
31565
31575
  }
31566
- hasLocusReview(prNumber) {
31576
+ getBranchName() {
31577
+ return this.branchName;
31578
+ }
31579
+ getBaseBranch() {
31580
+ return this.baseBranch;
31581
+ }
31582
+ getBaseCommit() {
31583
+ if (!this.baseBranch)
31584
+ return null;
31567
31585
  try {
31568
- const output = execFileSync2("gh", ["pr", "view", prNumber, "--json", "reviews"], {
31569
- cwd: this.projectPath,
31570
- encoding: "utf-8",
31571
- stdio: ["pipe", "pipe", "pipe"]
31572
- }).trim();
31573
- const data = JSON.parse(output || "{}");
31574
- return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
31586
+ return this.gitExec(["rev-parse", this.baseBranch]).trim();
31575
31587
  } catch {
31576
- return false;
31588
+ return null;
31577
31589
  }
31578
31590
  }
31579
- listUnreviewedLocusPrs() {
31580
- const allPrs = this.listLocusPrs();
31581
- return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
31582
- }
31583
- buildPrBody(task2, agentId, summary) {
31591
+ buildPrBody(completedTasks, summaries) {
31584
31592
  const sections = [];
31585
- sections.push(`## Task: ${task2.title}`);
31593
+ sections.push("## Completed Tasks");
31586
31594
  sections.push("");
31587
- if (task2.description) {
31588
- sections.push(task2.description);
31589
- sections.push("");
31590
- }
31591
- if (task2.acceptanceChecklist?.length > 0) {
31592
- sections.push("## Acceptance Criteria");
31593
- for (const item of task2.acceptanceChecklist) {
31594
- sections.push(`- [ ] ${item.text}`);
31595
+ for (let i = 0;i < completedTasks.length; i++) {
31596
+ const task2 = completedTasks[i];
31597
+ sections.push(`### ${i + 1}. ${task2.title}`);
31598
+ sections.push(`Task ID: \`${task2.id}\``);
31599
+ if (summaries[i]) {
31600
+ sections.push("");
31601
+ sections.push(summaries[i]);
31595
31602
  }
31596
31603
  sections.push("");
31597
31604
  }
31598
- if (summary) {
31599
- sections.push("## Agent Summary");
31600
- sections.push(summary);
31601
- sections.push("");
31602
- }
31603
31605
  sections.push("---");
31604
- sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task2.id}\``);
31606
+ sections.push(`*Created by Locus Agent \`${this.config.agentId.slice(-8)}\`*`);
31605
31607
  return sections.join(`
31606
31608
  `);
31607
31609
  }
31608
- extractPrNumber(url3) {
31609
- const match = url3.match(/\/pull\/(\d+)/);
31610
- return match ? Number.parseInt(match[1], 10) : 0;
31611
- }
31612
- }
31613
- var init_pr_service = __esm(() => {
31614
- init_git_utils();
31615
- });
31616
-
31617
- // ../sdk/src/worktree/worktree-config.ts
31618
- var WORKTREE_ROOT_DIR = ".locus-worktrees", WORKTREE_BRANCH_PREFIX = "agent", DEFAULT_WORKTREE_CONFIG;
31619
- var init_worktree_config = __esm(() => {
31620
- DEFAULT_WORKTREE_CONFIG = {
31621
- rootDir: WORKTREE_ROOT_DIR,
31622
- branchPrefix: WORKTREE_BRANCH_PREFIX,
31623
- cleanupPolicy: "retain-on-failure"
31624
- };
31625
- });
31626
-
31627
- // ../sdk/src/worktree/worktree-manager.ts
31628
- import { execFileSync as execFileSync3, execSync } from "node:child_process";
31629
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, rmSync, statSync } from "node:fs";
31630
- import { join as join4, resolve as resolve2, sep } from "node:path";
31631
-
31632
- class WorktreeManager {
31633
- config;
31634
- projectPath;
31635
- log;
31636
- constructor(projectPath, config2, log) {
31637
- this.projectPath = resolve2(projectPath);
31638
- this.config = { ...DEFAULT_WORKTREE_CONFIG, ...config2 };
31639
- this.log = log ?? ((_msg) => {
31640
- return;
31641
- });
31642
- }
31643
- get rootPath() {
31644
- return join4(this.projectPath, this.config.rootDir);
31645
- }
31646
- buildBranchName(taskId, taskSlug) {
31647
- const sanitized = taskSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
31648
- return `${this.config.branchPrefix}/${taskId}-${sanitized}`;
31649
- }
31650
- create(options) {
31651
- const branch = this.buildBranchName(options.taskId, options.taskSlug);
31652
- const worktreeDir = `${options.agentId}-${options.taskId}`;
31653
- const worktreePath = join4(this.rootPath, worktreeDir);
31654
- this.ensureDirectory(this.rootPath, "Worktree root");
31655
- const baseBranch = options.baseBranch ?? this.config.baseBranch ?? this.getCurrentBranch();
31656
- if (!this.branchExists(baseBranch)) {
31657
- this.log(`Base branch "${baseBranch}" not found locally, fetching from origin`, "info");
31658
- try {
31659
- this.gitExec(["fetch", "origin", baseBranch], this.projectPath);
31660
- this.gitExec(["branch", baseBranch, `origin/${baseBranch}`], this.projectPath);
31661
- } catch {
31662
- this.log(`Could not fetch/create local branch for "${baseBranch}", falling back to current branch`, "warn");
31663
- }
31664
- }
31665
- this.log(`Creating worktree: ${worktreeDir} (branch: ${branch}, base: ${baseBranch})`, "info");
31666
- if (existsSync4(worktreePath)) {
31667
- this.log(`Removing stale worktree directory: ${worktreePath}`, "warn");
31668
- try {
31669
- this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
31670
- } catch {
31671
- rmSync(worktreePath, { recursive: true, force: true });
31672
- this.git("worktree prune", this.projectPath);
31673
- }
31674
- }
31675
- if (this.branchExists(branch)) {
31676
- this.log(`Deleting existing branch: ${branch}`, "warn");
31677
- const branchWorktrees = this.list().filter((wt) => wt.branch === branch);
31678
- for (const wt of branchWorktrees) {
31679
- const worktreePath2 = resolve2(wt.path);
31680
- if (wt.isMain || !this.isManagedWorktreePath(worktreePath2)) {
31681
- throw new Error(`Branch "${branch}" is checked out at "${worktreePath2}". Remove or detach that worktree before retrying.`);
31682
- }
31683
- this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
31684
- this.remove(worktreePath2, false);
31685
- }
31686
- try {
31687
- this.git(`branch -D "${branch}"`, this.projectPath);
31688
- } catch {
31689
- this.git("worktree prune", this.projectPath);
31690
- this.git(`branch -D "${branch}"`, this.projectPath);
31691
- }
31692
- }
31693
- const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
31694
- try {
31695
- addWorktree();
31696
- } catch (error48) {
31697
- if (!this.isMissingDirectoryError(error48)) {
31698
- throw error48;
31699
- }
31700
- this.log(`Worktree creation failed due to missing directories. Retrying after cleanup: ${worktreePath}`, "warn");
31701
- this.cleanupFailedWorktree(worktreePath, branch);
31702
- this.ensureDirectory(this.rootPath, "Worktree root");
31703
- addWorktree();
31704
- }
31705
- const baseCommitHash = this.git("rev-parse HEAD", worktreePath).trim();
31706
- this.log(`Worktree created at ${worktreePath} (base: ${baseCommitHash.slice(0, 8)})`, "success");
31707
- return { worktreePath, branch, baseBranch, baseCommitHash };
31708
- }
31709
- list() {
31710
- const output = this.git("worktree list --porcelain", this.projectPath);
31711
- const worktrees = [];
31712
- const blocks = output.trim().split(`
31713
-
31714
- `);
31715
- for (const block of blocks) {
31716
- if (!block.trim())
31717
- continue;
31718
- const lines = block.trim().split(`
31719
- `);
31720
- let path = "";
31721
- let head = "";
31722
- let branch = "";
31723
- let isMain = false;
31724
- let isPrunable = false;
31725
- for (const line of lines) {
31726
- if (line.startsWith("worktree ")) {
31727
- path = line.slice("worktree ".length);
31728
- } else if (line.startsWith("HEAD ")) {
31729
- head = line.slice("HEAD ".length);
31730
- } else if (line.startsWith("branch ")) {
31731
- branch = line.slice("branch ".length).replace("refs/heads/", "");
31732
- } else if (line === "bare" || path === this.projectPath) {
31733
- isMain = true;
31734
- } else if (line === "prunable") {
31735
- isPrunable = true;
31736
- } else if (line === "detached") {
31737
- branch = "(detached)";
31738
- }
31739
- }
31740
- if (resolve2(path) === this.projectPath) {
31741
- isMain = true;
31742
- }
31743
- if (path) {
31744
- worktrees.push({ path, branch, head, isMain, isPrunable });
31745
- }
31746
- }
31747
- return worktrees;
31748
- }
31749
- listAgentWorktrees() {
31750
- return this.list().filter((wt) => !wt.isMain);
31751
- }
31752
- remove(worktreePath, deleteBranch = true) {
31753
- const absolutePath = resolve2(worktreePath);
31754
- const worktrees = this.list();
31755
- const worktree = worktrees.find((wt) => resolve2(wt.path) === absolutePath);
31756
- const branchToDelete = worktree?.branch;
31757
- this.log(`Removing worktree: ${absolutePath}`, "info");
31758
- try {
31759
- this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
31760
- } catch {
31761
- if (existsSync4(absolutePath)) {
31762
- rmSync(absolutePath, { recursive: true, force: true });
31763
- }
31764
- this.git("worktree prune", this.projectPath);
31765
- }
31766
- if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
31767
- try {
31768
- this.git(`branch -D "${branchToDelete}"`, this.projectPath);
31769
- this.log(`Deleted branch: ${branchToDelete}`, "success");
31770
- } catch {
31771
- this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
31772
- }
31773
- }
31774
- this.log("Worktree removed", "success");
31775
- }
31776
- prune() {
31777
- const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
31778
- this.git("worktree prune", this.projectPath);
31779
- const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
31780
- const pruned = before - after;
31781
- if (pruned > 0) {
31782
- this.log(`Pruned ${pruned} stale worktree(s)`, "success");
31783
- }
31784
- return pruned;
31785
- }
31786
- removeAll() {
31787
- const agentWorktrees = this.listAgentWorktrees();
31788
- let removed = 0;
31789
- for (const wt of agentWorktrees) {
31790
- try {
31791
- this.remove(wt.path, true);
31792
- removed++;
31793
- } catch {
31794
- this.log(`Failed to remove worktree: ${wt.path}`, "warn");
31795
- }
31796
- }
31797
- if (existsSync4(this.rootPath)) {
31798
- try {
31799
- rmSync(this.rootPath, { recursive: true, force: true });
31800
- } catch {}
31801
- }
31802
- return removed;
31803
- }
31804
- hasChanges(worktreePath) {
31805
- const status = this.git("status --porcelain", worktreePath).trim();
31806
- return status.length > 0;
31807
- }
31808
- hasCommitsAhead(worktreePath, baseBranch) {
31809
- try {
31810
- const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
31811
- return Number.parseInt(count, 10) > 0;
31812
- } catch (err) {
31813
- this.log(`Could not compare HEAD against base branch "${baseBranch}": ${err instanceof Error ? err.message : String(err)}`, "warn");
31814
- return false;
31815
- }
31816
- }
31817
- hasCommitsAheadOfHash(worktreePath, baseHash) {
31818
- try {
31819
- const headHash = this.git("rev-parse HEAD", worktreePath).trim();
31820
- return headHash !== baseHash;
31821
- } catch {
31822
- return false;
31823
- }
31824
- }
31825
- commitChanges(worktreePath, message, baseBranch, baseCommitHash) {
31826
- const hasUncommittedChanges = this.hasChanges(worktreePath);
31827
- if (hasUncommittedChanges) {
31828
- const statusOutput = this.git("status --porcelain", worktreePath).trim();
31829
- this.log(`Detected uncommitted changes:
31830
- ${statusOutput.split(`
31831
- `).slice(0, 10).join(`
31832
- `)}${statusOutput.split(`
31833
- `).length > 10 ? `
31834
- ... and ${statusOutput.split(`
31835
- `).length - 10} more` : ""}`, "info");
31836
- }
31837
- if (!hasUncommittedChanges) {
31838
- if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
31839
- const hash3 = this.git("rev-parse HEAD", worktreePath).trim();
31840
- this.log(`Agent already committed changes (${hash3.slice(0, 8)}); skipping additional commit`, "info");
31841
- return hash3;
31842
- }
31843
- if (baseCommitHash && this.hasCommitsAheadOfHash(worktreePath, baseCommitHash)) {
31844
- const hash3 = this.git("rev-parse HEAD", worktreePath).trim();
31845
- this.log(`Agent already committed changes (${hash3.slice(0, 8)}, detected via base commit hash); skipping additional commit`, "info");
31846
- return hash3;
31847
- }
31848
- const branch = this.getBranch(worktreePath);
31849
- this.log(`No changes detected in worktree (branch: ${branch}, baseBranch: ${baseBranch ?? "none"}, baseCommitHash: ${baseCommitHash?.slice(0, 8) ?? "none"})`, "warn");
31850
- return null;
31851
- }
31852
- this.git("add -A", worktreePath);
31853
- const staged = this.git("diff --cached --name-only", worktreePath).trim();
31854
- if (!staged) {
31855
- this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
31856
- return null;
31857
- }
31858
- this.log(`Staging ${staged.split(`
31859
- `).length} file(s) for commit`, "info");
31860
- this.gitExec(["commit", "-m", message], worktreePath);
31861
- const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
31862
- this.log(`Committed: ${hash2.slice(0, 8)}`, "success");
31863
- return hash2;
31864
- }
31865
- pushBranch(worktreePath, remote = "origin") {
31866
- const branch = this.getBranch(worktreePath);
31867
- this.log(`Pushing branch ${branch} to ${remote}`, "info");
31868
- try {
31869
- this.gitExec(["push", "-u", remote, branch], worktreePath);
31870
- this.log(`Pushed ${branch} to ${remote}`, "success");
31871
- return branch;
31872
- } catch (error48) {
31873
- if (!this.isNonFastForwardPushError(error48)) {
31874
- throw error48;
31875
- }
31876
- this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
31877
- try {
31878
- this.gitExec(["fetch", remote, branch], worktreePath);
31879
- } catch {}
31880
- this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
31881
- this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
31882
- return branch;
31883
- }
31884
- }
31885
- getBranch(worktreePath) {
31886
- return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
31887
- }
31888
- hasWorktreeForTask(taskId) {
31889
- return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
31890
- }
31891
- branchExists(branchName) {
31892
- try {
31893
- this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
31894
- return true;
31895
- } catch {
31896
- return false;
31897
- }
31898
- }
31899
- getCurrentBranch() {
31900
- return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
31901
- }
31902
- isManagedWorktreePath(worktreePath) {
31903
- const rootPath = resolve2(this.rootPath);
31904
- const candidate = resolve2(worktreePath);
31905
- const rootWithSep = rootPath.endsWith(sep) ? rootPath : `${rootPath}${sep}`;
31906
- return candidate.startsWith(rootWithSep);
31907
- }
31908
- ensureDirectory(dirPath, label) {
31909
- if (existsSync4(dirPath)) {
31910
- if (!statSync(dirPath).isDirectory()) {
31911
- throw new Error(`${label} exists but is not a directory: ${dirPath}`);
31912
- }
31913
- return;
31914
- }
31915
- mkdirSync2(dirPath, { recursive: true });
31916
- }
31917
- isMissingDirectoryError(error48) {
31918
- const message = error48 instanceof Error ? error48.message : String(error48);
31919
- return message.includes("cannot create directory") || message.includes("No such file or directory");
31920
- }
31921
- cleanupFailedWorktree(worktreePath, branch) {
31922
- try {
31923
- this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
31924
- } catch {}
31925
- if (existsSync4(worktreePath)) {
31926
- rmSync(worktreePath, { recursive: true, force: true });
31927
- }
31928
- try {
31929
- this.git("worktree prune", this.projectPath);
31930
- } catch {}
31931
- if (this.branchExists(branch)) {
31932
- try {
31933
- this.git(`branch -D "${branch}"`, this.projectPath);
31934
- } catch {}
31935
- }
31936
- }
31937
- isNonFastForwardPushError(error48) {
31938
- const message = error48 instanceof Error ? error48.message : String(error48);
31939
- return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
31940
- }
31941
- git(args, cwd) {
31942
- return execSync(`git ${args}`, {
31943
- cwd,
31944
- encoding: "utf-8",
31945
- stdio: ["pipe", "pipe", "pipe"]
31946
- });
31947
- }
31948
- gitExec(args, cwd) {
31949
- return execFileSync3("git", args, {
31950
- cwd,
31610
+ gitExec(args) {
31611
+ return execFileSync2("git", args, {
31612
+ cwd: this.projectPath,
31951
31613
  encoding: "utf-8",
31952
31614
  stdio: ["pipe", "pipe", "pipe"]
31953
31615
  });
31954
31616
  }
31955
31617
  }
31956
- var init_worktree_manager = __esm(() => {
31957
- init_worktree_config();
31618
+ var init_git_workflow = __esm(() => {
31619
+ init_git_utils();
31958
31620
  });
31959
31621
 
31960
31622
  // ../sdk/src/core/prompt-builder.ts
31961
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
31962
- import { join as join5 } from "node:path";
31623
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "node:fs";
31624
+ import { join as join4 } from "node:path";
31963
31625
 
31964
31626
  class PromptBuilder {
31965
31627
  projectPath;
@@ -32001,7 +31663,7 @@ ${task2.description || "No description provided."}
32001
31663
  }
32002
31664
  const contextPath = getLocusPath(this.projectPath, "contextFile");
32003
31665
  let hasLocalContext = false;
32004
- if (existsSync5(contextPath)) {
31666
+ if (existsSync4(contextPath)) {
32005
31667
  try {
32006
31668
  const context = readFileSync4(contextPath, "utf-8");
32007
31669
  if (context.trim().length > 20) {
@@ -32057,7 +31719,7 @@ ${serverContext.context}
32057
31719
 
32058
31720
  `;
32059
31721
  const indexPath = getLocusPath(this.projectPath, "indexFile");
32060
- if (existsSync5(indexPath)) {
31722
+ if (existsSync4(indexPath)) {
32061
31723
  prompt += `## Codebase Overview
32062
31724
  There is an index file in the .locus/codebase-index.json and if you need you can check it.
32063
31725
 
@@ -32134,7 +31796,7 @@ ${query}
32134
31796
  }
32135
31797
  const contextPath = getLocusPath(this.projectPath, "contextFile");
32136
31798
  let hasLocalContext = false;
32137
- if (existsSync5(contextPath)) {
31799
+ if (existsSync4(contextPath)) {
32138
31800
  try {
32139
31801
  const context = readFileSync4(contextPath, "utf-8");
32140
31802
  if (context.trim().length > 20) {
@@ -32170,7 +31832,7 @@ ${fallback}
32170
31832
 
32171
31833
  `;
32172
31834
  const indexPath = getLocusPath(this.projectPath, "indexFile");
32173
- if (existsSync5(indexPath)) {
31835
+ if (existsSync4(indexPath)) {
32174
31836
  prompt += `## Codebase Overview
32175
31837
  There is an index file in the .locus/codebase-index.json and if you need you can check it.
32176
31838
 
@@ -32185,7 +31847,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
32185
31847
  }
32186
31848
  getProjectConfig() {
32187
31849
  const configPath = getLocusPath(this.projectPath, "configFile");
32188
- if (existsSync5(configPath)) {
31850
+ if (existsSync4(configPath)) {
32189
31851
  try {
32190
31852
  return JSON.parse(readFileSync4(configPath, "utf-8"));
32191
31853
  } catch {
@@ -32195,8 +31857,8 @@ There is an index file in the .locus/codebase-index.json and if you need you can
32195
31857
  return null;
32196
31858
  }
32197
31859
  getFallbackContext() {
32198
- const readmePath = join5(this.projectPath, "README.md");
32199
- if (existsSync5(readmePath)) {
31860
+ const readmePath = join4(this.projectPath, "README.md");
31861
+ if (existsSync4(readmePath)) {
32200
31862
  try {
32201
31863
  const content = readFileSync4(readmePath, "utf-8");
32202
31864
  const limit = 1000;
@@ -32215,7 +31877,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
32215
31877
  if (e.startsWith(".") || e === "node_modules")
32216
31878
  return false;
32217
31879
  try {
32218
- return statSync2(join5(this.projectPath, e)).isDirectory();
31880
+ return statSync(join4(this.projectPath, e)).isDirectory();
32219
31881
  } catch {
32220
31882
  return false;
32221
31883
  }
@@ -32300,162 +31962,6 @@ var init_task_executor = __esm(() => {
32300
31962
  init_prompt_builder();
32301
31963
  });
32302
31964
 
32303
- // ../sdk/src/agent/git-workflow.ts
32304
- class GitWorkflow {
32305
- config;
32306
- log;
32307
- ghUsername;
32308
- worktreeManager;
32309
- prService;
32310
- constructor(config2, log, ghUsername) {
32311
- this.config = config2;
32312
- this.log = log;
32313
- this.ghUsername = ghUsername;
32314
- const projectPath = config2.projectPath || process.cwd();
32315
- this.worktreeManager = config2.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }, log) : null;
32316
- this.prService = config2.autoPush ? new PrService(projectPath, log) : null;
32317
- }
32318
- createTaskWorktree(task2, defaultExecutor) {
32319
- if (!this.worktreeManager) {
32320
- return {
32321
- worktreePath: null,
32322
- baseBranch: null,
32323
- baseCommitHash: null,
32324
- executor: defaultExecutor
32325
- };
32326
- }
32327
- const slug = task2.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
32328
- const result = this.worktreeManager.create({
32329
- taskId: task2.id,
32330
- taskSlug: slug,
32331
- agentId: this.config.agentId,
32332
- baseBranch: this.config.baseBranch
32333
- });
32334
- this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
32335
- const provider = this.config.provider ?? PROVIDER.CLAUDE;
32336
- const taskAiRunner = createAiRunner(provider, {
32337
- projectPath: result.worktreePath,
32338
- model: this.config.model,
32339
- log: this.log
32340
- });
32341
- const taskExecutor = new TaskExecutor({
32342
- aiRunner: taskAiRunner,
32343
- projectPath: result.worktreePath,
32344
- log: this.log
32345
- });
32346
- return {
32347
- worktreePath: result.worktreePath,
32348
- baseBranch: result.baseBranch,
32349
- baseCommitHash: result.baseCommitHash,
32350
- executor: taskExecutor
32351
- };
32352
- }
32353
- commitAndPush(worktreePath, task2, baseBranch, baseCommitHash) {
32354
- if (!this.worktreeManager) {
32355
- return { branch: null, pushed: false, pushFailed: false };
32356
- }
32357
- try {
32358
- const trailers = [
32359
- `Task-ID: ${task2.id}`,
32360
- `Agent: ${this.config.agentId}`,
32361
- "Co-authored-by: LocusAI <agent@locusai.team>"
32362
- ];
32363
- if (this.ghUsername) {
32364
- trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
32365
- }
32366
- const commitMessage = `feat(agent): ${task2.title}
32367
-
32368
- ${trailers.join(`
32369
- `)}`;
32370
- const hash2 = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch, baseCommitHash);
32371
- if (!hash2) {
32372
- this.log("No changes to commit for this task", "info");
32373
- return {
32374
- branch: null,
32375
- pushed: false,
32376
- pushFailed: false,
32377
- noChanges: true,
32378
- skipReason: "No changes were committed, so no branch was pushed."
32379
- };
32380
- }
32381
- const localBranch = this.worktreeManager.getBranch(worktreePath);
32382
- if (this.config.autoPush) {
32383
- try {
32384
- return {
32385
- branch: this.worktreeManager.pushBranch(worktreePath),
32386
- pushed: true,
32387
- pushFailed: false
32388
- };
32389
- } catch (err) {
32390
- const errorMessage = err instanceof Error ? err.message : String(err);
32391
- this.log(`Git push failed: ${errorMessage}`, "error");
32392
- return {
32393
- branch: localBranch,
32394
- pushed: false,
32395
- pushFailed: true,
32396
- pushError: errorMessage
32397
- };
32398
- }
32399
- }
32400
- this.log("Auto-push disabled; skipping branch push", "info");
32401
- return {
32402
- branch: localBranch,
32403
- pushed: false,
32404
- pushFailed: false,
32405
- skipReason: "Auto-push is disabled, so PR creation was skipped."
32406
- };
32407
- } catch (err) {
32408
- const errorMessage = err instanceof Error ? err.message : String(err);
32409
- this.log(`Git commit failed: ${errorMessage}`, "error");
32410
- return {
32411
- branch: null,
32412
- pushed: false,
32413
- pushFailed: true,
32414
- pushError: `Git commit/push failed: ${errorMessage}`
32415
- };
32416
- }
32417
- }
32418
- createPullRequest(task2, branch, summary, baseBranch) {
32419
- if (!this.prService) {
32420
- const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
32421
- this.log(`PR creation skipped: ${errorMessage}`, "warn");
32422
- return { url: null, error: errorMessage };
32423
- }
32424
- this.log(`Attempting PR creation from branch: ${branch}`, "info");
32425
- try {
32426
- const result = this.prService.createPr({
32427
- task: task2,
32428
- branch,
32429
- baseBranch,
32430
- agentId: this.config.agentId,
32431
- summary
32432
- });
32433
- return { url: result.url };
32434
- } catch (err) {
32435
- const errorMessage = err instanceof Error ? err.message : String(err);
32436
- this.log(`PR creation failed: ${errorMessage}`, "error");
32437
- return { url: null, error: errorMessage };
32438
- }
32439
- }
32440
- cleanupWorktree(worktreePath, keepBranch) {
32441
- if (!this.worktreeManager || !worktreePath)
32442
- return;
32443
- try {
32444
- this.worktreeManager.remove(worktreePath, !keepBranch);
32445
- this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
32446
- } catch {
32447
- this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
32448
- }
32449
- }
32450
- }
32451
- var init_git_workflow = __esm(() => {
32452
- init_factory();
32453
- init_config();
32454
- init_pr_service();
32455
- init_worktree_manager();
32456
- init_task_executor();
32457
- });
32458
-
32459
31965
  // ../sdk/src/agent/worker-cli.ts
32460
31966
  var exports_worker_cli = {};
32461
31967
  __export(exports_worker_cli, {
@@ -32488,16 +31994,8 @@ function parseWorkerArgs(argv) {
32488
31994
  config2.apiKey = args[++i];
32489
31995
  else if (arg === "--project-path")
32490
31996
  config2.projectPath = args[++i];
32491
- else if (arg === "--main-project-path")
32492
- config2.mainProjectPath = args[++i];
32493
31997
  else if (arg === "--model")
32494
31998
  config2.model = args[++i];
32495
- else if (arg === "--use-worktrees")
32496
- config2.useWorktrees = true;
32497
- else if (arg === "--auto-push")
32498
- config2.autoPush = true;
32499
- else if (arg === "--base-branch")
32500
- config2.baseBranch = args[++i];
32501
31999
  else if (arg === "--provider") {
32502
32000
  const value = args[i + 1];
32503
32001
  if (value && !value.startsWith("--"))
@@ -32539,8 +32037,8 @@ class AgentWorker {
32539
32037
  tasksCompleted = 0;
32540
32038
  heartbeatInterval = null;
32541
32039
  currentTaskId = null;
32542
- currentWorktreePath = null;
32543
- postCleanupDelayMs = 5000;
32040
+ completedTaskList = [];
32041
+ taskSummaries = [];
32544
32042
  constructor(config2) {
32545
32043
  this.config = config2;
32546
32044
  const projectPath = config2.projectPath || process.cwd();
@@ -32555,17 +32053,12 @@ class AgentWorker {
32555
32053
  }
32556
32054
  });
32557
32055
  const log = this.log.bind(this);
32558
- if (config2.useWorktrees && !isGitAvailable()) {
32559
- this.log("git is not installed — worktree isolation will not work", "error");
32560
- config2.useWorktrees = false;
32056
+ if (!isGitAvailable()) {
32057
+ this.log("git is not installed — branch management will not work", "error");
32561
32058
  }
32562
- if (config2.autoPush && !isGhAvailable(projectPath)) {
32059
+ if (!isGhAvailable(projectPath)) {
32563
32060
  this.log("GitHub CLI (gh) not available or not authenticated. Branch push can continue, but automatic PR creation may fail until gh is configured. Install from https://cli.github.com/", "warn");
32564
32061
  }
32565
- const ghUsername = config2.autoPush ? getGhUsername() : null;
32566
- if (ghUsername) {
32567
- this.log(`GitHub user: ${ghUsername}`, "info");
32568
- }
32569
32062
  const provider = config2.provider ?? PROVIDER.CLAUDE;
32570
32063
  this.aiRunner = createAiRunner(provider, {
32571
32064
  projectPath,
@@ -32578,18 +32071,9 @@ class AgentWorker {
32578
32071
  log
32579
32072
  });
32580
32073
  this.knowledgeBase = new KnowledgeBase(projectPath);
32581
- this.gitWorkflow = new GitWorkflow(config2, log, ghUsername);
32074
+ this.gitWorkflow = new GitWorkflow(config2, log);
32582
32075
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
32583
32076
  this.log(`Using ${providerLabel} CLI for all phases`, "info");
32584
- if (config2.useWorktrees) {
32585
- this.log("Per-task worktree isolation enabled", "info");
32586
- if (config2.baseBranch) {
32587
- this.log(`Base branch for worktrees: ${config2.baseBranch}`, "info");
32588
- }
32589
- if (config2.autoPush) {
32590
- this.log("Auto-push enabled: branches will be pushed to remote", "info");
32591
- }
32592
- }
32593
32077
  }
32594
32078
  log(message, level = "info") {
32595
32079
  const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
@@ -32638,56 +32122,26 @@ class AgentWorker {
32638
32122
  }
32639
32123
  async executeTask(task2) {
32640
32124
  const fullTask = await this.client.tasks.getById(task2.id, this.config.workspaceId);
32641
- const { worktreePath, baseBranch, baseCommitHash, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
32642
- this.currentWorktreePath = worktreePath;
32643
- let branchPushed = false;
32644
- let keepBranch = false;
32645
- let preserveWorktree = false;
32646
32125
  try {
32647
- const result = await executor.execute(fullTask);
32648
- let taskBranch = null;
32649
- let prUrl = null;
32650
- let prError = null;
32126
+ const result = await this.taskExecutor.execute(fullTask);
32651
32127
  let noChanges = false;
32652
- if (result.success && worktreePath) {
32653
- const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined, baseCommitHash ?? undefined);
32128
+ let taskBranch = null;
32129
+ if (result.success) {
32130
+ const commitResult = this.gitWorkflow.commitAndPush(fullTask);
32654
32131
  taskBranch = commitResult.branch;
32655
- branchPushed = commitResult.pushed;
32656
- keepBranch = taskBranch !== null;
32657
32132
  noChanges = Boolean(commitResult.noChanges);
32658
- if (commitResult.pushFailed) {
32659
- preserveWorktree = true;
32660
- prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
32661
- this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
32662
- }
32663
- if (branchPushed && taskBranch) {
32664
- const prResult = this.gitWorkflow.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
32665
- prUrl = prResult.url;
32666
- prError = prResult.error ?? null;
32667
- if (!prUrl) {
32668
- preserveWorktree = true;
32669
- this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
32670
- }
32671
- } else if (commitResult.skipReason) {
32672
- this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
32673
- }
32674
- } else if (result.success && !worktreePath) {
32675
- this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
32676
32133
  }
32677
32134
  return {
32678
32135
  ...result,
32679
32136
  branch: taskBranch ?? undefined,
32680
- prUrl: prUrl ?? undefined,
32681
- prError: prError ?? undefined,
32682
32137
  noChanges: noChanges || undefined
32683
32138
  };
32684
- } finally {
32685
- if (preserveWorktree || keepBranch) {
32686
- this.currentWorktreePath = null;
32687
- } else {
32688
- this.gitWorkflow.cleanupWorktree(worktreePath, keepBranch);
32689
- this.currentWorktreePath = null;
32690
- }
32139
+ } catch (err) {
32140
+ const msg = err instanceof Error ? err.message : String(err);
32141
+ return {
32142
+ success: false,
32143
+ summary: `Execution error: ${msg}`
32144
+ };
32691
32145
  }
32692
32146
  }
32693
32147
  updateProgress(task2, summary) {
@@ -32720,19 +32174,13 @@ class AgentWorker {
32720
32174
  this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
32721
32175
  });
32722
32176
  }
32723
- async delayAfterCleanup() {
32724
- if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
32725
- return;
32726
- this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
32727
- await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
32728
- }
32729
32177
  async run() {
32730
32178
  this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
32731
32179
  const handleShutdown = () => {
32732
32180
  this.log("Received shutdown signal. Aborting...", "warn");
32733
32181
  this.aiRunner.abort();
32734
32182
  this.stopHeartbeat();
32735
- this.gitWorkflow.cleanupWorktree(this.currentWorktreePath, false);
32183
+ this.gitWorkflow.checkoutBaseBranch();
32736
32184
  process.exit(1);
32737
32185
  };
32738
32186
  process.on("SIGTERM", handleShutdown);
@@ -32744,10 +32192,12 @@ class AgentWorker {
32744
32192
  } else {
32745
32193
  this.log("No active sprint found.", "warn");
32746
32194
  }
32195
+ const branchName = this.gitWorkflow.createBranch(this.config.sprintId);
32196
+ this.log(`Working on branch: ${branchName}`, "info");
32747
32197
  while (this.tasksCompleted < this.maxTasks) {
32748
32198
  const task2 = await this.getNextTask();
32749
32199
  if (!task2) {
32750
- this.log("No more tasks to process. Exiting.", "info");
32200
+ this.log("No more tasks to process.", "info");
32751
32201
  break;
32752
32202
  }
32753
32203
  this.log(`Claimed: ${task2.title}`, "success");
@@ -32763,7 +32213,7 @@ class AgentWorker {
32763
32213
  });
32764
32214
  await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
32765
32215
  author: this.config.agentId,
32766
- text: `⚠️ Agent execution finished with no file changes, so no commit/branch/PR was created.
32216
+ text: `⚠️ Agent execution finished with no file changes, so no commit was created.
32767
32217
 
32768
32218
  ${result.summary}`
32769
32219
  });
@@ -32772,22 +32222,17 @@ ${result.summary}`
32772
32222
  const updatePayload = {
32773
32223
  status: "IN_REVIEW" /* IN_REVIEW */
32774
32224
  };
32775
- if (result.prUrl) {
32776
- updatePayload.prUrl = result.prUrl;
32777
- }
32778
32225
  await this.client.tasks.update(task2.id, this.config.workspaceId, updatePayload);
32779
32226
  const branchInfo = result.branch ? `
32780
32227
 
32781
32228
  Branch: \`${result.branch}\`` : "";
32782
- const prInfo = result.prUrl ? `
32783
- PR: ${result.prUrl}` : "";
32784
- const prErrorInfo = result.prError ? `
32785
- PR automation error: ${result.prError}` : "";
32786
32229
  await this.client.tasks.addComment(task2.id, this.config.workspaceId, {
32787
32230
  author: this.config.agentId,
32788
- text: `✅ ${result.summary}${branchInfo}${prInfo}${prErrorInfo}`
32231
+ text: `✅ ${result.summary}${branchInfo}`
32789
32232
  });
32790
32233
  this.tasksCompleted++;
32234
+ this.completedTaskList.push({ title: task2.title, id: task2.id });
32235
+ this.taskSummaries.push(result.summary);
32791
32236
  this.updateProgress(task2, result.summary);
32792
32237
  }
32793
32238
  } else {
@@ -32803,8 +32248,24 @@ PR automation error: ${result.prError}` : "";
32803
32248
  }
32804
32249
  this.currentTaskId = null;
32805
32250
  this.sendHeartbeat();
32806
- await this.delayAfterCleanup();
32807
32251
  }
32252
+ if (this.completedTaskList.length > 0) {
32253
+ this.log("All tasks done. Creating pull request...", "info");
32254
+ const prResult = this.gitWorkflow.createPullRequest(this.completedTaskList, this.taskSummaries);
32255
+ if (prResult.url) {
32256
+ this.log(`PR created: ${prResult.url}`, "success");
32257
+ for (const task2 of this.completedTaskList) {
32258
+ try {
32259
+ await this.client.tasks.update(task2.id, this.config.workspaceId, {
32260
+ prUrl: prResult.url
32261
+ });
32262
+ } catch {}
32263
+ }
32264
+ } else if (prResult.error) {
32265
+ this.log(`PR creation failed: ${prResult.error}`, "error");
32266
+ }
32267
+ }
32268
+ this.gitWorkflow.checkoutBaseBranch();
32808
32269
  this.currentTaskId = null;
32809
32270
  this.stopHeartbeat();
32810
32271
  this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});