@locusai/sdk 0.8.1 → 0.9.1

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 (72) hide show
  1. package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts +2 -0
  2. package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts.map +1 -0
  3. package/dist/agent/__tests__/worker.no-changes.test.d.ts +2 -0
  4. package/dist/agent/__tests__/worker.no-changes.test.d.ts.map +1 -0
  5. package/dist/agent/document-fetcher.d.ts.map +1 -1
  6. package/dist/agent/index.d.ts +1 -0
  7. package/dist/agent/index.d.ts.map +1 -1
  8. package/dist/agent/review-service.d.ts.map +1 -1
  9. package/dist/agent/reviewer-worker.d.ts +42 -0
  10. package/dist/agent/reviewer-worker.d.ts.map +1 -0
  11. package/dist/agent/task-executor.d.ts +1 -1
  12. package/dist/agent/task-executor.d.ts.map +1 -1
  13. package/dist/agent/worker.d.ts +47 -4
  14. package/dist/agent/worker.d.ts.map +1 -1
  15. package/dist/agent/worker.js +1102 -506
  16. package/dist/ai/claude-runner.d.ts +5 -0
  17. package/dist/ai/claude-runner.d.ts.map +1 -1
  18. package/dist/ai/codex-runner.d.ts +5 -0
  19. package/dist/ai/codex-runner.d.ts.map +1 -1
  20. package/dist/ai/runner.d.ts +5 -0
  21. package/dist/ai/runner.d.ts.map +1 -1
  22. package/dist/core/config.d.ts +10 -2
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/prompt-builder.d.ts +3 -6
  27. package/dist/core/prompt-builder.d.ts.map +1 -1
  28. package/dist/git/git-utils.d.ts +31 -0
  29. package/dist/git/git-utils.d.ts.map +1 -0
  30. package/dist/git/index.d.ts +3 -0
  31. package/dist/git/index.d.ts.map +1 -0
  32. package/dist/git/pr-service.d.ts +66 -0
  33. package/dist/git/pr-service.d.ts.map +1 -0
  34. package/dist/index-node.d.ts +5 -1
  35. package/dist/index-node.d.ts.map +1 -1
  36. package/dist/index-node.js +2560 -729
  37. package/dist/index.d.ts +0 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +17 -49
  40. package/dist/modules/auth.d.ts +3 -0
  41. package/dist/modules/auth.d.ts.map +1 -1
  42. package/dist/modules/tasks.d.ts +0 -5
  43. package/dist/modules/tasks.d.ts.map +1 -1
  44. package/dist/modules/workspaces.d.ts +10 -10
  45. package/dist/modules/workspaces.d.ts.map +1 -1
  46. package/dist/orchestrator.d.ts +38 -5
  47. package/dist/orchestrator.d.ts.map +1 -1
  48. package/dist/planning/agents/architect.d.ts +15 -0
  49. package/dist/planning/agents/architect.d.ts.map +1 -0
  50. package/dist/planning/agents/sprint-organizer.d.ts +14 -0
  51. package/dist/planning/agents/sprint-organizer.d.ts.map +1 -0
  52. package/dist/planning/agents/tech-lead.d.ts +15 -0
  53. package/dist/planning/agents/tech-lead.d.ts.map +1 -0
  54. package/dist/planning/index.d.ts +4 -0
  55. package/dist/planning/index.d.ts.map +1 -0
  56. package/dist/planning/plan-manager.d.ts +52 -0
  57. package/dist/planning/plan-manager.d.ts.map +1 -0
  58. package/dist/planning/planning-meeting.d.ts +36 -0
  59. package/dist/planning/planning-meeting.d.ts.map +1 -0
  60. package/dist/planning/sprint-plan.d.ts +47 -0
  61. package/dist/planning/sprint-plan.d.ts.map +1 -0
  62. package/dist/project/knowledge-base.d.ts +25 -0
  63. package/dist/project/knowledge-base.d.ts.map +1 -0
  64. package/dist/worktree/index.d.ts +3 -0
  65. package/dist/worktree/index.d.ts.map +1 -0
  66. package/dist/worktree/worktree-config.d.ts +58 -0
  67. package/dist/worktree/worktree-config.d.ts.map +1 -0
  68. package/dist/worktree/worktree-manager.d.ts +96 -0
  69. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  70. package/package.json +2 -2
  71. package/dist/modules/ai.d.ts +0 -55
  72. package/dist/modules/ai.d.ts.map +0 -1
@@ -52,8 +52,7 @@ __export(exports_src, {
52
52
  InvitationsModule: () => InvitationsModule,
53
53
  DocsModule: () => DocsModule,
54
54
  CiModule: () => CiModule,
55
- AuthModule: () => AuthModule,
56
- AIModule: () => AIModule
55
+ AuthModule: () => AuthModule
57
56
  });
58
57
  module.exports = __toCommonJS(exports_src);
59
58
  var import_axios = __toESM(require("axios"));
@@ -86,43 +85,6 @@ class BaseModule {
86
85
  }
87
86
  }
88
87
 
89
- // src/modules/ai.ts
90
- class AIModule extends BaseModule {
91
- async chat(workspaceId, body) {
92
- const { data } = await this.api.post(`/ai/${workspaceId}/chat`, body, { timeout: 300000 });
93
- return data;
94
- }
95
- async detectIntent(workspaceId, body) {
96
- const { data } = await this.api.post(`/ai/${workspaceId}/chat/intent`, body, { timeout: 300000 });
97
- return data;
98
- }
99
- async executeIntent(workspaceId, body) {
100
- const { data } = await this.api.post(`/ai/${workspaceId}/chat/execute`, body, { timeout: 300000 });
101
- return data;
102
- }
103
- async listSessions(workspaceId) {
104
- const { data } = await this.api.get(`/ai/${workspaceId}/sessions`);
105
- return data;
106
- }
107
- async getSession(workspaceId, sessionId) {
108
- const { data } = await this.api.get(`/ai/${workspaceId}/session/${sessionId}`);
109
- return data;
110
- }
111
- getChatStreamUrl(workspaceId, sessionId) {
112
- return `${this.api.defaults.baseURL}/ai/${workspaceId}/chat/stream?sessionId=${sessionId}`;
113
- }
114
- async deleteSession(workspaceId, sessionId) {
115
- await this.api.delete(`/ai/${workspaceId}/session/${sessionId}`);
116
- }
117
- async shareSession(workspaceId, sessionId, body) {
118
- await this.api.post(`/ai/${workspaceId}/session/${sessionId}/share`, body);
119
- }
120
- async getSharedSession(sessionId) {
121
- const { data } = await this.api.get(`/ai/shared/${sessionId}`);
122
- return data;
123
- }
124
- }
125
-
126
88
  // src/modules/auth.ts
127
89
  class AuthModule extends BaseModule {
128
90
  async getProfile() {
@@ -149,6 +111,10 @@ class AuthModule extends BaseModule {
149
111
  const { data } = await this.api.post("/auth/complete-registration", body);
150
112
  return data;
151
113
  }
114
+ async deleteAccount() {
115
+ const { data } = await this.api.delete("/auth/account");
116
+ return data;
117
+ }
152
118
  }
153
119
 
154
120
  // src/modules/ci.ts
@@ -346,10 +312,6 @@ class TasksModule extends BaseModule {
346
312
  updates
347
313
  });
348
314
  }
349
- async getContext(id, workspaceId) {
350
- const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}/context`);
351
- return data;
352
- }
353
315
  }
354
316
 
355
317
  // src/modules/workspaces.ts
@@ -386,10 +348,6 @@ class WorkspacesModule extends BaseModule {
386
348
  const { data } = await this.api.get(`/workspaces/${id}/stats`);
387
349
  return data;
388
350
  }
389
- async getManifestStatus(workspaceId) {
390
- const { data } = await this.api.get(`/workspaces/${workspaceId}/manifest-status`);
391
- return data;
392
- }
393
351
  async getActivity(id, limit) {
394
352
  const { data } = await this.api.get(`/workspaces/${id}/activity`, {
395
353
  params: { limit }
@@ -400,6 +358,18 @@ class WorkspacesModule extends BaseModule {
400
358
  const { data } = await this.api.post(`/workspaces/${id}/dispatch`, { workerId, sprintId });
401
359
  return data.task;
402
360
  }
361
+ async heartbeat(workspaceId, agentId, currentTaskId, status) {
362
+ const { data } = await this.api.post(`/workspaces/${workspaceId}/agents/heartbeat`, {
363
+ agentId,
364
+ currentTaskId: currentTaskId ?? null,
365
+ status: status ?? "WORKING"
366
+ });
367
+ return data.agent;
368
+ }
369
+ async getAgents(workspaceId) {
370
+ const { data } = await this.api.get(`/workspaces/${workspaceId}/agents`);
371
+ return data.agents;
372
+ }
403
373
  async listApiKeys(workspaceId) {
404
374
  const { data } = await this.api.get(`/workspaces/${workspaceId}/api-keys`);
405
375
  return data.apiKeys;
@@ -418,7 +388,6 @@ class LocusClient {
418
388
  api;
419
389
  emitter;
420
390
  auth;
421
- ai;
422
391
  tasks;
423
392
  sprints;
424
393
  workspaces;
@@ -438,7 +407,6 @@ class LocusClient {
438
407
  });
439
408
  this.setupInterceptors();
440
409
  this.auth = new AuthModule(this.api, this.emitter);
441
- this.ai = new AIModule(this.api, this.emitter);
442
410
  this.tasks = new TasksModule(this.api, this.emitter);
443
411
  this.sprints = new SprintsModule(this.api, this.emitter);
444
412
  this.workspaces = new WorkspacesModule(this.api, this.emitter);
@@ -512,8 +480,7 @@ __export(exports_worker, {
512
480
  AgentWorker: () => AgentWorker
513
481
  });
514
482
  module.exports = __toCommonJS(exports_worker);
515
- var import_node_fs5 = require("node:fs");
516
- var import_node_path7 = require("node:path");
483
+ var import_shared3 = require("@locusai/shared");
517
484
 
518
485
  // src/core/config.ts
519
486
  var import_node_path = require("node:path");
@@ -523,19 +490,27 @@ var PROVIDER = {
523
490
  };
524
491
  var DEFAULT_MODEL = {
525
492
  [PROVIDER.CLAUDE]: "opus",
526
- [PROVIDER.CODEX]: "gpt-5.2-codex"
493
+ [PROVIDER.CODEX]: "gpt-5.3-codex"
494
+ };
495
+ var LOCUS_SCHEMA_BASE_URL = "https://locusai.dev/schemas";
496
+ var LOCUS_SCHEMAS = {
497
+ config: `${LOCUS_SCHEMA_BASE_URL}/config.schema.json`,
498
+ settings: `${LOCUS_SCHEMA_BASE_URL}/settings.schema.json`
527
499
  };
528
500
  var LOCUS_CONFIG = {
529
501
  dir: ".locus",
530
502
  configFile: "config.json",
503
+ settingsFile: "settings.json",
531
504
  indexFile: "codebase-index.json",
532
- contextFile: "CLAUDE.md",
505
+ contextFile: "LOCUS.md",
533
506
  artifactsDir: "artifacts",
534
507
  documentsDir: "documents",
535
- agentSkillsDir: ".agent/skills",
536
508
  sessionsDir: "sessions",
537
509
  reviewsDir: "reviews",
538
- plansDir: "plans"
510
+ plansDir: "plans",
511
+ projectDir: "project",
512
+ projectContextFile: "context.md",
513
+ projectProgressFile: "progress.md"
539
514
  };
540
515
  var LOCUS_GITIGNORE_PATTERNS = [
541
516
  "# Locus AI - Session data (user-specific, can grow large)",
@@ -548,11 +523,17 @@ var LOCUS_GITIGNORE_PATTERNS = [
548
523
  ".locus/reviews/",
549
524
  "",
550
525
  "# Locus AI - Plans (generated per task)",
551
- ".locus/plans/"
526
+ ".locus/plans/",
527
+ "",
528
+ "# Locus AI - Agent worktrees (parallel execution)",
529
+ ".locus-worktrees/",
530
+ "",
531
+ "# Locus AI - Settings (contains API key, telegram config, etc.)",
532
+ ".locus/settings.json"
552
533
  ];
553
534
  function getLocusPath(projectPath, fileName) {
554
- if (fileName === "contextFile") {
555
- return import_node_path.join(projectPath, LOCUS_CONFIG.contextFile);
535
+ if (fileName === "projectContextFile" || fileName === "projectProgressFile") {
536
+ return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.projectDir, LOCUS_CONFIG[fileName]);
556
537
  }
557
538
  return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
558
539
  }
@@ -628,6 +609,14 @@ var c = {
628
609
  };
629
610
 
630
611
  // src/ai/claude-runner.ts
612
+ var SANDBOX_SETTINGS = JSON.stringify({
613
+ sandbox: {
614
+ enabled: true,
615
+ autoAllow: true,
616
+ allowUnsandboxedCommands: false
617
+ }
618
+ });
619
+
631
620
  class ClaudeRunner {
632
621
  model;
633
622
  log;
@@ -635,6 +624,7 @@ class ClaudeRunner {
635
624
  eventEmitter;
636
625
  currentToolName;
637
626
  activeTools = new Map;
627
+ activeProcess = null;
638
628
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
639
629
  this.model = model;
640
630
  this.log = log;
@@ -643,6 +633,12 @@ class ClaudeRunner {
643
633
  setEventEmitter(emitter) {
644
634
  this.eventEmitter = emitter;
645
635
  }
636
+ abort() {
637
+ if (this.activeProcess && !this.activeProcess.killed) {
638
+ this.activeProcess.kill("SIGTERM");
639
+ this.activeProcess = null;
640
+ }
641
+ }
646
642
  async run(prompt) {
647
643
  const maxRetries = 3;
648
644
  let lastError = null;
@@ -671,7 +667,9 @@ class ClaudeRunner {
671
667
  "stream-json",
672
668
  "--include-partial-messages",
673
669
  "--model",
674
- this.model
670
+ this.model,
671
+ "--settings",
672
+ SANDBOX_SETTINGS
675
673
  ];
676
674
  const env = {
677
675
  ...process.env,
@@ -688,6 +686,7 @@ class ClaudeRunner {
688
686
  stdio: ["pipe", "pipe", "pipe"],
689
687
  env
690
688
  });
689
+ this.activeProcess = claude;
691
690
  let buffer = "";
692
691
  let stderrBuffer = "";
693
692
  let resolveChunk = null;
@@ -755,6 +754,7 @@ class ClaudeRunner {
755
754
  signalEnd();
756
755
  });
757
756
  claude.on("close", (code) => {
757
+ this.activeProcess = null;
758
758
  if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
759
759
  process.stderr.write(`${stderrBuffer}
