@locusai/sdk 0.8.0 → 0.9.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 (73) 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 +2 -0
  7. package/dist/agent/index.d.ts.map +1 -1
  8. package/dist/agent/review-service.d.ts +21 -0
  9. package/dist/agent/review-service.d.ts.map +1 -0
  10. package/dist/agent/reviewer-worker.d.ts +42 -0
  11. package/dist/agent/reviewer-worker.d.ts.map +1 -0
  12. package/dist/agent/task-executor.d.ts +2 -2
  13. package/dist/agent/task-executor.d.ts.map +1 -1
  14. package/dist/agent/worker.d.ts +47 -5
  15. package/dist/agent/worker.d.ts.map +1 -1
  16. package/dist/agent/worker.js +1127 -470
  17. package/dist/ai/claude-runner.d.ts +6 -1
  18. package/dist/ai/claude-runner.d.ts.map +1 -1
  19. package/dist/ai/codex-runner.d.ts +5 -0
  20. package/dist/ai/codex-runner.d.ts.map +1 -1
  21. package/dist/ai/runner.d.ts +6 -1
  22. package/dist/ai/runner.d.ts.map +1 -1
  23. package/dist/core/config.d.ts +12 -2
  24. package/dist/core/config.d.ts.map +1 -1
  25. package/dist/core/index.d.ts +1 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/prompt-builder.d.ts +3 -6
  28. package/dist/core/prompt-builder.d.ts.map +1 -1
  29. package/dist/git/git-utils.d.ts +31 -0
  30. package/dist/git/git-utils.d.ts.map +1 -0
  31. package/dist/git/index.d.ts +3 -0
  32. package/dist/git/index.d.ts.map +1 -0
  33. package/dist/git/pr-service.d.ts +66 -0
  34. package/dist/git/pr-service.d.ts.map +1 -0
  35. package/dist/index-node.d.ts +5 -1
  36. package/dist/index-node.d.ts.map +1 -1
  37. package/dist/index-node.js +2461 -568
  38. package/dist/index.d.ts +0 -3
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +17 -49
  41. package/dist/modules/auth.d.ts +3 -0
  42. package/dist/modules/auth.d.ts.map +1 -1
  43. package/dist/modules/tasks.d.ts +0 -5
  44. package/dist/modules/tasks.d.ts.map +1 -1
  45. package/dist/modules/workspaces.d.ts +10 -10
  46. package/dist/modules/workspaces.d.ts.map +1 -1
  47. package/dist/orchestrator.d.ts +38 -5
  48. package/dist/orchestrator.d.ts.map +1 -1
  49. package/dist/planning/agents/architect.d.ts +15 -0
  50. package/dist/planning/agents/architect.d.ts.map +1 -0
  51. package/dist/planning/agents/sprint-organizer.d.ts +14 -0
  52. package/dist/planning/agents/sprint-organizer.d.ts.map +1 -0
  53. package/dist/planning/agents/tech-lead.d.ts +15 -0
  54. package/dist/planning/agents/tech-lead.d.ts.map +1 -0
  55. package/dist/planning/index.d.ts +4 -0
  56. package/dist/planning/index.d.ts.map +1 -0
  57. package/dist/planning/plan-manager.d.ts +52 -0
  58. package/dist/planning/plan-manager.d.ts.map +1 -0
  59. package/dist/planning/planning-meeting.d.ts +36 -0
  60. package/dist/planning/planning-meeting.d.ts.map +1 -0
  61. package/dist/planning/sprint-plan.d.ts +47 -0
  62. package/dist/planning/sprint-plan.d.ts.map +1 -0
  63. package/dist/project/knowledge-base.d.ts +25 -0
  64. package/dist/project/knowledge-base.d.ts.map +1 -0
  65. package/dist/worktree/index.d.ts +3 -0
  66. package/dist/worktree/index.d.ts.map +1 -0
  67. package/dist/worktree/worktree-config.d.ts +58 -0
  68. package/dist/worktree/worktree-config.d.ts.map +1 -0
  69. package/dist/worktree/worktree-manager.d.ts +96 -0
  70. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  71. package/package.json +2 -2
  72. package/dist/modules/ai.d.ts +0 -55
  73. 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,6 +480,7 @@ __export(exports_worker, {
512
480
  AgentWorker: () => AgentWorker
513
481
  });
514
482
  module.exports = __toCommonJS(exports_worker);
483
+ var import_shared3 = require("@locusai/shared");
515
484
 
516
485
  // src/core/config.ts
517
486
  var import_node_path = require("node:path");
@@ -521,28 +490,50 @@ var PROVIDER = {
521
490
  };
522
491
  var DEFAULT_MODEL = {
523
492
  [PROVIDER.CLAUDE]: "opus",
524
- [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`
525
499
  };
526
500
  var LOCUS_CONFIG = {
527
501
  dir: ".locus",
528
502
  configFile: "config.json",
503
+ settingsFile: "settings.json",
529
504
  indexFile: "codebase-index.json",
530
- contextFile: "CLAUDE.md",
505
+ contextFile: "LOCUS.md",
531
506
  artifactsDir: "artifacts",
532
507
  documentsDir: "documents",
533
- agentSkillsDir: ".agent/skills",
534
- sessionsDir: "sessions"
508
+ sessionsDir: "sessions",
509
+ reviewsDir: "reviews",
510
+ plansDir: "plans",
511
+ projectDir: "project",
512
+ projectContextFile: "context.md",
513
+ projectProgressFile: "progress.md"
535
514
  };
536
515
  var LOCUS_GITIGNORE_PATTERNS = [
537
516
  "# Locus AI - Session data (user-specific, can grow large)",
538
517
  ".locus/sessions/",
539
518
  "",
540
519
  "# Locus AI - Artifacts (local-only, user-specific)",
541
- ".locus/artifacts/"
520
+ ".locus/artifacts/",
521
+ "",
522
+ "# Locus AI - Review reports (generated per sprint)",
523
+ ".locus/reviews/",
524
+ "",
525
+ "# Locus AI - Plans (generated per task)",
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"
542
533
  ];
543
534
  function getLocusPath(projectPath, fileName) {
544
- if (fileName === "contextFile") {
545
- 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]);
546
537
  }
547
538
  return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
548
539
  }
@@ -618,6 +609,14 @@ var c = {
618
609
  };
619
610
 
620
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
+
621
620
  class ClaudeRunner {
622
621
  model;
623
622
  log;
@@ -625,6 +624,7 @@ class ClaudeRunner {
625
624
  eventEmitter;
626
625
  currentToolName;
627
626
  activeTools = new Map;
627
+ activeProcess = null;
628
628
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
629
629
  this.model = model;
630
630
  this.log = log;
@@ -633,7 +633,13 @@ class ClaudeRunner {
633
633
  setEventEmitter(emitter) {
634
634
  this.eventEmitter = emitter;
635
635
  }
636
- async run(prompt, _isPlanning = false) {
636
+ abort() {
637
+ if (this.activeProcess && !this.activeProcess.killed) {
638
+ this.activeProcess.kill("SIGTERM");
639
+ this.activeProcess = null;
640
+ }
641
+ }
642
+ async run(prompt) {
637
643
  const maxRetries = 3;
638
644
  let lastError = null;
639
645
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
@@ -661,7 +667,9 @@ class ClaudeRunner {
661
667
  "stream-json",
662
668
  "--include-partial-messages",
663
669
  "--model",
664
- this.model
670
+ this.model,
671
+ "--settings",
672
+ SANDBOX_SETTINGS
665
673
  ];
666
674
  const env = {
667
675
  ...process.env,
@@ -678,6 +686,7 @@ class ClaudeRunner {
678
686
  stdio: ["pipe", "pipe", "pipe"],
679
687
  env
680
688
  });
689
+ this.activeProcess = claude;
681
690
  let buffer = "";
682
691
  let stderrBuffer = "";
683
692
  let resolveChunk = null;
@@ -745,6 +754,7 @@ class ClaudeRunner {
745
754
  signalEnd();
746
755
  });
747
756
  claude.on("close", (code) => {
757
+ this.activeProcess = null;
748
758
  if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
749
759
  process.stderr.write(`${stderrBuffer}
750
760
  `);
@@ -902,7 +912,9 @@ class ClaudeRunner {
902
912
  "stream-json",
903
913
  "--include-partial-messages",
904
914
  "--model",
905
- this.model
915
+ this.model,
916
+ "--settings",
917
+ SANDBOX_SETTINGS
906
918
  ];
907
919
  const env = {
908
920
  ...process.env,
@@ -914,6 +926,7 @@ class ClaudeRunner {
914
926
  stdio: ["pipe", "pipe", "pipe"],
915
927
  env
916
928
  });
929
+ this.activeProcess = claude;
917
930
  let finalResult = "";
918
931
  let errorOutput = "";
919
932
  let buffer = "";
@@ -947,6 +960,7 @@ class ClaudeRunner {
947
960
  reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
948
961
  });
949
962
  claude.on("close", (code) => {
963
+ this.activeProcess = null;
950
964
  if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
951
965
  process.stderr.write(`${stderrBuffer}
952
966
  `);
@@ -1013,11 +1027,18 @@ class CodexRunner {
1013
1027
  projectPath;
1014
1028
  model;
1015
1029
  log;
1030
+ activeProcess = null;
1016
1031
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
1017
1032
  this.projectPath = projectPath;
1018
1033
  this.model = model;
1019
1034
  this.log = log;
1020
1035
  }
1036
+ abort() {
1037
+ if (this.activeProcess && !this.activeProcess.killed) {
1038
+ this.activeProcess.kill("SIGTERM");
1039
+ this.activeProcess = null;
1040
+ }
1041
+ }
1021
1042
  async run(prompt) {
1022
1043
  const maxRetries = 3;
1023
1044
  let lastError = null;
@@ -1044,6 +1065,7 @@ class CodexRunner {
1044
1065
  env: process.env,
1045
1066
  shell: false
1046
1067
  });
1068
+ this.activeProcess = codex;
1047
1069
  let resolveChunk = null;
1048
1070
  const chunkQueue = [];
1049
1071
  let processEnded = false;
@@ -1093,6 +1115,7 @@ class CodexRunner {
1093
1115
  signalEnd();
1094
1116
  });
1095
1117
  codex.on("close", (code) => {
1118
+ this.activeProcess = null;
1096
1119
  this.cleanupTempFile(outputPath);
1097
1120
  if (code === 0) {
1098
1121
  const result = this.readOutput(outputPath, finalOutput);
@@ -1138,6 +1161,7 @@ class CodexRunner {
1138
1161
  env: process.env,
1139
1162
  shell: false
1140
1163
  });
1164
+ this.activeProcess = codex;
1141
1165
  let output = "";
1142
1166
  let errorOutput = "";
1143
1167
  const handleOutput = (data) => {
@@ -1155,6 +1179,7 @@ class CodexRunner {
1155
1179
  reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
1156
1180
  });
1157
1181
  codex.on("close", (code) => {
1182
+ this.activeProcess = null;
1158
1183
  this.cleanupTempFile(outputPath);
1159
1184
  if (code === 0) {
1160
1185
  resolve2(this.readOutput(outputPath, output));
@@ -1169,7 +1194,8 @@ class CodexRunner {
1169
1194
  buildArgs(outputPath) {
1170
1195
  const args = [
1171
1196
  "exec",
1172
- "--full-auto",
1197
+ "--sandbox",
1198
+ "workspace-write",
1173
1199
  "--skip-git-repo-check",
1174
1200
  "--output-last-message",
1175
1201
  outputPath
@@ -1236,315 +1262,745 @@ function createAiRunner(provider, config) {
1236
1262
  }
1237
1263
  }
1238
1264
 
1239
- // src/core/indexer.ts
1240
- var import_node_crypto2 = require("node:crypto");
1241
- var import_node_fs2 = require("node:fs");
1242
- var import_node_path4 = require("node:path");
1243
- 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
+ }
1244
1357
 
1245
- class CodebaseIndexer {
1358
+ // src/git/pr-service.ts
1359
+ var import_node_child_process4 = require("node:child_process");
1360
+ class PrService {
1246
1361
  projectPath;
1247
- indexPath;
1248
- fullReindexRatioThreshold = 0.2;
1249
- constructor(projectPath) {
1362
+ log;
1363
+ constructor(projectPath, log) {
1250
1364
  this.projectPath = projectPath;
1251
- 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 };
1252
1407
  }
1253
- async index(onProgress, treeSummarizer, force = false) {
1254
- if (!treeSummarizer) {
1255
- 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.`);
1256
1411
  }