760
760
  `);
@@ -912,7 +912,9 @@ class ClaudeRunner {
912
912
  "stream-json",
913
913
  "--include-partial-messages",
914
914
  "--model",
915
- this.model
915
+ this.model,
916
+ "--settings",
917
+ SANDBOX_SETTINGS
916
918
  ];
917
919
  const env = {
918
920
  ...process.env,
@@ -924,6 +926,7 @@ class ClaudeRunner {
924
926
  stdio: ["pipe", "pipe", "pipe"],
925
927
  env
926
928
  });
929
+ this.activeProcess = claude;
927
930
  let finalResult = "";
928
931
  let errorOutput = "";
929
932
  let buffer = "";
@@ -957,6 +960,7 @@ class ClaudeRunner {
957
960
  reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
958
961
  });
959
962
  claude.on("close", (code) => {
963
+ this.activeProcess = null;
960
964
  if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
961
965
  process.stderr.write(`${stderrBuffer}
962
966
  `);
@@ -1023,11 +1027,18 @@ class CodexRunner {
1023
1027
  projectPath;
1024
1028
  model;
1025
1029
  log;
1030
+ activeProcess = null;
1026
1031
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
1027
1032
  this.projectPath = projectPath;
1028
1033
  this.model = model;
1029
1034
  this.log = log;
1030
1035
  }
1036
+ abort() {
1037
+ if (this.activeProcess && !this.activeProcess.killed) {
1038
+ this.activeProcess.kill("SIGTERM");
1039
+ this.activeProcess = null;
1040
+ }
1041
+ }
1031
1042
  async run(prompt) {
1032
1043
  const maxRetries = 3;
1033
1044
  let lastError = null;
@@ -1054,6 +1065,7 @@ class CodexRunner {
1054
1065
  env: process.env,
1055
1066
  shell: false
1056
1067
  });
1068
+ this.activeProcess = codex;
1057
1069
  let resolveChunk = null;
1058
1070
  const chunkQueue = [];
1059
1071
  let processEnded = false;
@@ -1103,6 +1115,7 @@ class CodexRunner {
1103
1115
  signalEnd();
1104
1116
  });
1105
1117
  codex.on("close", (code) => {
1118
+ this.activeProcess = null;
1106
1119
  this.cleanupTempFile(outputPath);
1107
1120
  if (code === 0) {
1108
1121
  const result = this.readOutput(outputPath, finalOutput);
@@ -1148,6 +1161,7 @@ class CodexRunner {
1148
1161
  env: process.env,
1149
1162
  shell: false
1150
1163
  });
1164
+ this.activeProcess = codex;
1151
1165
  let output = "";
1152
1166
  let errorOutput = "";
1153
1167
  const handleOutput = (data) => {
@@ -1165,6 +1179,7 @@ class CodexRunner {
1165
1179
  reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
1166
1180
  });
1167
1181
  codex.on("close", (code) => {
1182
+ this.activeProcess = null;
1168
1183
  this.cleanupTempFile(outputPath);
1169
1184
  if (code === 0) {
1170
1185
  resolve2(this.readOutput(outputPath, output));
@@ -1179,7 +1194,8 @@ class CodexRunner {
1179
1194
  buildArgs(outputPath) {
1180
1195
  const args = [
1181
1196
  "exec",
1182
- "--full-auto",
1197
+ "--sandbox",
1198
+ "workspace-write",
1183
1199
  "--skip-git-repo-check",
1184
1200
  "--output-last-message",
1185
1201
  outputPath
@@ -1246,375 +1262,745 @@ function createAiRunner(provider, config) {
1246
1262
  }
1247
1263
  }
1248
1264
 
1249
- // src/core/indexer.ts
1250
- var import_node_crypto2 = require("node:crypto");
1251
- var import_node_fs2 = require("node:fs");
1252
- var import_node_path4 = require("node:path");
1253
- var import_globby = require("globby");
1265
+ // src/git/git-utils.ts
1266
+ var import_node_child_process3 = require("node:child_process");
1267
+ function isGitAvailable() {
1268
+ try {
1269
+ import_node_child_process3.execFileSync("git", ["--version"], {
1270
+ encoding: "utf-8",
1271
+ stdio: ["pipe", "pipe", "pipe"]
1272
+ });
1273
+ return true;
1274
+ } catch {
1275
+ return false;
1276
+ }
1277
+ }
1278
+ function isGhAvailable(projectPath) {
1279
+ try {
1280
+ import_node_child_process3.execFileSync("gh", ["auth", "status"], {
1281
+ cwd: projectPath,
1282
+ encoding: "utf-8",
1283
+ stdio: ["pipe", "pipe", "pipe"]
1284
+ });
1285
+ return true;
1286
+ } catch {
1287
+ return false;
1288
+ }
1289
+ }
1290
+ function getGhUsername() {
1291
+ try {
1292
+ const output = import_node_child_process3.execFileSync("gh", ["api", "user", "--jq", ".login"], {
1293
+ encoding: "utf-8",
1294
+ stdio: ["pipe", "pipe", "pipe"]
1295
+ }).trim();
1296
+ return output || null;
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ }
1301
+ function detectRemoteProvider(projectPath) {
1302
+ const url = getRemoteUrl(projectPath);
1303
+ if (!url)
1304
+ return "unknown";
1305
+ if (url.includes("github.com"))
1306
+ return "github";
1307
+ if (url.includes("gitlab.com") || url.includes("gitlab"))
1308
+ return "gitlab";
1309
+ if (url.includes("bitbucket.org"))
1310
+ return "bitbucket";
1311
+ return "unknown";
1312
+ }
1313
+ function getRemoteUrl(projectPath, remote = "origin") {
1314
+ try {
1315
+ return import_node_child_process3.execFileSync("git", ["remote", "get-url", remote], {
1316
+ cwd: projectPath,
1317
+ encoding: "utf-8",
1318
+ stdio: ["pipe", "pipe", "pipe"]
1319
+ }).trim();
1320
+ } catch {
1321
+ return null;
1322
+ }
1323
+ }
1324
+ function getCurrentBranch(projectPath) {
1325
+ return import_node_child_process3.execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1326
+ cwd: projectPath,
1327
+ encoding: "utf-8",
1328
+ stdio: ["pipe", "pipe", "pipe"]
1329
+ }).trim();
1330
+ }
1331
+ function getDefaultBranch(projectPath, remote = "origin") {
1332
+ try {
1333
+ const ref = import_node_child_process3.execFileSync("git", ["symbolic-ref", `refs/remotes/${remote}/HEAD`], {
1334
+ cwd: projectPath,
1335
+ encoding: "utf-8",
1336
+ stdio: ["pipe", "pipe", "pipe"]
1337
+ }).trim();
1338
+ return ref.replace(`refs/remotes/${remote}/`, "");
1339
+ } catch {
1340
+ for (const candidate of ["main", "master"]) {
1341
+ try {
1342
+ import_node_child_process3.execFileSync("git", ["ls-remote", "--exit-code", "--heads", remote, candidate], {
1343
+ cwd: projectPath,
1344
+ encoding: "utf-8",
1345
+ stdio: ["pipe", "pipe", "pipe"]
1346
+ });
1347
+ return candidate;
1348
+ } catch {}
1349
+ }
1350
+ try {
1351
+ return getCurrentBranch(projectPath);
1352
+ } catch {
1353
+ return "main";
1354
+ }
1355
+ }
1356
+ }
1254
1357
 
1255
- class CodebaseIndexer {
1358
+ // src/git/pr-service.ts
1359
+ var import_node_child_process4 = require("node:child_process");
1360
+ class PrService {
1256
1361
  projectPath;
1257
- indexPath;
1258
- fullReindexRatioThreshold = 0.2;
1259
- constructor(projectPath) {
1362
+ log;
1363
+ constructor(projectPath, log) {
1260
1364
  this.projectPath = projectPath;
1261
- this.indexPath = import_node_path4.join(projectPath, ".locus", "codebase-index.json");
1365
+ this.log = log;
1366
+ }
1367
+ createPr(options) {
1368
+ const {
1369
+ task,
1370
+ branch,
1371
+ baseBranch: requestedBaseBranch,
1372
+ agentId,
1373
+ summary
1374
+ } = options;
1375
+ const provider = detectRemoteProvider(this.projectPath);
1376
+ if (provider !== "github") {
1377
+ throw new Error(`PR creation is only supported for GitHub repositories (detected: ${provider})`);
1378
+ }
1379
+ if (!isGhAvailable(this.projectPath)) {
1380
+ throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
1381
+ }
1382
+ const title = `[Locus] ${task.title}`;
1383
+ const body = this.buildPrBody(task, agentId, summary);
1384
+ const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
1385
+ this.validateCreatePrInputs(baseBranch, branch);
1386
+ this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
1387
+ const output = import_node_child_process4.execFileSync("gh", [
1388
+ "pr",
1389
+ "create",
1390
+ "--title",
1391
+ title,
1392
+ "--body",
1393
+ body,
1394
+ "--base",
1395
+ baseBranch,
1396
+ "--head",
1397
+ branch
1398
+ ], {
1399
+ cwd: this.projectPath,
1400
+ encoding: "utf-8",
1401
+ stdio: ["pipe", "pipe", "pipe"]
1402
+ }).trim();
1403
+ const url = output;
1404
+ const prNumber = this.extractPrNumber(url);
1405
+ this.log(`PR created: ${url}`, "success");
1406
+ return { url, number: prNumber };
1262
1407
  }
1263
- async index(onProgress, treeSummarizer, force = false) {
1264
- if (!treeSummarizer) {
1265
- throw new Error("A treeSummarizer is required for this indexing method.");
1408
+ validateCreatePrInputs(baseBranch, headBranch) {
1409
+ if (!this.hasRemoteBranch(baseBranch)) {
1410
+ throw new Error(`Base branch "${baseBranch}" does not exist on origin. Push/fetch refs and retry.`);
1266
1411
  }
1267
- onProgress?.("Generating file tree...");
1268
- const currentFiles = await this.getFileTree();
1269
- const treeString = currentFiles.join(`
1270
- `);
1271
- const newTreeHash = this.hashTree(treeString);
1272
- const existingIndex = this.loadIndex();
1273
- const currentHashes = this.computeFileHashes(currentFiles);
1274
- const existingHashes = existingIndex?.fileHashes;
1275
- const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
1276
- const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
1277
- if (canIncremental) {
1278
- onProgress?.("Performing incremental update");
1279
- const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
1280
- const changedFiles = [...added, ...modified];
1281
- const totalChanges = changedFiles.length + deleted.length;
1282
- const existingFileCount = Object.keys(existingHashes).length;
1283
- onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
1284
- if (existingFileCount > 0) {
1285
- const changeRatio = totalChanges / existingFileCount;
1286
- if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
1287
- onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
1288
- const incrementalIndex = await treeSummarizer(changedFiles.join(`
1289
- `));
1290
- const updatedIndex = this.cloneIndex(existingIndex);
1291
- this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
1292
- return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
1293
- }
1294
- if (changedFiles.length === 0 && deleted.length > 0) {
1295
- onProgress?.(`Removing ${deleted.length} deleted files from index`);
1296
- const updatedIndex = this.cloneIndex(existingIndex);
1297
- this.removeFilesFromIndex(updatedIndex, deleted);
1298
- return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
1299
- }
1300
- if (changedFiles.length === 0 && deleted.length === 0) {
1301
- onProgress?.("No actual file changes, updating hashes only");
1302
- const updatedIndex = this.cloneIndex(existingIndex);
1303
- return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
1304
- }
1305
- onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
1306
- }
1412
+ if (!this.hasRemoteBranch(headBranch)) {
1413
+ throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
1307
1414
  }
1308
- onProgress?.("AI is analyzing codebase structure...");
1309
- try {
1310
- const index = await treeSummarizer(treeString);
1311
- return this.applyIndexMetadata(index, currentHashes, newTreeHash);
1312
- } catch (error) {
1313
- throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : String(error)}`);
1415
+ const baseRef = this.resolveBranchRef(baseBranch);
1416
+ const headRef = this.resolveBranchRef(headBranch);
1417
+ if (!baseRef) {
1418
+ throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
1314
1419
  }