1257
- onProgress?.("Generating file tree...");
1258
- const currentFiles = await this.getFileTree();
1259
- const treeString = currentFiles.join(`
1260
- `);
1261
- const newTreeHash = this.hashTree(treeString);
1262
- const existingIndex = this.loadIndex();
1263
- const currentHashes = this.computeFileHashes(currentFiles);
1264
- const existingHashes = existingIndex?.fileHashes;
1265
- const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
1266
- const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
1267
- if (canIncremental) {
1268
- onProgress?.("Performing incremental update");
1269
- const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
1270
- const changedFiles = [...added, ...modified];
1271
- const totalChanges = changedFiles.length + deleted.length;
1272
- const existingFileCount = Object.keys(existingHashes).length;
1273
- onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
1274
- if (existingFileCount > 0) {
1275
- const changeRatio = totalChanges / existingFileCount;
1276
- if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
1277
- onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
1278
- const incrementalIndex = await treeSummarizer(changedFiles.join(`
1279
- `));
1280
- const updatedIndex = this.cloneIndex(existingIndex);
1281
- this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
1282
- return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
1283
- }
1284
- if (changedFiles.length === 0 && deleted.length > 0) {
1285
- onProgress?.(`Removing ${deleted.length} deleted files from index`);
1286
- const updatedIndex = this.cloneIndex(existingIndex);
1287
- this.removeFilesFromIndex(updatedIndex, deleted);
1288
- return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
1289
- }
1290
- if (changedFiles.length === 0 && deleted.length === 0) {
1291
- onProgress?.("No actual file changes, updating hashes only");
1292
- const updatedIndex = this.cloneIndex(existingIndex);
1293
- return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
1294
- }
1295
- onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
1296
- }
1412
+ if (!this.hasRemoteBranch(headBranch)) {
1413
+ throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
1297
1414
  }