1315
- }
1316
- async getFileTree() {
1317
- const gitmodulesPath = import_node_path4.join(this.projectPath, ".gitmodules");
1318
- const submoduleIgnores = [];
1319
- if (import_node_fs2.existsSync(gitmodulesPath)) {
1320
- try {
1321
- const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
1322
- const lines = content.split(`
1323
- `);
1324
- for (const line of lines) {
1325
- const match = line.match(/^\s*path\s*=\s*(.*)$/);
1326
- const path = match?.[1]?.trim();
1327
- if (path) {
1328
- submoduleIgnores.push(`${path}/**`);
1329
- submoduleIgnores.push(`**/${path}/**`);
1330
- }
1331
- }
1332
- } catch {}
1420
+ if (!headRef) {
1421
+ throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
1333
1422
  }
1334
- return import_globby.globby(["**/*"], {
1423
+ const commitsAhead = this.countCommitsAhead(baseRef, headRef);
1424
+ if (commitsAhead <= 0) {
1425
+ throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
1426
+ }
1427
+ }
1428
+ countCommitsAhead(baseRef, headRef) {
1429
+ const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
1335
1430
  cwd: this.projectPath,
1336
- gitignore: true,
1337
- ignore: [
1338
- ...submoduleIgnores,
1339
- "**/node_modules/**",
1340
- "**/dist/**",
1341
- "**/build/**",
1342
- "**/target/**",
1343
- "**/bin/**",
1344
- "**/obj/**",
1345
- "**/.next/**",
1346
- "**/.svelte-kit/**",
1347
- "**/.nuxt/**",
1348
- "**/.cache/**",
1349
- "**/out/**",
1350
- "**/__tests__/**",
1351
- "**/coverage/**",
1352
- "**/*.test.*",
1353
- "**/*.spec.*",
1354
- "**/*.d.ts",
1355
- "**/tsconfig.tsbuildinfo",
1356
- "**/.locus/*.json",
1357
- "**/.locus/*.md",
1358
- "**/.locus/!(artifacts)/**",
1359
- "**/.git/**",
1360
- "**/.svn/**",
1361
- "**/.hg/**",
1362
- "**/.vscode/**",
1363
- "**/.idea/**",
1364
- "**/.DS_Store",
1365
- "**/bun.lock",
1366
- "**/package-lock.json",
1367
- "**/yarn.lock",
1368
- "**/pnpm-lock.yaml",
1369
- "**/Cargo.lock",
1370
- "**/go.sum",
1371
- "**/poetry.lock",
1372
- "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
1373
- ]
1374
- });
1431
+ encoding: "utf-8",
1432
+ stdio: ["pipe", "pipe", "pipe"]
1433
+ }).trim();
1434
+ const value = Number.parseInt(output || "0", 10);
1435
+ return Number.isNaN(value) ? 0 : value;
1375
1436
  }
1376
- loadIndex() {
1377
- if (import_node_fs2.existsSync(this.indexPath)) {
1378
- try {
1379
- return JSON.parse(import_node_fs2.readFileSync(this.indexPath, "utf-8"));
1380
- } catch {
1381
- return null;
1382
- }
1437
+ resolveBranchRef(branch) {
1438
+ if (this.hasLocalBranch(branch)) {
1439
+ return branch;
1440
+ }
1441
+ if (this.hasRemoteTrackingBranch(branch)) {
1442
+ return `origin/${branch}`;
1383
1443
  }
1384
1444
  return null;
1385
1445
  }
1386
- saveIndex(index) {
1387
- const dir = import_node_path4.dirname(this.indexPath);
1388
- if (!import_node_fs2.existsSync(dir)) {
1389
- import_node_fs2.mkdirSync(dir, { recursive: true });
1446
+ hasLocalBranch(branch) {
1447
+ try {
1448
+ import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
1449
+ cwd: this.projectPath,
1450
+ stdio: ["pipe", "pipe", "pipe"]
1451
+ });
1452
+ return true;
1453
+ } catch {
1454
+ return false;
1390
1455
  }
1391
- import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
1392
1456
  }
1393
- cloneIndex(index) {
1394
- return JSON.parse(JSON.stringify(index));
1457
+ hasRemoteTrackingBranch(branch) {
1458
+ try {
1459
+ import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
1460
+ cwd: this.projectPath,
1461
+ stdio: ["pipe", "pipe", "pipe"]
1462
+ });
1463
+ return true;
1464
+ } catch {
1465
+ return false;
1466
+ }
1395
1467
  }
1396
- applyIndexMetadata(index, fileHashes, treeHash) {
1397
- index.lastIndexed = new Date().toISOString();
1398
- index.treeHash = treeHash;
1399
- index.fileHashes = fileHashes;
1400
- return index;
1468
+ hasRemoteBranch(branch) {
1469
+ try {
1470
+ import_node_child_process4.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
1471
+ cwd: this.projectPath,
1472
+ stdio: ["pipe", "pipe", "pipe"]
1473
+ });
1474
+ return true;
1475
+ } catch {
1476
+ return false;
1477
+ }
1478
+ }
1479
+ getPrDiff(branch) {
1480
+ return import_node_child_process4.execFileSync("gh", ["pr", "diff", branch], {
1481
+ cwd: this.projectPath,
1482
+ encoding: "utf-8",
1483
+ stdio: ["pipe", "pipe", "pipe"],
1484
+ maxBuffer: 10 * 1024 * 1024
1485
+ });
1401
1486
  }