1298
- onProgress?.("AI is analyzing codebase structure...");
1299
- try {
1300
- const index = await treeSummarizer(treeString);
1301
- return this.applyIndexMetadata(index, currentHashes, newTreeHash);
1302
- } catch (error) {
1303
- 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.`);
1304
1419
  }
1305
- }
1306
- async getFileTree() {
1307
- const gitmodulesPath = import_node_path4.join(this.projectPath, ".gitmodules");
1308
- const submoduleIgnores = [];
1309
- if (import_node_fs2.existsSync(gitmodulesPath)) {
1310
- try {
1311
- const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
1312
- const lines = content.split(`
1313
- `);
1314
- for (const line of lines) {
1315
- const match = line.match(/^\s*path\s*=\s*(.*)$/);
1316
- const path = match?.[1]?.trim();
1317
- if (path) {
1318
- submoduleIgnores.push(`${path}/**`);
1319
- submoduleIgnores.push(`**/${path}/**`);
1320
- }
1321
- }
1322
- } catch {}
1420
+ if (!headRef) {
1421
+ throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
1422
+ }
1423
+ const commitsAhead = this.countCommitsAhead(baseRef, headRef);
1424
+ if (commitsAhead <= 0) {
1425
+ throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
1323
1426
  }
1324
- return import_globby.globby(["**/*"], {
1427
+ }
1428
+ countCommitsAhead(baseRef, headRef) {
1429
+ const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
1325
1430
  cwd: this.projectPath,
1326
- gitignore: true,
1327
- ignore: [
1328
- ...submoduleIgnores,
1329
- "**/node_modules/**",
1330
- "**/dist/**",
1331
- "**/build/**",
1332
- "**/target/**",
1333
- "**/bin/**",
1334
- "**/obj/**",
1335
- "**/.next/**",
1336
- "**/.svelte-kit/**",
1337
- "**/.nuxt/**",
1338
- "**/.cache/**",
1339
- "**/out/**",
1340
- "**/__tests__/**",
1341
- "**/coverage/**",
1342
- "**/*.test.*",
1343
- "**/*.spec.*",
1344
- "**/*.d.ts",
1345
- "**/tsconfig.tsbuildinfo",
1346
- "**/.locus/*.json",
1347
- "**/.locus/*.md",
1348
- "**/.locus/!(artifacts)/**",
1349
- "**/.git/**",
1350
- "**/.svn/**",
1351
- "**/.hg/**",
1352
- "**/.vscode/**",
1353
- "**/.idea/**",
1354
- "**/.DS_Store",
1355
- "**/bun.lock",
1356
- "**/package-lock.json",
1357
- "**/yarn.lock",
1358
- "**/pnpm-lock.yaml",
1359
- "**/Cargo.lock",
1360
- "**/go.sum",
1361
- "**/poetry.lock",
1362
- "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
1363
- ]
1364
- });
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;
1365
1436
  }
1366
- loadIndex() {
1367
- if (import_node_fs2.existsSync(this.indexPath)) {
1368
- try {
1369
- return JSON.parse(import_node_fs2.readFileSync(this.indexPath, "utf-8"));
1370
- } catch {
1371
- return null;
1372
- }
1437
+ resolveBranchRef(branch) {
1438
+ if (this.hasLocalBranch(branch)) {
1439
+ return branch;
1440
+ }
1441
+ if (this.hasRemoteTrackingBranch(branch)) {
1442
+ return `origin/${branch}`;
1373
1443
  }
1374
1444
  return null;
1375
1445
  }
1376
- saveIndex(index) {
1377
- const dir = import_node_path4.dirname(this.indexPath);
1378
- if (!import_node_fs2.existsSync(dir)) {
1379
- 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;
1380
1455
  }
1381
- import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
1382
1456
  }
1383
- cloneIndex(index) {
1384
- 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
+ }
1385
1467
  }
1386
- applyIndexMetadata(index, fileHashes, treeHash) {
1387
- index.lastIndexed = new Date().toISOString();
1388
- index.treeHash = treeHash;
1389
- index.fileHashes = fileHashes;
1390
- 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
+ }
1391
1478
  }
1392
- hashTree(tree) {
1393
- return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
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
+ });
1486
+ }
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
+ }
1394
1513
  }
1395
- hashFile(filePath) {
1514
+ listLocusPrs() {
1396
1515
  try {
1397
- const content = import_node_fs2.readFileSync(import_node_path4.join(this.projectPath, filePath), "utf-8");
1398
- 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
+ }));
1399
1537
  } catch {
1400
- return null;
1538
+ this.log("Failed to list Locus PRs", "warn");
1539
+ return [];
1401
1540
  }
1402
1541
  }
1403
- computeFileHashes(files) {
1404
- const hashes = {};
1405
- for (const file of files) {
1406
- const hash = this.hashFile(file);
1407
- if (hash !== null) {
1408
- hashes[file] = hash;
1409
- }
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;
1410
1553
  }
1411
- return hashes;
1412
1554
  }
1413
- diffFiles(currentHashes, existingHashes) {
1414
- const currentFiles = Object.keys(currentHashes);
1415
- const existingFiles = Object.keys(existingHashes);
1416
- const existingSet = new Set(existingFiles);
1417
- const currentSet = new Set(currentFiles);
1418
- const added = currentFiles.filter((f) => !existingSet.has(f));
1419
- const deleted = existingFiles.filter((f) => !currentSet.has(f));
1420
- const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
1421
- return { added, deleted, modified };
1555
+ listUnreviewedLocusPrs() {
1556
+ const allPrs = this.listLocusPrs();
1557
+ return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
1422
1558
  }
1423
- removeFilesFromIndex(index, files) {
1424
- const fileSet = new Set(files);
1425
- for (const file of files) {
1426
- 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("");
1427
1566
  }
1428
- for (const [symbol, paths] of Object.entries(index.symbols)) {
1429
- index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
1430
- if (index.symbols[symbol].length === 0) {
1431
- 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}`);
1432
1571
  }
1572
+ sections.push("");
1433
1573
  }
1434
- }
1435
- mergeIndex(existing, incremental, newHashes, newTreeHash) {
1436
- const mergedSymbols = { ...existing.symbols };
1437
- for (const [symbol, paths] of Object.entries(incremental.symbols)) {
1438
- if (mergedSymbols[symbol]) {
1439
- mergedSymbols[symbol] = [
1440
- ...new Set([...mergedSymbols[symbol], ...paths])
1441
- ];
1442
- } else {
1443
- mergedSymbols[symbol] = paths;
1444
- }
1574
+ if (summary) {
1575
+ sections.push("## Agent Summary");
1576
+ sections.push(summary);
1577
+ sections.push("");
1445
1578
  }
1446
- const merged = {
1447
- symbols: mergedSymbols,
1448
- responsibilities: {
1449
- ...existing.responsibilities,
1450
- ...incremental.responsibilities
1451
- },
1452
- lastIndexed: ""
1453
- };
1454
- 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;
1455
1587
  }
1456
1588
  }
1457
1589
 
1458
- // src/agent/codebase-indexer-service.ts
1459
- class CodebaseIndexerService {
1460
- deps;
1461
- indexer;
1462
- constructor(deps) {
1463
- this.deps = deps;
1464
- 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");
1465
1599
  }
1466
- async reindex(force = false) {
1467
- try {
1468
- const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
1469
- const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
1470
- 1. Key symbols (classes, functions, types) and their locations
1471
- 2. Responsibilities of each directory/file
1472
- 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
1473
1658
 
1474
- Analyze this file tree and provide a JSON response with:
1475
- - "symbols": object mapping symbol names to file paths (array)
1476
- - "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(`
1477
1673
 
1478
- File tree:
1479
- ${tree}
1674
+ ---
1480
1675
 
1481
- Return ONLY valid JSON, no markdown formatting.`;
1482
- const response = await this.deps.aiRunner.run(prompt, true);
1483
- const jsonMatch = response.match(/\{[\s\S]*\}/);
1484
- if (jsonMatch) {
1485
- return JSON.parse(jsonMatch[0]);
1486
- }
1487
- return { symbols: {}, responsibilities: {}, lastIndexed: "" };
1488
- }, force);
1489
- if (index === null) {
1490
- this.deps.log("No changes detected, skipping reindex", "info");
1491
- return;
1492
- }
1493
- this.indexer.saveIndex(index);
1494
- this.deps.log("Codebase reindexed successfully", "success");
1495
- } catch (error) {
1496
- 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 });
1497
1714
  }
1498
1715
  }
1499
1716
  }
1500
1717
 
1501
- // src/agent/document-fetcher.ts
1718
+ // src/worktree/worktree-manager.ts
1719
+ var import_node_child_process5 = require("node:child_process");
1502
1720
  var import_node_fs3 = require("node:fs");
1503
1721
  var import_node_path5 = require("node:path");
1504
- class DocumentFetcher {
1505
- deps;
1506
- constructor(deps) {
1507
- 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
+ });
1508
1743
  }
1509
- async fetch() {
1510
- const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
1511
- if (!import_node_fs3.existsSync(documentsDir)) {
1512
- 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
+ }
1513
1766
  }
1514
- try {
1515
- const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
1516
- const groupMap = new Map(groups.map((g) => [g.id, g.name]));
1517
- const docs2 = await this.deps.client.docs.list(this.deps.workspaceId);
1518
- const artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
1519
- let fetchedCount = 0;
1520
- for (const doc of docs2) {
1521
- if (doc.groupId === artifactsGroupId) {
1522
- continue;
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.`);
1523
1774
  }
1524
- const groupName = groupMap.get(doc.groupId || "") || "General";
1525
- const groupDir = import_node_path5.join(documentsDir, groupName);
1526
- if (!import_node_fs3.existsSync(groupDir)) {
1527
- import_node_fs3.mkdirSync(groupDir, { recursive: true });
1528
- }
1529
- const fileName = `${doc.title}.md`;
1530
- const filePath = import_node_path5.join(groupDir, fileName);
1531
- if (!import_node_fs3.existsSync(filePath) || import_node_fs3.readFileSync(filePath, "utf-8") !== doc.content) {
1532
- import_node_fs3.writeFileSync(filePath, doc.content || "");
1533
- fetchedCount++;
1775
+ this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
1776
+ this.remove(worktreePath2, false);
1777
+ }
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);
1783
+ }
1784
+ }
1785
+ const addWorktree = () => this.git(`worktree add "${worktreePath}" -b "${branch}" "${baseBranch}"`, this.projectPath);
1786
+ try {
1787
+ addWorktree();
1788
+ } catch (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)";
1534
1829
  }
1535
1830
  }
1536
- if (fetchedCount > 0) {
1537
- this.deps.log(`Fetched ${fetchedCount} document(s) from server`, "info");
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
+ }
1837
+ }
1838
+ return worktrees;
1839
+ }
1840
+ listAgentWorktrees() {
1841
+ return this.list().filter((wt) => !wt.isMain);
1842
+ }
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");
1849
+ try {
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");
1538
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");
1902
+ return null;
1903
+ }
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");
1913
+ try {
1914
+ this.gitExec(["push", "-u", remote, branch], worktreePath);
1915
+ this.log(`Pushed ${branch} to ${remote}`, "success");
1916
+ return branch;
1539
1917
  } catch (error) {
1540
- this.deps.log(`Failed to fetch documents: ${error}`, "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");
1927
+ }
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;
1541
1942
  }