1402
- hashTree(tree) {
1403
- return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
1487
+ submitReview(prIdentifier, body, event) {
1488
+ try {
1489
+ import_node_child_process4.execFileSync("gh", [
1490
+ "pr",
1491
+ "review",
1492
+ prIdentifier,
1493
+ "--body",
1494
+ body,
1495
+ `--${event.toLowerCase().replace("_", "-")}`
1496
+ ], {
1497
+ cwd: this.projectPath,
1498
+ encoding: "utf-8",
1499
+ stdio: ["pipe", "pipe", "pipe"]
1500
+ });
1501
+ } catch (err) {
1502
+ const msg = err instanceof Error ? err.message : String(err);
1503
+ if (event === "REQUEST_CHANGES" && msg.includes("own pull request")) {
1504
+ import_node_child_process4.execFileSync("gh", ["pr", "review", prIdentifier, "--body", body, "--comment"], {
1505
+ cwd: this.projectPath,
1506
+ encoding: "utf-8",
1507
+ stdio: ["pipe", "pipe", "pipe"]
1508
+ });
1509
+ return;
1510
+ }
1511
+ throw err;
1512
+ }
1404
1513
  }
1405
- hashFile(filePath) {
1514
+ listLocusPrs() {
1406
1515
  try {
1407
- const content = import_node_fs2.readFileSync(import_node_path4.join(this.projectPath, filePath), "utf-8");
1408
- return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
1516
+ const output = import_node_child_process4.execFileSync("gh", [
1517
+ "pr",
1518
+ "list",
1519
+ "--search",
1520
+ "[Locus] in:title",
1521
+ "--state",
1522
+ "open",
1523
+ "--json",
1524
+ "number,title,url,headRefName"
1525
+ ], {
1526
+ cwd: this.projectPath,
1527
+ encoding: "utf-8",
1528
+ stdio: ["pipe", "pipe", "pipe"]
1529
+ }).trim();
1530
+ const prs = JSON.parse(output || "[]");
1531
+ return prs.map((pr) => ({
1532
+ number: pr.number,
1533
+ title: pr.title,
1534
+ url: pr.url,
1535
+ branch: pr.headRefName
1536
+ }));
1409
1537
  } catch {
1410
- return null;
1538
+ this.log("Failed to list Locus PRs", "warn");
1539
+ return [];
1411
1540
  }
1412
1541
  }
1413
- computeFileHashes(files) {
1414
- const hashes = {};
1415
- for (const file of files) {
1416
- const hash = this.hashFile(file);
1417
- if (hash !== null) {
1418
- hashes[file] = hash;
1419
- }
1542
+ hasLocusReview(prNumber) {
1543
+ try {
1544
+ const output = import_node_child_process4.execFileSync("gh", ["pr", "view", prNumber, "--json", "reviews"], {
1545
+ cwd: this.projectPath,
1546
+ encoding: "utf-8",
1547
+ stdio: ["pipe", "pipe", "pipe"]
1548
+ }).trim();
1549
+ const data = JSON.parse(output || "{}");
1550
+ return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
1551
+ } catch {
1552
+ return false;
1420
1553
  }
1421
- return hashes;
1422
1554
  }
1423
- diffFiles(currentHashes, existingHashes) {
1424
- const currentFiles = Object.keys(currentHashes);
1425
- const existingFiles = Object.keys(existingHashes);
1426
- const existingSet = new Set(existingFiles);
1427
- const currentSet = new Set(currentFiles);
1428
- const added = currentFiles.filter((f) => !existingSet.has(f));
1429
- const deleted = existingFiles.filter((f) => !currentSet.has(f));
1430
- const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
1431
- return { added, deleted, modified };
1555
+ listUnreviewedLocusPrs() {
1556
+ const allPrs = this.listLocusPrs();
1557
+ return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
1432
1558
  }
1433
- removeFilesFromIndex(index, files) {
1434
- const fileSet = new Set(files);
1435
- for (const file of files) {
1436
- delete index.responsibilities[file];
1559
+ buildPrBody(task, agentId, summary) {
1560
+ const sections = [];
1561
+ sections.push(`## Task: ${task.title}`);
1562
+ sections.push("");
1563
+ if (task.description) {
1564
+ sections.push(task.description);
1565
+ sections.push("");
1437
1566
  }
1438
- for (const [symbol, paths] of Object.entries(index.symbols)) {
1439
- index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
1440
- if (index.symbols[symbol].length === 0) {
1441
- delete index.symbols[symbol];
1567
+ if (task.acceptanceChecklist?.length > 0) {
1568
+ sections.push("## Acceptance Criteria");
1569
+ for (const item of task.acceptanceChecklist) {
1570
+ sections.push(`- [ ] ${item.text}`);
1442
1571
  }
1572
+ sections.push("");
1443
1573
  }
1444
- }
1445
- mergeIndex(existing, incremental, newHashes, newTreeHash) {
1446
- const mergedSymbols = { ...existing.symbols };
1447
- for (const [symbol, paths] of Object.entries(incremental.symbols)) {
1448
- if (mergedSymbols[symbol]) {
1449
- mergedSymbols[symbol] = [
1450
- ...new Set([...mergedSymbols[symbol], ...paths])
1451
- ];
1452
- } else {
1453
- mergedSymbols[symbol] = paths;
1454
- }
1574
+ if (summary) {
1575
+ sections.push("## Agent Summary");
1576
+ sections.push(summary);
1577
+ sections.push("");
1455
1578
  }
1456
- const merged = {
1457
- symbols: mergedSymbols,
1458
- responsibilities: {
1459
- ...existing.responsibilities,
1460
- ...incremental.responsibilities
1461
- },
1462
- lastIndexed: ""
1463
- };
1464
- return this.applyIndexMetadata(merged, newHashes, newTreeHash);
1579
+ sections.push("---");
1580
+ sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task.id}\``);
1581
+ return sections.join(`
1582
+ `);
1583
+ }
1584
+ extractPrNumber(url) {
1585
+ const match = url.match(/\/pull\/(\d+)/);
1586
+ return match ? Number.parseInt(match[1], 10) : 0;
1465
1587
  }
1466
1588
  }
1467
1589
 
1468
- // src/agent/codebase-indexer-service.ts
1469
- class CodebaseIndexerService {
1470
- deps;
1471
- indexer;
1472
- constructor(deps) {
1473
- this.deps = deps;
1474
- this.indexer = new CodebaseIndexer(deps.projectPath);
1590
+ // src/project/knowledge-base.ts
1591
+ var import_node_fs2 = require("node:fs");
1592
+ var import_node_path4 = require("node:path");
1593
+ class KnowledgeBase {
1594
+ contextPath;
1595
+ progressPath;
1596
+ constructor(projectPath) {
1597
+ this.contextPath = getLocusPath(projectPath, "projectContextFile");
1598
+ this.progressPath = getLocusPath(projectPath, "projectProgressFile");
1475
1599
  }
1476
- async reindex(force = false) {
1477
- try {
1478
- const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
1479
- const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
1480
- 1. Key symbols (classes, functions, types) and their locations
1481
- 2. Responsibilities of each directory/file
1482
- 3. Overall project structure
1600
+ readContext() {
1601
+ if (!import_node_fs2.existsSync(this.contextPath)) {
1602
+ return "";
1603
+ }
1604
+ return import_node_fs2.readFileSync(this.contextPath, "utf-8");
1605
+ }
1606
+ readProgress() {
1607
+ if (!import_node_fs2.existsSync(this.progressPath)) {
1608
+ return "";
1609
+ }
1610
+ return import_node_fs2.readFileSync(this.progressPath, "utf-8");
1611
+ }
1612
+ updateContext(content) {
1613
+ this.ensureDir(this.contextPath);
1614
+ import_node_fs2.writeFileSync(this.contextPath, content);
1615
+ }
1616
+ updateProgress(event) {
1617
+ this.ensureDir(this.progressPath);
1618
+ const existing = this.readProgress();
1619
+ const timestamp = (event.timestamp ?? new Date).toISOString();
1620
+ let entry = "";
1621
+ switch (event.type) {
1622
+ case "task_completed":
1623
+ entry = `- [x] ${event.title} — completed ${timestamp}`;
1624
+ break;
1625
+ case "sprint_started":
1626
+ entry = `
1627
+ ## Current Sprint: ${event.title}
1628
+ **Status:** ACTIVE | Started: ${timestamp}
1629
+ `;
1630
+ break;
1631
+ case "sprint_completed":
1632
+ entry = `
1633
+ ### Sprint Completed: ${event.title} — ${timestamp}
1634
+ `;
1635
+ break;
1636
+ case "blocker":
1637
+ entry = `- BLOCKER: ${event.title}`;
1638
+ break;
1639
+ case "pr_opened":
1640
+ entry = `- [ ] ${event.title} — PR opened ${timestamp}`;
1641
+ break;
1642
+ case "pr_reviewed":
1643
+ entry = `- ${event.title} — reviewed ${timestamp}`;
1644
+ break;
1645
+ case "pr_merged":
1646
+ entry = `- [x] ${event.title} — PR merged ${timestamp}`;
1647
+ break;
1648
+ case "exec_completed":
1649
+ entry = `- [x] ${event.title} — exec ${timestamp}`;
1650
+ break;
1651
+ }
1652
+ if (event.details) {
1653
+ entry += `
1654
+ ${event.details}`;
1655
+ }
1656
+ const updated = existing ? `${existing}
1657
+ ${entry}` : `# Project Progress
1483
1658
 
1484
- Analyze this file tree and provide a JSON response with:
1485
- - "symbols": object mapping symbol names to file paths (array)
1486
- - "responsibilities": object mapping paths to brief descriptions
1659
+ ${entry}`;
1660
+ import_node_fs2.writeFileSync(this.progressPath, updated);
1661
+ }
1662
+ getFullContext() {
1663
+ const context = this.readContext();
1664
+ const progress = this.readProgress();
1665
+ const parts = [];
1666
+ if (context.trim()) {
1667
+ parts.push(context.trim());
1668
+ }
1669
+ if (progress.trim()) {
1670
+ parts.push(progress.trim());
1671
+ }
1672
+ return parts.join(`
1487
1673
 
1488
- File tree:
1489
- ${tree}
1674
+ ---
1490
1675
 
1491
- Return ONLY valid JSON, no markdown formatting.`;
1492
- const response = await this.deps.aiRunner.run(prompt);
1493
- const jsonMatch = response.match(/\{[\s\S]*\}/);
1494
- if (jsonMatch) {
1495
- return JSON.parse(jsonMatch[0]);
1496
- }
1497
- return { symbols: {}, responsibilities: {}, lastIndexed: "" };
1498
- }, force);
1499
- if (index === null) {
1500
- this.deps.log("No changes detected, skipping reindex", "info");
1501
- return;
1502
- }
1503
- this.indexer.saveIndex(index);
1504
- this.deps.log("Codebase reindexed successfully", "success");
1505
- } catch (error) {
1506
- this.deps.log(`Failed to reindex codebase: ${error}`, "error");
1676
+ `);
1677
+ }
1678
+ initialize(info) {
1679
+ this.ensureDir(this.contextPath);
1680
+ this.ensureDir(this.progressPath);
1681
+ const techStackList = info.techStack.map((t) => `- ${t}`).join(`
1682
+ `);
1683
+ const contextContent = `# Project: ${info.name}
1684
+
1685
+ ## Mission
1686
+ ${info.mission}
1687
+
1688
+ ## Tech Stack
1689
+ ${techStackList}
1690
+
1691
+ ## Architecture
1692
+ <!-- Describe your high-level architecture here -->
1693
+
1694
+ ## Key Decisions
1695
+ <!-- Document important technical decisions and their rationale -->
1696
+
1697
+ ## Feature Areas
1698
+ <!-- List your main feature areas and their status -->
1699
+ `;
1700
+ const progressContent = `# Project Progress
1701
+
1702
+ No sprints started yet.
1703
+ `;
1704
+ import_node_fs2.writeFileSync(this.contextPath, contextContent);
1705
+ import_node_fs2.writeFileSync(this.progressPath, progressContent);
1706
+ }
1707
+ get exists() {
1708
+ return import_node_fs2.existsSync(this.contextPath) || import_node_fs2.existsSync(this.progressPath);
1709
+ }
1710
+ ensureDir(filePath) {
1711
+ const dir = import_node_path4.dirname(filePath);
1712
+ if (!import_node_fs2.existsSync(dir)) {
1713
+ import_node_fs2.mkdirSync(dir, { recursive: true });
1507
1714
  }
1508
1715
  }
1509
1716
  }
1510
1717
 
1511
- // src/agent/document-fetcher.ts
1718
+ // src/worktree/worktree-manager.ts
1719
+ var import_node_child_process5 = require("node:child_process");
1512
1720
  var import_node_fs3 = require("node:fs");
1513
1721
  var import_node_path5 = require("node:path");
1514
- class DocumentFetcher {
1515
- deps;
1516
- constructor(deps) {
1517
- this.deps = deps;
1722
+
1723
+ // src/worktree/worktree-config.ts
1724
+ var WORKTREE_ROOT_DIR = ".locus-worktrees";
1725
+ var WORKTREE_BRANCH_PREFIX = "agent";
1726
+ var DEFAULT_WORKTREE_CONFIG = {
1727
+ rootDir: WORKTREE_ROOT_DIR,
1728
+ branchPrefix: WORKTREE_BRANCH_PREFIX,
1729
+ cleanupPolicy: "retain-on-failure"
1730
+ };
1731
+
1732
+ // src/worktree/worktree-manager.ts
1733
+ class WorktreeManager {
1734
+ config;
1735
+ projectPath;
1736
+ log;
1737
+ constructor(projectPath, config, log) {
1738
+ this.projectPath = import_node_path5.resolve(projectPath);
1739
+ this.config = { ...DEFAULT_WORKTREE_CONFIG, ...config };
1740
+ this.log = log ?? ((_msg) => {
1741
+ return;
1742
+ });
1518
1743
  }
1519
- async fetch() {
1520
- const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
1521
- if (!import_node_fs3.existsSync(documentsDir)) {
1522
- import_node_fs3.mkdirSync(documentsDir, { recursive: true });
1744
+ get rootPath() {
1745
+ return import_node_path5.join(this.projectPath, this.config.rootDir);
1746
+ }
1747
+ buildBranchName(taskId, taskSlug) {
1748
+ const sanitized = taskSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
1749
+ return `${this.config.branchPrefix}/${taskId}-${sanitized}`;
1750
+ }
1751
+ create(options) {
1752
+ const branch = this.buildBranchName(options.taskId, options.taskSlug);
1753
+ const worktreeDir = `${options.agentId}-${options.taskId}`;
1754
+ const worktreePath = import_node_path5.join(this.rootPath, worktreeDir);
1755
+ this.ensureDirectory(this.rootPath, "Worktree root");
1756
+ const baseBranch = options.baseBranch ?? this.config.baseBranch ?? this.getCurrentBranch();
1757
+ this.log(`Creating worktree: ${worktreeDir} (branch: ${branch}, base: ${baseBranch})`, "info");
1758
+ if (import_node_fs3.existsSync(worktreePath)) {
1759
+ this.log(`Removing stale worktree directory: ${worktreePath}`, "warn");
1760
+ try {
1761
+ this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
1762
+ } catch {
1763
+ import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
1764
+ this.git("worktree prune", this.projectPath);
1765
+ }
1523
1766
  }
1524
- try {
1525
- const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
1526
- const groupMap = new Map(groups.map((g) => [g.id, g.name]));
1527
- const docs2 = await this.deps.client.docs.list(this.deps.workspaceId);
1528
- const artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
1529
- let fetchedCount = 0;
1530
- for (const doc of docs2) {
1531
- if (doc.groupId === artifactsGroupId) {
1532
- continue;
1533
- }
1534
- const groupName = groupMap.get(doc.groupId || "") || "General";
1535
- const groupDir = import_node_path5.join(documentsDir, groupName);
1536
- if (!import_node_fs3.existsSync(groupDir)) {
1537
- import_node_fs3.mkdirSync(groupDir, { recursive: true });
1538
- }
1539
- const fileName = `${doc.title}.md`;
1540
- const filePath = import_node_path5.join(groupDir, fileName);
1541
- if (!import_node_fs3.existsSync(filePath) || import_node_fs3.readFileSync(filePath, "utf-8") !== doc.content) {
1542
- import_node_fs3.writeFileSync(filePath, doc.content || "");
1543
- fetchedCount++;
1767
+ if (this.branchExists(branch)) {
1768
+ this.log(`Deleting existing branch: ${branch}`, "warn");
1769
+ const branchWorktrees = this.list().filter((wt) => wt.branch === branch);
1770
+ for (const wt of branchWorktrees) {
1771
+ const worktreePath2 = import_node_path5.resolve(wt.path);
1772
+ if (wt.isMain || !this.isManagedWorktreePath(worktreePath2)) {
1773
+ throw new Error(`Branch "${branch}" is checked out at "${worktreePath2}". Remove or detach that worktree before retrying.`);
1544
1774
  }
1775
+ this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
1776
+ this.remove(worktreePath2, false);
1545
1777
  }
1546
- if (fetchedCount > 0) {
1547
- this.deps.log(`Fetched ${fetchedCount} document(s) from server`, "info");
1778
+ try {
1779
+ this.git(`branch -D "${branch}"`, this.projectPath);
1780
+ } catch {
1781
+ this.git("worktree prune", this.projectPath);
1782
+ this.git(`branch -D "${branch}"`, this.projectPath);
1548
1783
  }
1784
+ }
1785
+ const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
1786
+ try {
1787
+ addWorktree();
1549
1788
  } catch (error) {
1550
- this.deps.log(`Failed to fetch documents: ${error}`, "error");
1789
+ if (!this.isMissingDirectoryError(error)) {
1790
+ throw error;
1791
+ }
1792
+ this.log(`Worktree creation failed due to missing directories. Retrying after cleanup: ${worktreePath}`, "warn");
1793
+ this.cleanupFailedWorktree(worktreePath, branch);
1794
+ this.ensureDirectory(this.rootPath, "Worktree root");
1795
+ addWorktree();
1796
+ }
1797
+ this.log(`Worktree created at ${worktreePath}`, "success");
1798
+ return { worktreePath, branch, baseBranch };
1799
+ }
1800
+ list() {
1801
+ const output = this.git("worktree list --porcelain", this.projectPath);
1802
+ const worktrees = [];
1803
+ const blocks = output.trim().split(`
1804
+
1805
+ `);
1806
+ for (const block of blocks) {
1807
+ if (!block.trim())
1808
+ continue;
1809
+ const lines = block.trim().split(`
1810
+ `);
1811
+ let path = "";
1812
+ let head = "";
1813
+ let branch = "";
1814
+ let isMain = false;
1815
+ let isPrunable = false;
1816
+ for (const line of lines) {
1817
+ if (line.startsWith("worktree ")) {
1818
+ path = line.slice("worktree ".length);
1819
+ } else if (line.startsWith("HEAD ")) {
1820
+ head = line.slice("HEAD ".length);
1821
+ } else if (line.startsWith("branch ")) {
1822
+ branch = line.slice("branch ".length).replace("refs/heads/", "");
1823
+ } else if (line === "bare" || path === this.projectPath) {
1824
+ isMain = true;
1825
+ } else if (line === "prunable") {
1826
+ isPrunable = true;
1827
+ } else if (line === "detached") {
1828
+ branch = "(detached)";
1829
+ }
1830
+ }
1831
+ if (import_node_path5.resolve(path) === this.projectPath) {
1832
+ isMain = true;
1833
+ }
1834
+ if (path) {
1835
+ worktrees.push({ path, branch, head, isMain, isPrunable });
1836
+ }
1551
1837
  }
1838
+ return worktrees;
1552
1839
  }
1553
- }
1554
-
1555
- // src/agent/review-service.ts
1556
- var import_node_child_process3 = require("node:child_process");
1557
-
1558
- class ReviewService {
1559
- deps;
1560
- constructor(deps) {
1561
- this.deps = deps;
1840
+ listAgentWorktrees() {
1841
+ return this.list().filter((wt) => !wt.isMain);
1562
1842
  }
1563
- async reviewStagedChanges(sprint) {
1564
- const { projectPath, log } = this.deps;
1843
+ remove(worktreePath, deleteBranch = true) {
1844
+ const absolutePath = import_node_path5.resolve(worktreePath);
1845
+ const worktrees = this.list();
1846
+ const worktree = worktrees.find((wt) => import_node_path5.resolve(wt.path) === absolutePath);
1847
+ const branchToDelete = worktree?.branch;
1848
+ this.log(`Removing worktree: ${absolutePath}`, "info");
1565
1849
  try {
1566
- import_node_child_process3.execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
1567
- log("Staged all changes for review.", "info");
1568
- } catch (err) {
1569
- log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
1850
+ this.git(`worktree remove "${absolutePath}" --force`, this.projectPath);
1851
+ } catch {
1852
+ if (import_node_fs3.existsSync(absolutePath)) {
1853
+ import_node_fs3.rmSync(absolutePath, { recursive: true, force: true });
1854
+ }
1855
+ this.git("worktree prune", this.projectPath);
1856
+ }
1857
+ if (deleteBranch && branchToDelete && !branchToDelete.startsWith("(")) {
1858
+ try {
1859
+ this.git(`branch -D "${branchToDelete}"`, this.projectPath);
1860
+ this.log(`Deleted branch: ${branchToDelete}`, "success");
1861
+ } catch {
1862
+ this.log(`Could not delete branch: ${branchToDelete} (may already be deleted)`, "warn");
1863
+ }
1864
+ }
1865
+ this.log("Worktree removed", "success");
1866
+ }
1867
+ prune() {
1868
+ const before = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
1869
+ this.git("worktree prune", this.projectPath);
1870
+ const after = this.listAgentWorktrees().filter((wt) => wt.isPrunable).length;
1871
+ const pruned = before - after;
1872
+ if (pruned > 0) {
1873
+ this.log(`Pruned ${pruned} stale worktree(s)`, "success");
1874
+ }
1875
+ return pruned;
1876
+ }
1877
+ removeAll() {
1878
+ const agentWorktrees = this.listAgentWorktrees();
1879
+ let removed = 0;
1880
+ for (const wt of agentWorktrees) {
1881
+ try {
1882
+ this.remove(wt.path, true);
1883
+ removed++;
1884
+ } catch {
1885
+ this.log(`Failed to remove worktree: ${wt.path}`, "warn");
1886
+ }
1887
+ }
1888
+ if (import_node_fs3.existsSync(this.rootPath)) {
1889
+ try {
1890
+ import_node_fs3.rmSync(this.rootPath, { recursive: true, force: true });
1891
+ } catch {}
1892
+ }
1893
+ return removed;
1894
+ }
1895
+ hasChanges(worktreePath) {
1896
+ const status = this.git("status --porcelain", worktreePath).trim();
1897
+ return status.length > 0;
1898
+ }
1899
+ commitChanges(worktreePath, message) {
1900
+ if (!this.hasChanges(worktreePath)) {
1901
+ this.log("No changes to commit", "info");
1570
1902
  return null;
1571
1903
  }
1572
- let diff;
1904
+ this.git("add -A", worktreePath);
1905
+ this.gitExec(["commit", "-m", message], worktreePath);
1906
+ const hash = this.git("rev-parse HEAD", worktreePath).trim();
1907
+ this.log(`Committed: ${hash.slice(0, 8)}`, "success");
1908
+ return hash;
1909
+ }
1910
+ pushBranch(worktreePath, remote = "origin") {
1911
+ const branch = this.getBranch(worktreePath);
1912
+ this.log(`Pushing branch ${branch} to ${remote}`, "info");
1573
1913
  try {
1574
- diff = import_node_child_process3.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
1575
- cwd: projectPath,
1576
- maxBuffer: 10 * 1024 * 1024
1577
- }).toString();
1578
- } catch (err) {
1579
- log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
1580
- return null;
1914
+ this.gitExec(["push", "-u", remote, branch], worktreePath);
1915
+ this.log(`Pushed ${branch} to ${remote}`, "success");
1916
+ return branch;
1917
+ } catch (error) {
1918
+ if (!this.isNonFastForwardPushError(error)) {
1919
+ throw error;
1920
+ }
1921
+ this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
1922
+ try {
1923
+ this.gitExec(["fetch", remote, branch], worktreePath);
1924
+ } catch {}
1925
+ this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
1926
+ this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
1581
1927
  }
1582
- if (!diff.trim()) {
1583
- return null;
1928
+ return branch;
1929
+ }
1930
+ getBranch(worktreePath) {
1931
+ return this.git("rev-parse --abbrev-ref HEAD", worktreePath).trim();
1932
+ }
1933
+ hasWorktreeForTask(taskId) {
1934
+ return this.listAgentWorktrees().some((wt) => wt.branch.includes(taskId) || wt.path.includes(taskId));
1935
+ }
1936
+ branchExists(branchName) {
1937
+ try {
1938
+ this.git(`rev-parse --verify "refs/heads/${branchName}"`, this.projectPath);
1939
+ return true;
1940
+ } catch {
1941
+ return false;
1584
1942
  }
1585
- const sprintInfo = sprint ? `Sprint: ${sprint.name} (${sprint.id})` : "No active sprint";
1586
- const reviewPrompt = `# Code Review Request
1587
-
1588
- ## Context
1589
- ${sprintInfo}
1590
- Date: ${new Date().toISOString()}
1591
-
1592
- ## Staged Changes (git diff)
1593
- \`\`\`diff
1594
- ${diff}
1595
- \`\`\`
1596
-
1597
- ## Instructions
1598
- You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
1599
-
1600
- 1. **Summary** — Brief overview of what changed and why.
1601
- 2. **Files Changed** — List each file with a short description of changes.
1602
- 3. **Code Quality** Note any code quality concerns (naming, structure, complexity).
1603
- 4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
1604
- 5. **Recommendations** — Actionable suggestions for improvement.
1605
- 6. **Overall Assessment** A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
1606
-
1607
- Keep the review concise but thorough. Focus on substance over style.
1608
- Do NOT output <promise>COMPLETE</promise> — just output the review report as markdown.`;
1609
- log("Running AI review on staged changes...", "info");
1610
- const report = await this.deps.aiRunner.run(reviewPrompt);
1611
- return report;
1943
+ }
1944
+ getCurrentBranch() {
1945
+ return this.git("rev-parse --abbrev-ref HEAD", this.projectPath).trim();
1946
+ }
1947
+ isManagedWorktreePath(worktreePath) {
1948
+ const rootPath = import_node_path5.resolve(this.rootPath);
1949
+ const candidate = import_node_path5.resolve(worktreePath);
1950
+ const rootWithSep = rootPath.endsWith(import_node_path5.sep) ? rootPath : `${rootPath}${import_node_path5.sep}`;
1951
+ return candidate.startsWith(rootWithSep);
1952
+ }
1953
+ ensureDirectory(dirPath, label) {
1954
+ if (import_node_fs3.existsSync(dirPath)) {
1955
+ if (!import_node_fs3.statSync(dirPath).isDirectory()) {
1956
+ throw new Error(`${label} exists but is not a directory: ${dirPath}`);
1957
+ }
1958
+ return;
1959
+ }
1960
+ import_node_fs3.mkdirSync(dirPath, { recursive: true });
1961
+ }
1962
+ isMissingDirectoryError(error) {
1963
+ const message = error instanceof Error ? error.message : String(error);
1964
+ return message.includes("cannot create directory") || message.includes("No such file or directory");
1965
+ }
1966
+ cleanupFailedWorktree(worktreePath, branch) {
1967
+ try {
1968
+ this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
1969
+ } catch {}
1970
+ if (import_node_fs3.existsSync(worktreePath)) {
1971
+ import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
1972
+ }
1973
+ try {
1974
+ this.git("worktree prune", this.projectPath);
1975
+ } catch {}
1976
+ if (this.branchExists(branch)) {
1977
+ try {
1978
+ this.git(`branch -D "${branch}"`, this.projectPath);
1979
+ } catch {}
1980
+ }
1981
+ }
1982
+ isNonFastForwardPushError(error) {
1983
+ const message = error instanceof Error ? error.message : String(error);
1984
+ return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
1985
+ }
1986
+ git(args, cwd) {
1987
+ return import_node_child_process5.execSync(`git ${args}`, {
1988
+ cwd,
1989
+ encoding: "utf-8",
1990
+ stdio: ["pipe", "pipe", "pipe"]
1991
+ });
1992
+ }
1993
+ gitExec(args, cwd) {
1994
+ return import_node_child_process5.execFileSync("git", args, {
1995
+ cwd,
1996
+ encoding: "utf-8",
1997
+ stdio: ["pipe", "pipe", "pipe"]
1998
+ });
1612
1999
  }
1613
2000
  }
1614
2001
 
1615
2002
  // src/core/prompt-builder.ts
1616
2003
  var import_node_fs4 = require("node:fs");
1617
- var import_node_os2 = require("node:os");
1618
2004
  var import_node_path6 = require("node:path");
1619
2005
  var import_shared2 = require("@locusai/shared");
1620
2006
  class PromptBuilder {
@@ -1683,11 +2069,12 @@ ${fallback}
1683
2069
  if (serverContext) {
1684
2070
  prompt += `## Project Context (Server)
1685
2071
  `;
1686
- if (serverContext.project) {
1687
- prompt += `- Project: ${serverContext.project.name || "Unknown"}
2072
+ const project = serverContext.project;
2073
+ if (project) {
2074
+ prompt += `- Project: ${project.name || "Unknown"}
1688
2075
  `;
1689
- if (!hasLocalContext && serverContext.project.techStack?.length) {
1690
- prompt += `- Tech Stack: ${serverContext.project.techStack.join(", ")}
2076
+ if (!hasLocalContext && project.techStack?.length) {
2077
+ prompt += `- Tech Stack: ${project.techStack.join(", ")}
1691
2078
  `;
1692
2079
  }
1693
2080
  }
@@ -1700,12 +2087,11 @@ ${serverContext.context}
1700
2087
  `;
1701
2088
  }
1702
2089
  prompt += this.getProjectStructure();
1703
- prompt += this.getSkillsInfo();
1704
2090
  prompt += `## Project Knowledge Base
1705
2091
  `;
1706
2092
  prompt += `You have access to the following documentation directories for context:
1707
2093
  `;
1708
- prompt += `- Artifacts: \`.locus/artifacts\`)
2094
+ prompt += `- Artifacts: \`.locus/artifacts\`
1709
2095
  `;
1710
2096
  prompt += `- Documents: \`.locus/documents\`
1711
2097
  `;
@@ -1763,11 +2149,9 @@ ${comment.text}
1763
2149
  }
1764
2150
  }
1765
2151
  prompt += `## Instructions
1766
- 1. Complete this task.
2152
+ 1. Complete this task.
1767
2153
  2. **Artifact Management**: If you create any high-level documentation (PRDs, technical drafts, architecture docs), you MUST save them in \`.locus/artifacts/\`. Do NOT create them in the root directory.
1768
- 3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
1769
- 4. When finished successfully, output: <promise>COMPLETE</promise>
1770
- `;
2154
+ 3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
1771
2155
  return prompt;
1772
2156
  }
1773
2157
  async buildGenericPrompt(query) {
@@ -1814,7 +2198,6 @@ ${fallback}
1814
2198
  }
1815
2199
  }
1816
2200
  prompt += this.getProjectStructure();
1817
- prompt += this.getSkillsInfo();
1818
2201
  prompt += `## Project Knowledge Base
1819
2202
  `;
1820
2203
  prompt += `You have access to the following documentation directories for context:
@@ -1835,9 +2218,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1835
2218
  }
1836
2219
  prompt += `## Instructions
1837
2220
  1. Execute the prompt based on the provided project context.
1838
- 2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
1839
- 3. When finished successfully, output: <promise>COMPLETE</promise>
1840
- `;
2221
+ 2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
1841
2222
  return prompt;
1842
2223
  }
1843
2224
  getProjectConfig() {
@@ -1893,55 +2274,6 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1893
2274
  return "";
1894
2275
  }
1895
2276
  }
1896
- getSkillsInfo() {
1897
- const projectSkillsDirs = [
1898
- LOCUS_CONFIG.agentSkillsDir,
1899
- ".cursor/skills",
1900
- ".claude/skills",
1901
- ".codex/skills",
1902
- ".gemini/skills"
1903
- ];
1904
- const globalHome = import_node_os2.homedir();
1905
- const globalSkillsDirs = [
1906
- import_node_path6.join(globalHome, ".cursor/skills"),
1907
- import_node_path6.join(globalHome, ".claude/skills"),
1908
- import_node_path6.join(globalHome, ".codex/skills"),
1909
- import_node_path6.join(globalHome, ".gemini/skills")
1910
- ];
1911
- const allSkillNames = new Set;
1912
- for (const relativePath of projectSkillsDirs) {
1913
- const fullPath = import_node_path6.join(this.projectPath, relativePath);
1914
- this.scanSkillsInDirectory(fullPath, allSkillNames);
1915
- }
1916
- for (const fullPath of globalSkillsDirs) {
1917
- this.scanSkillsInDirectory(fullPath, allSkillNames);
1918
- }
1919
- const uniqueSkills = Array.from(allSkillNames).sort();
1920
- if (uniqueSkills.length === 0)
1921
- return "";
1922
- return `## Available Agent Skills
1923
- ` + `The project has the following specialized skills available (from project or global locations):
1924
- ` + uniqueSkills.map((s) => `- ${s}`).join(`
1925
- `) + `
1926
-
1927
- `;
1928
- }
1929
- scanSkillsInDirectory(dirPath, skillSet) {
1930
- if (!import_node_fs4.existsSync(dirPath))
1931
- return;
1932
- try {
1933
- const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
1934
- try {
1935
- return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
1936
- } catch {
1937
- return false;
1938
- }
1939
- });
1940
- for (const entry of entries) {
1941
- skillSet.add(entry);
1942
- }
1943
- } catch {}
1944
- }
1945
2277
  roleToText(role) {
1946
2278
  if (!role) {
1947
2279
  return null;
@@ -1971,21 +2303,15 @@ class TaskExecutor {
1971
2303
  this.deps = deps;
1972
2304
  this.promptBuilder = new PromptBuilder(deps.projectPath);
1973
2305
  }
1974
- async execute(task, context) {
2306
+ async execute(task) {
1975
2307
  this.deps.log(`Executing: ${task.title}`, "info");
1976
- const basePrompt = await this.promptBuilder.build(task, {
1977
- taskContext: context
1978
- });
2308
+ const basePrompt = await this.promptBuilder.build(task);
1979
2309
  try {
1980
2310
  this.deps.log("Starting Execution...", "info");
1981
- const executionPrompt = `${basePrompt}
1982
-
1983
- When finished, output: <promise>COMPLETE</promise>`;
1984
- const output = await this.deps.aiRunner.run(executionPrompt);
1985
- const success = output.includes("<promise>COMPLETE</promise>");
2311
+ await this.deps.aiRunner.run(basePrompt);
1986
2312
  return {
1987
- success,
1988
- summary: success ? "Task completed by the agent" : "The agent did not signal completion"
2313
+ success: true,
2314
+ summary: "Task completed by the agent"
1989
2315
  };
1990
2316
  } catch (error) {
1991
2317
  return { success: false, summary: `Error: ${error}` };
@@ -2009,12 +2335,17 @@ class AgentWorker {
2009
2335
  config;
2010
2336
  client;
2011
2337
  aiRunner;
2012
- indexerService;
2013
- documentFetcher;
2014
2338
  taskExecutor;
2015
- reviewService;
2339
+ knowledgeBase;
2340
+ worktreeManager = null;
2341
+ prService = null;
2016
2342
  maxTasks = 50;
2017
2343
  tasksCompleted = 0;
2344
+ heartbeatInterval = null;
2345
+ currentTaskId = null;
2346
+ currentWorktreePath = null;
2347
+ postCleanupDelayMs = 5000;
2348
+ ghUsername = null;
2018
2349
  constructor(config) {
2019
2350
  this.config = config;
2020
2351
  const projectPath = config.projectPath || process.cwd();
@@ -2029,35 +2360,47 @@ class AgentWorker {
2029
2360
  }
2030
2361
  });
2031
2362
  const log = this.log.bind(this);
2363
+ if (config.useWorktrees && !isGitAvailable()) {
2364
+ this.log("git is not installed — worktree isolation will not work", "error");
2365
+ config.useWorktrees = false;
2366
+ }
2367
+ if (config.autoPush && !isGhAvailable(projectPath)) {
2368
+ 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");
2369
+ }
2370
+ if (config.autoPush) {
2371
+ this.ghUsername = getGhUsername();
2372
+ if (this.ghUsername) {
2373
+ this.log(`GitHub user: ${this.ghUsername}`, "info");
2374
+ }
2375
+ }
2032
2376
  const provider = config.provider ?? PROVIDER.CLAUDE;
2033
2377
  this.aiRunner = createAiRunner(provider, {
2034
2378
  projectPath,
2035
2379
  model: config.model,
2036
2380
  log
2037
2381
  });
2038
- this.indexerService = new CodebaseIndexerService({
2039
- aiRunner: this.aiRunner,
2040
- projectPath,
2041
- log
2042
- });
2043
- this.documentFetcher = new DocumentFetcher({
2044
- client: this.client,
2045
- workspaceId: config.workspaceId,
2046
- projectPath,
2047
- log
2048
- });
2049
2382
  this.taskExecutor = new TaskExecutor({
2050
2383
  aiRunner: this.aiRunner,
2051
2384
  projectPath,
2052
2385
  log
2053
2386
  });
2054
- this.reviewService = new ReviewService({
2055
- aiRunner: this.aiRunner,
2056
- projectPath,
2057
- log
2058
- });
2387
+ this.knowledgeBase = new KnowledgeBase(projectPath);
2388
+ if (config.useWorktrees) {
2389
+ this.worktreeManager = new WorktreeManager(projectPath, {
2390
+ cleanupPolicy: "auto"
2391
+ });
2392
+ }
2393
+ if (config.autoPush) {
2394
+ this.prService = new PrService(projectPath, log);
2395
+ }
2059
2396
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
2060
2397
  this.log(`Using ${providerLabel} CLI for all phases`, "info");
2398
+ if (config.useWorktrees) {
2399
+ this.log("Per-task worktree isolation enabled", "info");
2400
+ if (config.autoPush) {
2401
+ this.log("Auto-push enabled: branches will be pushed to remote", "info");
2402
+ }
2403
+ }
2061
2404
  }
2062
2405
  log(message, level = "info") {
2063
2406
  const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
@@ -2081,49 +2424,258 @@ class AgentWorker {
2081
2424
  }
2082
2425
  }
2083
2426
  async getNextTask() {
2427
+ const maxRetries = 10;
2428
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
2429
+ try {
2430
+ const task = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
2431
+ return task;
2432
+ } catch (error) {
2433
+ const isAxiosError = error != null && typeof error === "object" && "response" in error && typeof error.response?.status === "number";
2434
+ const status = isAxiosError ? error.response.status : 0;
2435
+ if (status === 404) {
2436
+ this.log("No tasks available in the backlog.", "info");
2437
+ return null;
2438
+ }
2439
+ const msg = error instanceof Error ? error.message : String(error);
2440
+ if (attempt < maxRetries) {
2441
+ this.log(`Nothing dispatched (attempt ${attempt}/${maxRetries}): ${msg}. Retrying in 30s...`, "warn");
2442
+ await new Promise((r) => setTimeout(r, 30000));
2443
+ } else {
2444
+ this.log(`Nothing dispatched after ${maxRetries} attempts: ${msg}`, "warn");
2445
+ return null;
2446
+ }
2447
+ }
2448
+ }
2449
+ return null;
2450
+ }
2451
+ createTaskWorktree(task) {
2452
+ if (!this.worktreeManager) {
2453
+ return {
2454
+ worktreePath: null,
2455
+ baseBranch: null,
2456
+ executor: this.taskExecutor
2457
+ };
2458
+ }
2459
+ const slug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
2460
+ const result = this.worktreeManager.create({
2461
+ taskId: task.id,
2462
+ taskSlug: slug,
2463
+ agentId: this.config.agentId
2464
+ });
2465
+ this.log(`Worktree created: ${result.worktreePath} (${result.branch})`, "info");
2466
+ const log = this.log.bind(this);
2467
+ const provider = this.config.provider ?? PROVIDER.CLAUDE;
2468
+ const taskAiRunner = createAiRunner(provider, {
2469
+ projectPath: result.worktreePath,
2470
+ model: this.config.model,
2471
+ log
2472
+ });
2473
+ const taskExecutor = new TaskExecutor({
2474
+ aiRunner: taskAiRunner,
2475
+ projectPath: result.worktreePath,
2476
+ log
2477
+ });
2478
+ return {
2479
+ worktreePath: result.worktreePath,
2480
+ baseBranch: result.baseBranch,
2481
+ executor: taskExecutor
2482
+ };
2483
+ }
2484
+ commitAndPushWorktree(worktreePath, task) {
2485
+ if (!this.worktreeManager) {
2486
+ return { branch: null, pushed: false, pushFailed: false };
2487
+ }
2084
2488
  try {
2085
- const task = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
2086
- return task;
2087
- } catch (error) {
2088
- this.log(`No task dispatched: ${error instanceof Error ? error.message : String(error)}`, "info");
2089
- return null;
2489
+ const trailers = [
2490
+ `Task-ID: ${task.id}`,
2491
+ `Agent: ${this.config.agentId}`,
2492
+ "Co-authored-by: LocusAI <noreply@locusai.dev>"
2493
+ ];
2494
+ if (this.ghUsername) {
2495
+ trailers.push(`Co-authored-by: ${this.ghUsername} <${this.ghUsername}@users.noreply.github.com>`);
2496
+ }
2497
+ const commitMessage = `feat(agent): ${task.title}
2498
+
2499
+ ${trailers.join(`
2500
+ `)}`;
2501
+ const hash = this.worktreeManager.commitChanges(worktreePath, commitMessage);
2502
+ if (!hash) {
2503
+ this.log("No changes to commit for this task", "info");
2504
+ return {
2505
+ branch: null,
2506
+ pushed: false,
2507
+ pushFailed: false,
2508
+ noChanges: true,
2509
+ skipReason: "No changes were committed, so no branch was pushed."
2510
+ };
2511
+ }
2512
+ const localBranch = this.worktreeManager.getBranch(worktreePath);
2513
+ if (this.config.autoPush) {
2514
+ try {
2515
+ return {
2516
+ branch: this.worktreeManager.pushBranch(worktreePath),
2517
+ pushed: true,
2518
+ pushFailed: false
2519
+ };
2520
+ } catch (err) {
2521
+ const errorMessage = err instanceof Error ? err.message : String(err);
2522
+ this.log(`Git push failed: ${errorMessage}`, "error");
2523
+ return {
2524
+ branch: localBranch,
2525
+ pushed: false,
2526
+ pushFailed: true,
2527
+ pushError: errorMessage
2528
+ };
2529
+ }
2530
+ }
2531
+ this.log("Auto-push disabled; skipping branch push", "info");
2532
+ return {
2533
+ branch: localBranch,
2534
+ pushed: false,
2535
+ pushFailed: false,
2536
+ skipReason: "Auto-push is disabled, so PR creation was skipped."
2537
+ };
2538
+ } catch (err) {
2539
+ const errorMessage = err instanceof Error ? err.message : String(err);
2540
+ this.log(`Git commit failed: ${errorMessage}`, "error");
2541
+ return { branch: null, pushed: false, pushFailed: false };
2090
2542
  }
2091
2543
  }
2092
- async executeTask(task) {
2093
- const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
2094
- let context = "";
2544
+ createPullRequest(task, branch, summary, baseBranch) {
2545
+ if (!this.prService) {
2546
+ const errorMessage = "PR service is not initialized. Enable auto-push to allow PR creation.";
2547
+ this.log(`PR creation skipped: ${errorMessage}`, "warn");
2548
+ return { url: null, error: errorMessage };
2549
+ }
2550
+ this.log(`Attempting PR creation from branch: ${branch}`, "info");
2095
2551
  try {
2096
- context = await this.client.tasks.getContext(task.id, this.config.workspaceId);
2552
+ const result = this.prService.createPr({
2553
+ task,
2554
+ branch,
2555
+ baseBranch,
2556
+ agentId: this.config.agentId,
2557
+ summary
2558
+ });
2559
+ return { url: result.url };
2097
2560
  } catch (err) {
2098
- this.log(`Failed to fetch task context: ${err}`, "warn");
2561
+ const errorMessage = err instanceof Error ? err.message : String(err);
2562
+ this.log(`PR creation failed: ${errorMessage}`, "error");
2563
+ return { url: null, error: errorMessage };
2099
2564
  }
2100
- const result = await this.taskExecutor.execute(fullTask, context);
2101
- await this.indexerService.reindex();
2102
- return result;
2103
2565
  }
2104
- async runStagedChangesReview(sprint) {
2566
+ cleanupTaskWorktree(worktreePath, keepBranch) {
2567
+ if (!this.worktreeManager || !worktreePath)
2568
+ return;
2105
2569
  try {
2106
- const report = await this.reviewService.reviewStagedChanges(sprint);
2107
- if (report) {
2108
- const reviewsDir = import_node_path7.join(this.config.projectPath, LOCUS_CONFIG.dir, "reviews");
2109
- if (!import_node_fs5.existsSync(reviewsDir)) {
2110
- import_node_fs5.mkdirSync(reviewsDir, { recursive: true });
2570
+ this.worktreeManager.remove(worktreePath, !keepBranch);
2571
+ this.log(keepBranch ? "Worktree cleaned up (branch preserved)" : "Worktree cleaned up", "info");
2572
+ } catch {
2573
+ this.log(`Could not clean up worktree: ${worktreePath}`, "warn");
2574
+ }
2575
+ this.currentWorktreePath = null;
2576
+ }
2577
+ async executeTask(task) {
2578
+ const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
2579
+ const { worktreePath, baseBranch, executor } = this.createTaskWorktree(fullTask);
2580
+ this.currentWorktreePath = worktreePath;
2581
+ let branchPushed = false;
2582
+ let keepBranch = false;
2583
+ let preserveWorktree = false;
2584
+ try {
2585
+ const result = await executor.execute(fullTask);
2586
+ let taskBranch = null;
2587
+ let prUrl = null;
2588
+ let prError = null;
2589
+ let noChanges = false;
2590
+ if (result.success && worktreePath) {
2591
+ const commitResult = this.commitAndPushWorktree(worktreePath, fullTask);
2592
+ taskBranch = commitResult.branch;
2593
+ branchPushed = commitResult.pushed;
2594
+ keepBranch = taskBranch !== null;
2595
+ noChanges = Boolean(commitResult.noChanges);
2596
+ if (commitResult.pushFailed) {
2597
+ preserveWorktree = true;
2598
+ prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
2599
+ this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
2600
+ }
2601
+ if (branchPushed && taskBranch) {
2602
+ const prResult = this.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
2603
+ prUrl = prResult.url;
2604
+ prError = prResult.error ?? null;
2605
+ if (!prUrl) {
2606
+ preserveWorktree = true;
2607
+ this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
2608
+ }
2609
+ } else if (commitResult.skipReason) {
2610
+ this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
2111
2611
  }
2112
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2113
- const sprintSlug = sprint?.name ? sprint.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
2114
- const fileName = `review-${sprintSlug}-${timestamp}.md`;
2115
- const filePath = import_node_path7.join(reviewsDir, fileName);
2116
- import_node_fs5.writeFileSync(filePath, report);
2117
- this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
2612
+ } else if (result.success && !worktreePath) {
2613
+ this.log("Skipping commit/push/PR flow because no task worktree is active.", "warn");
2614
+ }
2615
+ return {
2616
+ ...result,
2617
+ branch: taskBranch ?? undefined,
2618
+ prUrl: prUrl ?? undefined,
2619
+ prError: prError ?? undefined,
2620
+ noChanges: noChanges || undefined
2621
+ };
2622
+ } finally {
2623
+ if (preserveWorktree || keepBranch) {
2624
+ this.currentWorktreePath = null;
2118
2625
  } else {
2119
- this.log("No staged changes to review.", "info");
2626
+ this.cleanupTaskWorktree(worktreePath, keepBranch);
2627
+ }
2628
+ }
2629
+ }
2630
+ updateProgress(task, success) {
2631
+ try {
2632
+ if (success) {
2633
+ this.knowledgeBase.updateProgress({
2634
+ type: "task_completed",
2635
+ title: task.title,
2636
+ details: `Agent: ${this.config.agentId.slice(-8)}`
2637
+ });
2638
+ this.log(`Updated progress.md: ${task.title}`, "info");
2120
2639
  }
2121
2640
  } catch (err) {
2122
- this.log(`Review failed: ${err instanceof Error ? err.message : String(err)}`, "error");
2641
+ this.log(`Failed to update progress: ${err instanceof Error ? err.message : String(err)}`, "warn");
2642
+ }
2643
+ }
2644
+ startHeartbeat() {
2645
+ this.sendHeartbeat();
2646
+ this.heartbeatInterval = setInterval(() => {
2647
+ this.sendHeartbeat();
2648
+ }, 60000);
2649
+ }
2650
+ stopHeartbeat() {
2651
+ if (this.heartbeatInterval) {
2652
+ clearInterval(this.heartbeatInterval);
2653
+ this.heartbeatInterval = null;
2123
2654
  }
2124
2655
  }
2656
+ sendHeartbeat() {
2657
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
2658
+ this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
2659
+ });
2660
+ }
2661
+ async delayAfterCleanup() {
2662
+ if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
2663
+ return;
2664
+ this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
2665
+ await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
2666
+ }
2125
2667
  async run() {
2126
2668
  this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
2669
+ const handleShutdown = () => {
2670
+ this.log("Received shutdown signal. Aborting...", "warn");
2671
+ this.aiRunner.abort();
2672
+ this.stopHeartbeat();
2673
+ this.cleanupTaskWorktree(this.currentWorktreePath, false);
2674
+ process.exit(1);
2675
+ };
2676
+ process.on("SIGTERM", handleShutdown);
2677
+ process.on("SIGINT", handleShutdown);
2678
+ this.startHeartbeat();
2127
2679
  const sprint = await this.getActiveSprint();
2128
2680
  if (sprint) {
2129
2681
  this.log(`Active sprint found: ${sprint.name}`, "info");
@@ -2133,31 +2685,62 @@ class AgentWorker {
2133
2685
  while (this.tasksCompleted < this.maxTasks) {
2134
2686
  const task = await this.getNextTask();
2135
2687
  if (!task) {
2136
- this.log("No tasks remaining. Running review on staged changes...", "info");
2137
- await this.runStagedChangesReview(sprint);
2688
+ this.log("No more tasks to process. Exiting.", "info");
2138
2689
  break;
2139
2690
  }
2140
2691
  this.log(`Claimed: ${task.title}`, "success");
2692
+ this.currentTaskId = task.id;
2693
+ this.sendHeartbeat();
2141
2694
  const result = await this.executeTask(task);
2142
- try {
2143
- await this.documentFetcher.fetch();
2144
- } catch (err) {
2145
- this.log(`Document fetch failed: ${err}`, "error");
2146
- }
2147
2695
  if (result.success) {
2148
- this.log(`Completed: ${task.title}`, "success");
2149
- await this.client.tasks.update(task.id, this.config.workspaceId, {
2150
- status: "VERIFICATION"
2151
- });
2152
- await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2153
- author: this.config.agentId,
2154
- text: `✅ ${result.summary}`
2155
- });
2156
- this.tasksCompleted++;
2696
+ if (result.noChanges) {
2697
+ this.log(`Blocked: ${task.title} - execution produced no file changes`, "warn");
2698
+ await this.client.tasks.update(task.id, this.config.workspaceId, {
2699
+ status: import_shared3.TaskStatus.BLOCKED,
2700
+ assignedTo: null
2701
+ });
2702
+ await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2703
+ author: this.config.agentId,
2704
+ text: `⚠️ Agent execution finished with no file changes, so no commit/branch/PR was created.
2705
+
2706
+ ${result.summary}`
2707
+ });
2708
+ } else {
2709
+ this.log(`Completed: ${task.title}`, "success");
2710
+ const updatePayload = {
2711
+ status: import_shared3.TaskStatus.IN_REVIEW
2712
+ };
2713
+ if (result.prUrl) {
2714
+ updatePayload.prUrl = result.prUrl;
2715
+ }
2716
+ await this.client.tasks.update(task.id, this.config.workspaceId, updatePayload);
2717
+ const branchInfo = result.branch ? `
2718
+
2719
+ Branch: \`${result.branch}\`` : "";
2720
+ const prInfo = result.prUrl ? `
2721
+ PR: ${result.prUrl}` : "";
2722
+ const prErrorInfo = result.prError ? `
2723
+ PR automation error: ${result.prError}` : "";
2724
+ await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2725
+ author: this.config.agentId,
2726
+ text: `✅ ${result.summary}${branchInfo}${prInfo}${prErrorInfo}`
2727
+ });
2728
+ this.tasksCompleted++;
2729
+ this.updateProgress(task, true);
2730
+ if (result.prUrl) {
2731
+ try {
2732
+ this.knowledgeBase.updateProgress({
2733
+ type: "pr_opened",
2734
+ title: task.title,
2735
+ details: `PR: ${result.prUrl}`
2736
+ });
2737
+ } catch {}
2738
+ }
2739
+ }
2157
2740
  } else {
2158
2741
  this.log(`Failed: ${task.title} - ${result.summary}`, "error");
2159
2742
  await this.client.tasks.update(task.id, this.config.workspaceId, {
2160
- status: "BACKLOG",
2743
+ status: import_shared3.TaskStatus.BACKLOG,
2161
2744
  assignedTo: null
2162
2745
  });
2163
2746
  await this.client.tasks.addComment(task.id, this.config.workspaceId, {
@@ -2165,11 +2748,18 @@ class AgentWorker {
2165
2748
  text: `❌ ${result.summary}`
2166
2749
  });
2167
2750
  }
2751
+ this.currentTaskId = null;
2752
+ this.sendHeartbeat();
2753
+ await this.delayAfterCleanup();
2168
2754
  }
2755
+ this.currentTaskId = null;
2756
+ this.stopHeartbeat();
2757
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
2169
2758
  process.exit(0);
2170
2759
  }
2171
2760
  }
2172
- if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
2761
+ var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
2762
+ if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
2173
2763
  process.title = "locus-worker";
2174
2764
  const args = process.argv.slice(2);
2175
2765
  const config = {};
@@ -2187,8 +2777,14 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
2187
2777
  config.apiKey = args[++i];
2188
2778
  else if (arg === "--project-path")
2189
2779
  config.projectPath = args[++i];
2780
+ else if (arg === "--main-project-path")
2781
+ config.mainProjectPath = args[++i];
2190
2782
  else if (arg === "--model")
2191
2783
  config.model = args[++i];
2784
+ else if (arg === "--use-worktrees")
2785
+ config.useWorktrees = true;
2786
+ else if (arg === "--auto-push")
2787
+ config.autoPush = true;
2192
2788
  else if (arg === "--provider") {
2193
2789
  const value = args[i + 1];
2194
2790
  if (value && !value.startsWith("--"))