1542
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
+ });
1999
+ }
1543
2000
  }
1544
2001
 
1545
2002
  // src/core/prompt-builder.ts
1546
2003
  var import_node_fs4 = require("node:fs");
1547
- var import_node_os2 = require("node:os");
1548
2004
  var import_node_path6 = require("node:path");
1549
2005
  var import_shared2 = require("@locusai/shared");
1550
2006
  class PromptBuilder {
@@ -1613,11 +2069,12 @@ ${fallback}
1613
2069
  if (serverContext) {
1614
2070
  prompt += `## Project Context (Server)
1615
2071
  `;
1616
- if (serverContext.project) {
1617
- prompt += `- Project: ${serverContext.project.name || "Unknown"}
2072
+ const project = serverContext.project;
2073
+ if (project) {
2074
+ prompt += `- Project: ${project.name || "Unknown"}
1618
2075
  `;
1619
- if (!hasLocalContext && serverContext.project.techStack?.length) {
1620
- prompt += `- Tech Stack: ${serverContext.project.techStack.join(", ")}
2076
+ if (!hasLocalContext && project.techStack?.length) {
2077
+ prompt += `- Tech Stack: ${project.techStack.join(", ")}
1621
2078
  `;
1622
2079
  }
1623
2080
  }
@@ -1630,12 +2087,11 @@ ${serverContext.context}
1630
2087
  `;
1631
2088
  }
1632
2089
  prompt += this.getProjectStructure();
1633
- prompt += this.getSkillsInfo();
1634
2090
  prompt += `## Project Knowledge Base
1635
2091
  `;
1636
2092
  prompt += `You have access to the following documentation directories for context:
1637
2093
  `;
1638
- prompt += `- Artifacts: \`.locus/artifacts\`)
2094
+ prompt += `- Artifacts: \`.locus/artifacts\`
1639
2095
  `;
1640
2096
  prompt += `- Documents: \`.locus/documents\`
1641
2097
  `;
@@ -1693,11 +2149,9 @@ ${comment.text}
1693
2149
  }
1694
2150
  }
1695
2151
  prompt += `## Instructions
1696
- 1. Complete this task.
2152
+ 1. Complete this task.
1697
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.
1698
- 3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
1699
- 4. When finished successfully, output: <promise>COMPLETE</promise>
1700
- `;
2154
+ 3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
1701
2155
  return prompt;
1702
2156
  }
1703
2157
  async buildGenericPrompt(query) {
@@ -1744,7 +2198,6 @@ ${fallback}
1744
2198
  }
1745
2199
  }
1746
2200
  prompt += this.getProjectStructure();
1747
- prompt += this.getSkillsInfo();
1748
2201
  prompt += `## Project Knowledge Base
1749
2202
  `;
1750
2203
  prompt += `You have access to the following documentation directories for context:
@@ -1765,9 +2218,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1765
2218
  }
1766
2219
  prompt += `## Instructions
1767
2220
  1. Execute the prompt based on the provided project context.
1768
- 2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
1769
- 3. When finished successfully, output: <promise>COMPLETE</promise>
1770
- `;
2221
+ 2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
1771
2222
  return prompt;
1772
2223
  }
1773
2224
  getProjectConfig() {
@@ -1823,55 +2274,6 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1823
2274
  return "";
1824
2275
  }
1825
2276
  }
1826
- getSkillsInfo() {
1827
- const projectSkillsDirs = [
1828
- LOCUS_CONFIG.agentSkillsDir,
1829
- ".cursor/skills",
1830
- ".claude/skills",
1831
- ".codex/skills",
1832
- ".gemini/skills"
1833
- ];
1834
- const globalHome = import_node_os2.homedir();
1835
- const globalSkillsDirs = [
1836
- import_node_path6.join(globalHome, ".cursor/skills"),
1837
- import_node_path6.join(globalHome, ".claude/skills"),
1838
- import_node_path6.join(globalHome, ".codex/skills"),
1839
- import_node_path6.join(globalHome, ".gemini/skills")
1840
- ];
1841
- const allSkillNames = new Set;
1842
- for (const relativePath of projectSkillsDirs) {
1843
- const fullPath = import_node_path6.join(this.projectPath, relativePath);
1844
- this.scanSkillsInDirectory(fullPath, allSkillNames);
1845
- }
1846
- for (const fullPath of globalSkillsDirs) {
1847
- this.scanSkillsInDirectory(fullPath, allSkillNames);
1848
- }
1849
- const uniqueSkills = Array.from(allSkillNames).sort();
1850
- if (uniqueSkills.length === 0)
1851
- return "";
1852
- return `## Available Agent Skills
1853
- ` + `The project has the following specialized skills available (from project or global locations):
1854
- ` + uniqueSkills.map((s) => `- ${s}`).join(`
1855
- `) + `
1856
-
1857
- `;
1858
- }
1859
- scanSkillsInDirectory(dirPath, skillSet) {
1860
- if (!import_node_fs4.existsSync(dirPath))
1861
- return;
1862
- try {
1863
- const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
1864
- try {
1865
- return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
1866
- } catch {
1867
- return false;
1868
- }
1869
- });
1870
- for (const entry of entries) {
1871
- skillSet.add(entry);
1872
- }
1873
- } catch {}
1874
- }
1875
2277
  roleToText(role) {
1876
2278
  if (!role) {
1877
2279
  return null;
@@ -1901,42 +2303,15 @@ class TaskExecutor {
1901
2303
  this.deps = deps;
1902
2304
  this.promptBuilder = new PromptBuilder(deps.projectPath);
1903
2305
  }
1904
- async execute(task, context) {
2306
+ async execute(task) {
1905
2307
  this.deps.log(`Executing: ${task.title}`, "info");
1906
- const basePrompt = await this.promptBuilder.build(task, {
1907
- taskContext: context
1908
- });
2308
+ const basePrompt = await this.promptBuilder.build(task);
1909
2309
  try {
1910
- let plan = null;
1911
- this.deps.log("Phase 1: Planning (CLI)...", "info");
1912
- const planningPrompt = `${basePrompt}
1913
-
1914
- ## Phase 1: Planning
1915
- Analyze and create a detailed plan for THIS SPECIFIC TASK. Do NOT execute changes yet.`;
1916
- plan = await this.deps.aiRunner.run(planningPrompt, true);
1917
2310
  this.deps.log("Starting Execution...", "info");
1918
- let executionPrompt = basePrompt;
1919
- if (plan != null) {
1920
- executionPrompt += `
1921
-
1922
- ## Phase 2: Execution
1923
- Based on the plan, execute the task:
1924
-
1925
- ${plan}`;
1926
- } else {
1927
- executionPrompt += `
1928
-
1929
- ## Execution
1930
- Execute the task directly.`;
1931
- }
1932
- executionPrompt += `
1933
-
1934
- When finished, output: <promise>COMPLETE</promise>`;
1935
- const output = await this.deps.aiRunner.run(executionPrompt);
1936
- const success = output.includes("<promise>COMPLETE</promise>");
2311
+ await this.deps.aiRunner.run(basePrompt);
1937
2312
  return {
1938
- success,
1939
- summary: success ? "Task completed by the agent" : "The agent did not signal completion"
2313
+ success: true,
2314
+ summary: "Task completed by the agent"
1940
2315
  };
1941
2316
  } catch (error) {
1942
2317
  return { success: false, summary: `Error: ${error}` };
@@ -1960,14 +2335,17 @@ class AgentWorker {
1960
2335
  config;
1961
2336
  client;
1962
2337
  aiRunner;
1963
- indexerService;
1964
- documentFetcher;
1965
2338
  taskExecutor;
1966
- consecutiveEmpty = 0;
1967
- maxEmpty = 60;
2339
+ knowledgeBase;
2340
+ worktreeManager = null;
2341
+ prService = null;
1968
2342
  maxTasks = 50;
1969
2343
  tasksCompleted = 0;
1970
- pollInterval = 1e4;
2344
+ heartbeatInterval = null;
2345
+ currentTaskId = null;
2346
+ currentWorktreePath = null;
2347
+ postCleanupDelayMs = 5000;
2348
+ ghUsername = null;
1971
2349
  constructor(config) {
1972
2350
  this.config = config;
1973
2351
  const projectPath = config.projectPath || process.cwd();
@@ -1982,30 +2360,47 @@ class AgentWorker {
1982
2360
  }
1983
2361
  });
1984
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
+ }
1985
2376
  const provider = config.provider ?? PROVIDER.CLAUDE;
1986
2377
  this.aiRunner = createAiRunner(provider, {
1987
2378
  projectPath,
1988
2379
  model: config.model,
1989
2380
  log
1990
2381
  });
1991
- this.indexerService = new CodebaseIndexerService({
1992
- aiRunner: this.aiRunner,
1993
- projectPath,
1994
- log
1995
- });
1996
- this.documentFetcher = new DocumentFetcher({
1997
- client: this.client,
1998
- workspaceId: config.workspaceId,
1999
- projectPath,
2000
- log
2001
- });
2002
2382
  this.taskExecutor = new TaskExecutor({
2003
2383
  aiRunner: this.aiRunner,
2004
2384
  projectPath,
2005
2385
  log
2006
2386
  });
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
+ }
2007
2396
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
2008
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
+ }
2009
2404
  }
2010
2405
  log(message, level = "info") {
2011
2406
  const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
@@ -2029,74 +2424,323 @@ class AgentWorker {
2029
2424
  }
2030
2425
  }
2031
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
+ }
2032
2488
  try {
2033
- const task = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
2034
- return task;
2035
- } catch (error) {
2036
- this.log(`No task dispatched: ${error instanceof Error ? error.message : String(error)}`, "info");
2037
- 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 };
2542
+ }
2543
+ }
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");
2551
+ try {
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 };
2560
+ } catch (err) {
2561
+ const errorMessage = err instanceof Error ? err.message : String(err);
2562
+ this.log(`PR creation failed: ${errorMessage}`, "error");
2563
+ return { url: null, error: errorMessage };
2038
2564
  }
2039
2565
  }
2566
+ cleanupTaskWorktree(worktreePath, keepBranch) {
2567
+ if (!this.worktreeManager || !worktreePath)
2568
+ return;
2569
+ try {
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
+ }
2040
2577
  async executeTask(task) {
2041
2578
  const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
2042
- let context = "";
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");
2611
+ }
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;
2625
+ } else {
2626
+ this.cleanupTaskWorktree(worktreePath, keepBranch);
2627
+ }
2628
+ }
2629
+ }
2630
+ updateProgress(task, success) {
2043
2631
  try {
2044
- context = await this.client.tasks.getContext(task.id, this.config.workspaceId);
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");
2639
+ }
2045
2640
  } catch (err) {
2046
- this.log(`Failed to fetch task context: ${err}`, "warn");
2641
+ this.log(`Failed to update progress: ${err instanceof Error ? err.message : String(err)}`, "warn");
2047
2642
  }
2048
- const result = await this.taskExecutor.execute(fullTask, context);
2049
- await this.indexerService.reindex();
2050
- return result;
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;
2654
+ }
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));
2051
2666
  }
2052
2667
  async run() {
2053
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();
2054
2679
  const sprint = await this.getActiveSprint();
2055
2680
  if (sprint) {
2056
- this.log(`Active sprint found: ${sprint.name}. Ensuring plan is up to date...`, "info");
2057
- try {
2058
- await this.client.sprints.triggerAIPlanning(sprint.id, this.config.workspaceId);
2059
- this.log(`Sprint plan sync checked on server.`, "success");
2060
- } catch (err) {
2061
- this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
2062
- }
2681
+ this.log(`Active sprint found: ${sprint.name}`, "info");
2063
2682
  } else {
2064
- this.log("No active sprint found for planning.", "warn");
2683
+ this.log("No active sprint found.", "warn");
2065
2684
  }
2066
- while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
2685
+ while (this.tasksCompleted < this.maxTasks) {
2067
2686
  const task = await this.getNextTask();
2068
2687
  if (!task) {
2069
- if (this.consecutiveEmpty === 0) {
2070
- this.log("Queue empty, waiting for tasks...", "info");
2071
- }
2072
- this.consecutiveEmpty++;
2073
- if (this.consecutiveEmpty >= this.maxEmpty)
2074
- break;
2075
- await new Promise((r) => setTimeout(r, this.pollInterval));
2076
- continue;
2688
+ this.log("No more tasks to process. Exiting.", "info");
2689
+ break;
2077
2690
  }
2078
- this.consecutiveEmpty = 0;
2079
2691
  this.log(`Claimed: ${task.title}`, "success");
2692
+ this.currentTaskId = task.id;
2693
+ this.sendHeartbeat();
2080
2694
  const result = await this.executeTask(task);
2081
- try {
2082
- await this.documentFetcher.fetch();
2083
- } catch (err) {
2084
- this.log(`Document fetch failed: ${err}`, "error");
2085
- }
2086
2695
  if (result.success) {
2087
- this.log(`Completed: ${task.title}`, "success");
2088
- await this.client.tasks.update(task.id, this.config.workspaceId, {
2089
- status: "VERIFICATION"
2090
- });
2091
- await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2092
- author: this.config.agentId,
2093
- text: `✅ ${result.summary}`
2094
- });
2095
- 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
+ }
2096
2740
  } else {
2097
2741
  this.log(`Failed: ${task.title} - ${result.summary}`, "error");
2098
2742
  await this.client.tasks.update(task.id, this.config.workspaceId, {
2099
- status: "BACKLOG",
2743
+ status: import_shared3.TaskStatus.BACKLOG,
2100
2744
  assignedTo: null
2101
2745
  });
2102
2746
  await this.client.tasks.addComment(task.id, this.config.workspaceId, {
@@ -2104,11 +2748,18 @@ class AgentWorker {
2104
2748
  text: `❌ ${result.summary}`
2105
2749
  });
2106
2750
  }
2751
+ this.currentTaskId = null;
2752
+ this.sendHeartbeat();
2753
+ await this.delayAfterCleanup();
2107
2754
  }
2755
+ this.currentTaskId = null;
2756
+ this.stopHeartbeat();
2757
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
2108
2758
  process.exit(0);
2109
2759
  }
2110
2760
  }
2111
- 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") {
2112
2763
  process.title = "locus-worker";
2113
2764
  const args = process.argv.slice(2);
2114
2765
  const config = {};
@@ -2126,8 +2777,14 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
2126
2777
  config.apiKey = args[++i];
2127
2778
  else if (arg === "--project-path")
2128
2779
  config.projectPath = args[++i];
2780
+ else if (arg === "--main-project-path")
2781
+ config.mainProjectPath = args[++i];
2129
2782
  else if (arg === "--model")
2130
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;
2131
2788
  else if (arg === "--provider") {
2132
2789
  const value = args[i + 1];
2133
2790
  if (value && !value.startsWith("--"))