@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;
1252
1366
  }
1253
- async index(onProgress, treeSummarizer, force = false) {
1254
- if (!treeSummarizer) {
1255
- throw new Error("A treeSummarizer is required for this indexing method.");
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})`);
1256
1378
  }
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
- }
1379
+ if (!isGhAvailable(this.projectPath)) {
1380
+ throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
1297
1381
  }
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)}`);
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 };
1407
+ }
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.`);
1304
1411
  }
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 {}
1412
+ if (!this.hasRemoteBranch(headBranch)) {
1413
+ throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
1414
+ }
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.`);
1419
+ }
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(["**/*"], {
1325
- 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
- });
1365
1427
  }
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
- }
1428
+ countCommitsAhead(baseRef, headRef) {
1429
+ const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
1430
+ cwd: this.projectPath,
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;
1436
+ }
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
+ });
1394
1486
  }
1395
- hashFile(filePath) {
1487
+ submitReview(prIdentifier, body, event) {
1396
1488
  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);
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
+ }
1513
+ }
1514
+ listLocusPrs() {
1515
+ try {
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;
1523
- }
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 });
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.`);
1528
1774
  }
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");
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");
1538
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;
1942
+ }
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 {}
1541
1980
  }
1542
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
- this.taskExecutor = new TaskExecutor({
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,87 +2424,984 @@ 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 };
2564
+ }
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");
2038
2574
  }
2575
+ this.currentWorktreePath = null;
2039
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");
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;
2047
2654
  }
2048
- const result = await this.taskExecutor.execute(fullTask, context);
2049
- await this.indexerService.reindex();
2050
- return result;
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");
2684
+ }
2685
+ while (this.tasksCompleted < this.maxTasks) {
2686
+ const task = await this.getNextTask();
2687
+ if (!task) {
2688
+ this.log("No more tasks to process. Exiting.", "info");
2689
+ break;
2690
+ }
2691
+ this.log(`Claimed: ${task.title}`, "success");
2692
+ this.currentTaskId = task.id;
2693
+ this.sendHeartbeat();
2694
+ const result = await this.executeTask(task);
2695
+ if (result.success) {
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
+ }
2740
+ } else {
2741
+ this.log(`Failed: ${task.title} - ${result.summary}`, "error");
2742
+ await this.client.tasks.update(task.id, this.config.workspaceId, {
2743
+ status: import_shared3.TaskStatus.BACKLOG,
2744
+ assignedTo: null
2745
+ });
2746
+ await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2747
+ author: this.config.agentId,
2748
+ text: `❌ ${result.summary}`
2749
+ });
2750
+ }
2751
+ this.currentTaskId = null;
2752
+ this.sendHeartbeat();
2753
+ await this.delayAfterCleanup();
2754
+ }
2755
+ this.currentTaskId = null;
2756
+ this.stopHeartbeat();
2757
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
2758
+ process.exit(0);
2759
+ }
2760
+ }
2761
+ var workerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
2762
+ if (workerEntrypoint === "worker.js" || workerEntrypoint === "worker.ts") {
2763
+ process.title = "locus-worker";
2764
+ const args = process.argv.slice(2);
2765
+ const config = {};
2766
+ for (let i = 0;i < args.length; i++) {
2767
+ const arg = args[i];
2768
+ if (arg === "--agent-id")
2769
+ config.agentId = args[++i];
2770
+ else if (arg === "--workspace-id")
2771
+ config.workspaceId = args[++i];
2772
+ else if (arg === "--sprint-id")
2773
+ config.sprintId = args[++i];
2774
+ else if (arg === "--api-url")
2775
+ config.apiBase = args[++i];
2776
+ else if (arg === "--api-key")
2777
+ config.apiKey = args[++i];
2778
+ else if (arg === "--project-path")
2779
+ config.projectPath = args[++i];
2780
+ else if (arg === "--main-project-path")
2781
+ config.mainProjectPath = args[++i];
2782
+ else if (arg === "--model")
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;
2788
+ else if (arg === "--provider") {
2789
+ const value = args[i + 1];
2790
+ if (value && !value.startsWith("--"))
2791
+ i++;
2792
+ config.provider = resolveProvider(value);
2793
+ }
2794
+ }
2795
+ if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
2796
+ console.error("Missing required arguments");
2797
+ process.exit(1);
2798
+ }
2799
+ const worker = new AgentWorker(config);
2800
+ worker.run().catch((err) => {
2801
+ console.error("Fatal worker error:", err);
2802
+ process.exit(1);
2803
+ });
2804
+ }
2805
+
2806
+ // src/index-node.ts
2807
+ var exports_index_node = {};
2808
+ __export(exports_index_node, {
2809
+ sprintPlanToMarkdown: () => sprintPlanToMarkdown,
2810
+ plannedTasksToCreatePayloads: () => plannedTasksToCreatePayloads,
2811
+ parseSprintPlanFromAI: () => parseSprintPlanFromAI,
2812
+ getRemoteUrl: () => getRemoteUrl,
2813
+ getLocusPath: () => getLocusPath,
2814
+ getDefaultBranch: () => getDefaultBranch,
2815
+ getCurrentBranch: () => getCurrentBranch,
2816
+ getAgentArtifactsPath: () => getAgentArtifactsPath,
2817
+ detectRemoteProvider: () => detectRemoteProvider,
2818
+ createAiRunner: () => createAiRunner,
2819
+ c: () => c,
2820
+ WorktreeManager: () => WorktreeManager,
2821
+ WorkspacesModule: () => WorkspacesModule,
2822
+ WORKTREE_ROOT_DIR: () => WORKTREE_ROOT_DIR,
2823
+ WORKTREE_BRANCH_PREFIX: () => WORKTREE_BRANCH_PREFIX,
2824
+ TasksModule: () => TasksModule,
2825
+ TaskExecutor: () => TaskExecutor,
2826
+ SprintsModule: () => SprintsModule,
2827
+ ReviewerWorker: () => ReviewerWorker,
2828
+ ReviewService: () => ReviewService,
2829
+ PromptBuilder: () => PromptBuilder,
2830
+ PrService: () => PrService,
2831
+ PlanningMeeting: () => PlanningMeeting,
2832
+ PlanManager: () => PlanManager,
2833
+ PROVIDER: () => PROVIDER,
2834
+ OrganizationsModule: () => OrganizationsModule,
2835
+ LocusEvent: () => LocusEvent,
2836
+ LocusEmitter: () => LocusEmitter,
2837
+ LocusClient: () => LocusClient,
2838
+ LOCUS_SCHEMA_BASE_URL: () => LOCUS_SCHEMA_BASE_URL,
2839
+ LOCUS_SCHEMAS: () => LOCUS_SCHEMAS,
2840
+ LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
2841
+ LOCUS_CONFIG: () => LOCUS_CONFIG,
2842
+ KnowledgeBase: () => KnowledgeBase,
2843
+ InvitationsModule: () => InvitationsModule,
2844
+ HistoryManager: () => HistoryManager,
2845
+ ExecSession: () => ExecSession,
2846
+ ExecEventType: () => ExecEventType,
2847
+ ExecEventEmitter: () => ExecEventEmitter,
2848
+ DocumentFetcher: () => DocumentFetcher,
2849
+ DocsModule: () => DocsModule,
2850
+ DEFAULT_WORKTREE_CONFIG: () => DEFAULT_WORKTREE_CONFIG,
2851
+ DEFAULT_MODEL: () => DEFAULT_MODEL,
2852
+ ContextTracker: () => ContextTracker,
2853
+ CodexRunner: () => CodexRunner,
2854
+ CodebaseIndexerService: () => CodebaseIndexerService,
2855
+ CodebaseIndexer: () => CodebaseIndexer,
2856
+ ClaudeRunner: () => ClaudeRunner,
2857
+ CiModule: () => CiModule,
2858
+ AuthModule: () => AuthModule,
2859
+ AgentWorker: () => AgentWorker,
2860
+ AgentOrchestrator: () => AgentOrchestrator
2861
+ });
2862
+ module.exports = __toCommonJS(exports_index_node);
2863
+
2864
+ // src/core/indexer.ts
2865
+ var import_node_crypto2 = require("node:crypto");
2866
+ var import_node_fs5 = require("node:fs");
2867
+ var import_node_path7 = require("node:path");
2868
+ var import_globby = require("globby");
2869
+
2870
+ class CodebaseIndexer {
2871
+ projectPath;
2872
+ indexPath;
2873
+ fullReindexRatioThreshold = 0.2;
2874
+ constructor(projectPath) {
2875
+ this.projectPath = projectPath;
2876
+ this.indexPath = import_node_path7.join(projectPath, ".locus", "codebase-index.json");
2877
+ }
2878
+ async index(onProgress, treeSummarizer, force = false) {
2879
+ if (!treeSummarizer) {
2880
+ throw new Error("A treeSummarizer is required for this indexing method.");
2881
+ }
2882
+ onProgress?.("Generating file tree...");
2883
+ const currentFiles = await this.getFileTree();
2884
+ const treeString = currentFiles.join(`
2885
+ `);
2886
+ const newTreeHash = this.hashTree(treeString);
2887
+ const existingIndex = this.loadIndex();
2888
+ const currentHashes = this.computeFileHashes(currentFiles);
2889
+ const existingHashes = existingIndex?.fileHashes;
2890
+ const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
2891
+ const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
2892
+ if (canIncremental) {
2893
+ onProgress?.("Performing incremental update");
2894
+ const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
2895
+ const changedFiles = [...added, ...modified];
2896
+ const totalChanges = changedFiles.length + deleted.length;
2897
+ const existingFileCount = Object.keys(existingHashes).length;
2898
+ onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
2899
+ if (existingFileCount > 0) {
2900
+ const changeRatio = totalChanges / existingFileCount;
2901
+ if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
2902
+ onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
2903
+ const incrementalIndex = await treeSummarizer(changedFiles.join(`
2904
+ `));
2905
+ const updatedIndex = this.cloneIndex(existingIndex);
2906
+ this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
2907
+ return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
2908
+ }
2909
+ if (changedFiles.length === 0 && deleted.length > 0) {
2910
+ onProgress?.(`Removing ${deleted.length} deleted files from index`);
2911
+ const updatedIndex = this.cloneIndex(existingIndex);
2912
+ this.removeFilesFromIndex(updatedIndex, deleted);
2913
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
2914
+ }
2915
+ if (changedFiles.length === 0 && deleted.length === 0) {
2916
+ onProgress?.("No actual file changes, updating hashes only");
2917
+ const updatedIndex = this.cloneIndex(existingIndex);
2918
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
2919
+ }
2920
+ onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
2921
+ }
2922
+ }
2923
+ onProgress?.("AI is analyzing codebase structure...");
2924
+ try {
2925
+ const index = await treeSummarizer(treeString);
2926
+ return this.applyIndexMetadata(index, currentHashes, newTreeHash);
2927
+ } catch (error) {
2928
+ throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : String(error)}`);
2929
+ }
2930
+ }
2931
+ async getFileTree() {
2932
+ const gitmodulesPath = import_node_path7.join(this.projectPath, ".gitmodules");
2933
+ const submoduleIgnores = [];
2934
+ if (import_node_fs5.existsSync(gitmodulesPath)) {
2935
+ try {
2936
+ const content = import_node_fs5.readFileSync(gitmodulesPath, "utf-8");
2937
+ const lines = content.split(`
2938
+ `);
2939
+ for (const line of lines) {
2940
+ const match = line.match(/^\s*path\s*=\s*(.*)$/);
2941
+ const path = match?.[1]?.trim();
2942
+ if (path) {
2943
+ submoduleIgnores.push(`${path}/**`);
2944
+ submoduleIgnores.push(`**/${path}/**`);
2945
+ }
2946
+ }
2947
+ } catch {}
2948
+ }
2949
+ return import_globby.globby(["**/*"], {
2950
+ cwd: this.projectPath,
2951
+ gitignore: true,
2952
+ ignore: [
2953
+ ...submoduleIgnores,
2954
+ "**/node_modules/**",
2955
+ "**/dist/**",
2956
+ "**/build/**",
2957
+ "**/target/**",
2958
+ "**/bin/**",
2959
+ "**/obj/**",
2960
+ "**/.next/**",
2961
+ "**/.svelte-kit/**",
2962
+ "**/.nuxt/**",
2963
+ "**/.cache/**",
2964
+ "**/out/**",
2965
+ "**/__tests__/**",
2966
+ "**/coverage/**",
2967
+ "**/*.test.*",
2968
+ "**/*.spec.*",
2969
+ "**/*.d.ts",
2970
+ "**/tsconfig.tsbuildinfo",
2971
+ "**/.locus/*.json",
2972
+ "**/.locus/*.md",
2973
+ "**/.locus/!(artifacts)/**",
2974
+ "**/.git/**",
2975
+ "**/.svn/**",
2976
+ "**/.hg/**",
2977
+ "**/.vscode/**",
2978
+ "**/.idea/**",
2979
+ "**/.DS_Store",
2980
+ "**/bun.lock",
2981
+ "**/package-lock.json",
2982
+ "**/yarn.lock",
2983
+ "**/pnpm-lock.yaml",
2984
+ "**/Cargo.lock",
2985
+ "**/go.sum",
2986
+ "**/poetry.lock",
2987
+ "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
2988
+ ]
2989
+ });
2990
+ }
2991
+ loadIndex() {
2992
+ if (import_node_fs5.existsSync(this.indexPath)) {
2993
+ try {
2994
+ return JSON.parse(import_node_fs5.readFileSync(this.indexPath, "utf-8"));
2995
+ } catch {
2996
+ return null;
2997
+ }
2998
+ }
2999
+ return null;
3000
+ }
3001
+ saveIndex(index) {
3002
+ const dir = import_node_path7.dirname(this.indexPath);
3003
+ if (!import_node_fs5.existsSync(dir)) {
3004
+ import_node_fs5.mkdirSync(dir, { recursive: true });
3005
+ }
3006
+ import_node_fs5.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
3007
+ }
3008
+ cloneIndex(index) {
3009
+ return JSON.parse(JSON.stringify(index));
3010
+ }
3011
+ applyIndexMetadata(index, fileHashes, treeHash) {
3012
+ index.lastIndexed = new Date().toISOString();
3013
+ index.treeHash = treeHash;
3014
+ index.fileHashes = fileHashes;
3015
+ return index;
3016
+ }
3017
+ hashTree(tree) {
3018
+ return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
3019
+ }
3020
+ hashFile(filePath) {
3021
+ try {
3022
+ const content = import_node_fs5.readFileSync(import_node_path7.join(this.projectPath, filePath), "utf-8");
3023
+ return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
3024
+ } catch {
3025
+ return null;
3026
+ }
3027
+ }
3028
+ computeFileHashes(files) {
3029
+ const hashes = {};
3030
+ for (const file of files) {
3031
+ const hash = this.hashFile(file);
3032
+ if (hash !== null) {
3033
+ hashes[file] = hash;
3034
+ }
3035
+ }
3036
+ return hashes;
3037
+ }
3038
+ diffFiles(currentHashes, existingHashes) {
3039
+ const currentFiles = Object.keys(currentHashes);
3040
+ const existingFiles = Object.keys(existingHashes);
3041
+ const existingSet = new Set(existingFiles);
3042
+ const currentSet = new Set(currentFiles);
3043
+ const added = currentFiles.filter((f) => !existingSet.has(f));
3044
+ const deleted = existingFiles.filter((f) => !currentSet.has(f));
3045
+ const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
3046
+ return { added, deleted, modified };
3047
+ }
3048
+ removeFilesFromIndex(index, files) {
3049
+ const fileSet = new Set(files);
3050
+ for (const file of files) {
3051
+ delete index.responsibilities[file];
3052
+ }
3053
+ for (const [symbol, paths] of Object.entries(index.symbols)) {
3054
+ index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
3055
+ if (index.symbols[symbol].length === 0) {
3056
+ delete index.symbols[symbol];
3057
+ }
3058
+ }
3059
+ }
3060
+ mergeIndex(existing, incremental, newHashes, newTreeHash) {
3061
+ const mergedSymbols = { ...existing.symbols };
3062
+ for (const [symbol, paths] of Object.entries(incremental.symbols)) {
3063
+ if (mergedSymbols[symbol]) {
3064
+ mergedSymbols[symbol] = [
3065
+ ...new Set([...mergedSymbols[symbol], ...paths])
3066
+ ];
3067
+ } else {
3068
+ mergedSymbols[symbol] = paths;
3069
+ }
3070
+ }
3071
+ const merged = {
3072
+ symbols: mergedSymbols,
3073
+ responsibilities: {
3074
+ ...existing.responsibilities,
3075
+ ...incremental.responsibilities
3076
+ },
3077
+ lastIndexed: ""
3078
+ };
3079
+ return this.applyIndexMetadata(merged, newHashes, newTreeHash);
3080
+ }
3081
+ }
3082
+
3083
+ // src/agent/codebase-indexer-service.ts
3084
+ class CodebaseIndexerService {
3085
+ deps;
3086
+ indexer;
3087
+ constructor(deps) {
3088
+ this.deps = deps;
3089
+ this.indexer = new CodebaseIndexer(deps.projectPath);
3090
+ }
3091
+ async reindex(force = false) {
3092
+ try {
3093
+ const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
3094
+ const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
3095
+ 1. Key symbols (classes, functions, types) and their locations
3096
+ 2. Responsibilities of each directory/file
3097
+ 3. Overall project structure
3098
+
3099
+ Analyze this file tree and provide a JSON response with:
3100
+ - "symbols": object mapping symbol names to file paths (array)
3101
+ - "responsibilities": object mapping paths to brief descriptions
3102
+
3103
+ File tree:
3104
+ ${tree}
3105
+
3106
+ Return ONLY valid JSON, no markdown formatting.`;
3107
+ const response = await this.deps.aiRunner.run(prompt);
3108
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
3109
+ if (jsonMatch) {
3110
+ return JSON.parse(jsonMatch[0]);
3111
+ }
3112
+ return { symbols: {}, responsibilities: {}, lastIndexed: "" };
3113
+ }, force);
3114
+ if (index === null) {
3115
+ this.deps.log("No changes detected, skipping reindex", "info");
3116
+ return;
3117
+ }
3118
+ this.indexer.saveIndex(index);
3119
+ this.deps.log("Codebase reindexed successfully", "success");
3120
+ } catch (error) {
3121
+ this.deps.log(`Failed to reindex codebase: ${error}`, "error");
3122
+ }
3123
+ }
3124
+ }
3125
+ // src/agent/document-fetcher.ts
3126
+ var import_node_fs6 = require("node:fs");
3127
+ var import_node_path8 = require("node:path");
3128
+ class DocumentFetcher {
3129
+ deps;
3130
+ constructor(deps) {
3131
+ this.deps = deps;
3132
+ }
3133
+ async fetch() {
3134
+ const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
3135
+ if (!import_node_fs6.existsSync(documentsDir)) {
3136
+ import_node_fs6.mkdirSync(documentsDir, { recursive: true });
3137
+ }
3138
+ try {
3139
+ const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
3140
+ const groupMap = new Map(groups.map((g) => [g.id, g.name]));
3141
+ const docs2 = await this.deps.client.docs.list(this.deps.workspaceId);
3142
+ const artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
3143
+ let fetchedCount = 0;
3144
+ for (const doc of docs2) {
3145
+ if (doc.groupId === artifactsGroupId) {
3146
+ continue;
3147
+ }
3148
+ const groupName = groupMap.get(doc.groupId || "") || "General";
3149
+ const groupDir = import_node_path8.join(documentsDir, groupName);
3150
+ if (!import_node_fs6.existsSync(groupDir)) {
3151
+ import_node_fs6.mkdirSync(groupDir, { recursive: true });
3152
+ }
3153
+ const fileName = `${doc.title}.md`;
3154
+ const filePath = import_node_path8.join(groupDir, fileName);
3155
+ if (!import_node_fs6.existsSync(filePath) || import_node_fs6.readFileSync(filePath, "utf-8") !== doc.content) {
3156
+ import_node_fs6.writeFileSync(filePath, doc.content || "");
3157
+ fetchedCount++;
3158
+ }
3159
+ }
3160
+ if (fetchedCount > 0) {
3161
+ this.deps.log(`Fetched ${fetchedCount} document(s) from server`, "info");
3162
+ }
3163
+ } catch (error) {
3164
+ this.deps.log(`Failed to fetch documents: ${error}`, "error");
3165
+ throw error;
3166
+ }
3167
+ }
3168
+ }
3169
+ // src/agent/review-service.ts
3170
+ var import_node_child_process6 = require("node:child_process");
3171
+
3172
+ class ReviewService {
3173
+ deps;
3174
+ constructor(deps) {
3175
+ this.deps = deps;
3176
+ }
3177
+ async reviewStagedChanges(sprint) {
3178
+ const { projectPath, log } = this.deps;
3179
+ try {
3180
+ import_node_child_process6.execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
3181
+ log("Staged all changes for review.", "info");
3182
+ } catch (err) {
3183
+ log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
3184
+ return null;
3185
+ }
3186
+ let diff;
3187
+ try {
3188
+ diff = import_node_child_process6.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
3189
+ cwd: projectPath,
3190
+ maxBuffer: 10 * 1024 * 1024
3191
+ }).toString();
3192
+ } catch (err) {
3193
+ log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
3194
+ return null;
3195
+ }
3196
+ if (!diff.trim()) {
3197
+ return null;
3198
+ }
3199
+ const sprintInfo = sprint ? `Sprint: ${sprint.name} (${sprint.id})` : "No active sprint";
3200
+ const reviewPrompt = `# Code Review Request
3201
+
3202
+ ## Context
3203
+ ${sprintInfo}
3204
+ Date: ${new Date().toISOString()}
3205
+
3206
+ ## Staged Changes (git diff)
3207
+ \`\`\`diff
3208
+ ${diff}
3209
+ \`\`\`
3210
+
3211
+ ## Instructions
3212
+ You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
3213
+
3214
+ 1. **Summary** — Brief overview of what changed and why.
3215
+ 2. **Files Changed** — List each file with a short description of changes.
3216
+ 3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
3217
+ 4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
3218
+ 5. **Recommendations** — Actionable suggestions for improvement.
3219
+ 6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
3220
+
3221
+ Keep the review concise but thorough. Focus on substance over style.`;
3222
+ log("Running AI review on staged changes...", "info");
3223
+ const report = await this.deps.aiRunner.run(reviewPrompt);
3224
+ return report;
3225
+ }
3226
+ }
3227
+ // src/agent/reviewer-worker.ts
3228
+ function resolveProvider2(value) {
3229
+ if (!value || value.startsWith("--"))
3230
+ return PROVIDER.CLAUDE;
3231
+ if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
3232
+ return value;
3233
+ return PROVIDER.CLAUDE;
3234
+ }
3235
+
3236
+ class ReviewerWorker {
3237
+ config;
3238
+ client;
3239
+ aiRunner;
3240
+ prService;
3241
+ knowledgeBase;
3242
+ heartbeatInterval = null;
3243
+ currentTaskId = null;
3244
+ maxReviews = 50;
3245
+ reviewsCompleted = 0;
3246
+ constructor(config) {
3247
+ this.config = config;
3248
+ const projectPath = config.projectPath || process.cwd();
3249
+ this.client = new LocusClient({
3250
+ baseUrl: config.apiBase,
3251
+ token: config.apiKey,
3252
+ retryOptions: {
3253
+ maxRetries: 3,
3254
+ initialDelay: 1000,
3255
+ maxDelay: 5000,
3256
+ factor: 2
3257
+ }
3258
+ });
3259
+ const log = this.log.bind(this);
3260
+ const provider = config.provider ?? PROVIDER.CLAUDE;
3261
+ this.aiRunner = createAiRunner(provider, {
3262
+ projectPath,
3263
+ model: config.model,
3264
+ log
3265
+ });
3266
+ this.prService = new PrService(projectPath, log);
3267
+ this.knowledgeBase = new KnowledgeBase(projectPath);
3268
+ const providerLabel = provider === "codex" ? "Codex" : "Claude";
3269
+ this.log(`Reviewer agent using ${providerLabel} CLI`, "info");
3270
+ }
3271
+ log(message, level = "info") {
3272
+ const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
3273
+ const colorFn = {
3274
+ info: c.cyan,
3275
+ success: c.green,
3276
+ warn: c.yellow,
3277
+ error: c.red
3278
+ }[level];
3279
+ const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
3280
+ console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[R:${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
3281
+ }
3282
+ getNextUnreviewedPr() {
3283
+ const prs = this.prService.listUnreviewedLocusPrs();
3284
+ return prs.length > 0 ? prs[0] : null;
3285
+ }
3286
+ async reviewPr(pr) {
3287
+ const prNumber = String(pr.number);
3288
+ this.log(`Reviewing PR #${prNumber}: ${pr.title}`, "info");
3289
+ let diff;
3290
+ try {
3291
+ diff = this.prService.getPrDiff(prNumber);
3292
+ } catch (err) {
3293
+ return {
3294
+ reviewed: false,
3295
+ approved: false,
3296
+ summary: `Failed to get PR diff: ${err instanceof Error ? err.message : String(err)}`
3297
+ };
3298
+ }
3299
+ if (!diff.trim()) {
3300
+ return {
3301
+ reviewed: true,
3302
+ approved: true,
3303
+ summary: "PR has no changes (empty diff)"
3304
+ };
3305
+ }
3306
+ const reviewPrompt = `# Code Review Request
3307
+
3308
+ ## PR: ${pr.title}
3309
+
3310
+ ## PR Diff
3311
+ \`\`\`diff
3312
+ ${diff.slice(0, 1e5)}
3313
+ \`\`\`
3314
+
3315
+ ## Instructions
3316
+ You are a code reviewer. Review the PR diff above for:
3317
+
3318
+ 1. **Correctness** — Does the code do what the PR title suggests?
3319
+ 2. **Code Quality** — Naming, structure, complexity, readability.
3320
+ 3. **Potential Issues** — Bugs, security issues, edge cases, regressions.
3321
+
3322
+ Output your review in this exact format:
3323
+
3324
+ VERDICT: APPROVE or REQUEST_CHANGES
3325
+
3326
+ Then provide a concise review with specific findings. Keep it actionable and focused.`;
3327
+ const output = await this.aiRunner.run(reviewPrompt);
3328
+ const approved = output.includes("VERDICT: APPROVE");
3329
+ const summary = output.replace(/VERDICT:\s*(APPROVE|REQUEST_CHANGES)\n?/, "").trim();
3330
+ try {
3331
+ const event = approved ? "APPROVE" : "REQUEST_CHANGES";
3332
+ const reviewBody = `## Locus Agent Review
3333
+
3334
+ ${summary}`;
3335
+ this.prService.submitReview(prNumber, reviewBody, event);
3336
+ this.log(`Review posted on PR #${prNumber}: ${approved ? "APPROVED" : "CHANGES REQUESTED"}`, approved ? "success" : "warn");
3337
+ } catch (err) {
3338
+ this.log(`Failed to post PR review: ${err instanceof Error ? err.message : String(err)}`, "error");
3339
+ }
3340
+ return { reviewed: true, approved, summary };
3341
+ }
3342
+ startHeartbeat() {
3343
+ this.sendHeartbeat();
3344
+ this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 60000);
3345
+ }
3346
+ stopHeartbeat() {
3347
+ if (this.heartbeatInterval) {
3348
+ clearInterval(this.heartbeatInterval);
3349
+ this.heartbeatInterval = null;
3350
+ }
3351
+ }
3352
+ sendHeartbeat() {
3353
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
3354
+ this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
3355
+ });
3356
+ }
3357
+ async run() {
3358
+ this.log(`Reviewer agent started in ${this.config.projectPath || process.cwd()}`, "success");
3359
+ if (!isGhAvailable(this.config.projectPath)) {
3360
+ this.log("GitHub CLI (gh) not available — reviewer agent cannot operate", "error");
3361
+ process.exit(1);
2065
3362
  }
2066
- while (this.tasksCompleted < this.maxTasks && this.consecutiveEmpty < this.maxEmpty) {
2067
- const task = await this.getNextTask();
2068
- 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));
3363
+ const handleShutdown = () => {
3364
+ this.log("Received shutdown signal. Aborting...", "warn");
3365
+ this.aiRunner.abort();
3366
+ this.stopHeartbeat();
3367
+ process.exit(1);
3368
+ };
3369
+ process.on("SIGTERM", handleShutdown);
3370
+ process.on("SIGINT", handleShutdown);
3371
+ this.startHeartbeat();
3372
+ while (this.reviewsCompleted < this.maxReviews) {
3373
+ const pr = this.getNextUnreviewedPr();
3374
+ if (!pr) {
3375
+ this.log("No unreviewed PRs found. Waiting 30s...", "info");
3376
+ await new Promise((r) => setTimeout(r, 30000));
2076
3377
  continue;
2077
3378
  }
2078
- this.consecutiveEmpty = 0;
2079
- this.log(`Claimed: ${task.title}`, "success");
2080
- 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
- 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++;
3379
+ this.log(`Reviewing: ${pr.title} (PR #${pr.number})`, "success");
3380
+ this.sendHeartbeat();
3381
+ const result = await this.reviewPr(pr);
3382
+ if (result.reviewed) {
3383
+ const status = result.approved ? "APPROVED" : "CHANGES REQUESTED";
3384
+ try {
3385
+ this.knowledgeBase.updateProgress({
3386
+ type: "pr_reviewed",
3387
+ title: pr.title,
3388
+ details: `Review: ${status}`
3389
+ });
3390
+ } catch {}
3391
+ this.reviewsCompleted++;
2096
3392
  } else {
2097
- this.log(`Failed: ${task.title} - ${result.summary}`, "error");
2098
- await this.client.tasks.update(task.id, this.config.workspaceId, {
2099
- status: "BACKLOG",
2100
- assignedTo: null
2101
- });
2102
- await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2103
- author: this.config.agentId,
2104
- text: `❌ ${result.summary}`
2105
- });
3393
+ this.log(`Review skipped: ${result.summary}`, "warn");
2106
3394
  }
3395
+ this.currentTaskId = null;
2107
3396
  }
3397
+ this.stopHeartbeat();
3398
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
2108
3399
  process.exit(0);
2109
3400
  }
2110
3401
  }
2111
- if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
2112
- process.title = "locus-worker";
3402
+ var reviewerEntrypoint = process.argv[1]?.split(/[\\/]/).pop();
3403
+ if (reviewerEntrypoint === "reviewer-worker.js" || reviewerEntrypoint === "reviewer-worker.ts") {
3404
+ process.title = "locus-reviewer";
2113
3405
  const args = process.argv.slice(2);
2114
3406
  const config = {};
2115
3407
  for (let i = 0;i < args.length; i++) {
@@ -2132,59 +3424,19 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
2132
3424
  const value = args[i + 1];
2133
3425
  if (value && !value.startsWith("--"))
2134
3426
  i++;
2135
- config.provider = resolveProvider(value);
3427
+ config.provider = resolveProvider2(value);
2136
3428
  }
2137
3429
  }
2138
3430
  if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
2139
3431
  console.error("Missing required arguments");
2140
3432
  process.exit(1);
2141
3433
  }
2142
- const worker = new AgentWorker(config);
3434
+ const worker = new ReviewerWorker(config);
2143
3435
  worker.run().catch((err) => {
2144
- console.error("Fatal worker error:", err);
3436
+ console.error("Fatal reviewer error:", err);
2145
3437
  process.exit(1);
2146
3438
  });
2147
3439
  }
2148
-
2149
- // src/index-node.ts
2150
- var exports_index_node = {};
2151
- __export(exports_index_node, {
2152
- getLocusPath: () => getLocusPath,
2153
- getAgentArtifactsPath: () => getAgentArtifactsPath,
2154
- createAiRunner: () => createAiRunner,
2155
- c: () => c,
2156
- WorkspacesModule: () => WorkspacesModule,
2157
- TasksModule: () => TasksModule,
2158
- TaskExecutor: () => TaskExecutor,
2159
- SprintsModule: () => SprintsModule,
2160
- PromptBuilder: () => PromptBuilder,
2161
- PROVIDER: () => PROVIDER,
2162
- OrganizationsModule: () => OrganizationsModule,
2163
- LocusEvent: () => LocusEvent,
2164
- LocusEmitter: () => LocusEmitter,
2165
- LocusClient: () => LocusClient,
2166
- LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
2167
- LOCUS_CONFIG: () => LOCUS_CONFIG,
2168
- InvitationsModule: () => InvitationsModule,
2169
- HistoryManager: () => HistoryManager,
2170
- ExecSession: () => ExecSession,
2171
- ExecEventType: () => ExecEventType,
2172
- ExecEventEmitter: () => ExecEventEmitter,
2173
- DocumentFetcher: () => DocumentFetcher,
2174
- DocsModule: () => DocsModule,
2175
- DEFAULT_MODEL: () => DEFAULT_MODEL,
2176
- ContextTracker: () => ContextTracker,
2177
- CodexRunner: () => CodexRunner,
2178
- CodebaseIndexerService: () => CodebaseIndexerService,
2179
- CodebaseIndexer: () => CodebaseIndexer,
2180
- ClaudeRunner: () => ClaudeRunner,
2181
- CiModule: () => CiModule,
2182
- AuthModule: () => AuthModule,
2183
- AgentWorker: () => AgentWorker,
2184
- AgentOrchestrator: () => AgentOrchestrator,
2185
- AIModule: () => AIModule
2186
- });
2187
- module.exports = __toCommonJS(exports_index_node);
2188
3440
  // src/exec/context-tracker.ts
2189
3441
  var REFERENCE_ALIASES = {
2190
3442
  plan: ["the plan", "sprint plan", "project plan", "implementation plan"],
@@ -2598,8 +3850,8 @@ class ExecEventEmitter {
2598
3850
  }
2599
3851
  }
2600
3852
  // src/exec/history-manager.ts
2601
- var import_node_fs5 = require("node:fs");
2602
- var import_node_path7 = require("node:path");
3853
+ var import_node_fs7 = require("node:fs");
3854
+ var import_node_path9 = require("node:path");
2603
3855
  var DEFAULT_MAX_SESSIONS = 30;
2604
3856
  function generateSessionId2() {
2605
3857
  const timestamp = Date.now().toString(36);
@@ -2611,30 +3863,30 @@ class HistoryManager {
2611
3863
  historyDir;
2612
3864
  maxSessions;
2613
3865
  constructor(projectPath, options) {
2614
- this.historyDir = options?.historyDir ?? import_node_path7.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
3866
+ this.historyDir = options?.historyDir ?? import_node_path9.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
2615
3867
  this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
2616
3868
  this.ensureHistoryDir();
2617
3869
  }
2618
3870
  ensureHistoryDir() {
2619
- if (!import_node_fs5.existsSync(this.historyDir)) {
2620
- import_node_fs5.mkdirSync(this.historyDir, { recursive: true });
3871
+ if (!import_node_fs7.existsSync(this.historyDir)) {
3872
+ import_node_fs7.mkdirSync(this.historyDir, { recursive: true });
2621
3873
  }
2622
3874
  }
2623
3875
  getSessionPath(sessionId) {
2624
- return import_node_path7.join(this.historyDir, `${sessionId}.json`);
3876
+ return import_node_path9.join(this.historyDir, `${sessionId}.json`);
2625
3877
  }
2626
3878
  saveSession(session) {
2627
3879
  const filePath = this.getSessionPath(session.id);
2628
3880
  session.updatedAt = Date.now();
2629
- import_node_fs5.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
3881
+ import_node_fs7.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
2630
3882
  }
2631
3883
  loadSession(sessionId) {
2632
3884
  const filePath = this.getSessionPath(sessionId);
2633
- if (!import_node_fs5.existsSync(filePath)) {
3885
+ if (!import_node_fs7.existsSync(filePath)) {
2634
3886
  return null;
2635
3887
  }
2636
3888
  try {
2637
- const content = import_node_fs5.readFileSync(filePath, "utf-8");
3889
+ const content = import_node_fs7.readFileSync(filePath, "utf-8");
2638
3890
  return JSON.parse(content);
2639
3891
  } catch {
2640
3892
  return null;
@@ -2642,18 +3894,18 @@ class HistoryManager {
2642
3894
  }
2643
3895
  deleteSession(sessionId) {
2644
3896
  const filePath = this.getSessionPath(sessionId);
2645
- if (!import_node_fs5.existsSync(filePath)) {
3897
+ if (!import_node_fs7.existsSync(filePath)) {
2646
3898
  return false;
2647
3899
  }
2648
3900
  try {
2649
- import_node_fs5.rmSync(filePath);
3901
+ import_node_fs7.rmSync(filePath);
2650
3902
  return true;
2651
3903
  } catch {
2652
3904
  return false;
2653
3905
  }
2654
3906
  }
2655
3907
  listSessions(options) {
2656
- const files = import_node_fs5.readdirSync(this.historyDir);
3908
+ const files = import_node_fs7.readdirSync(this.historyDir);
2657
3909
  let sessions = [];
2658
3910
  for (const file of files) {
2659
3911
  if (file.endsWith(".json")) {
@@ -2726,11 +3978,11 @@ class HistoryManager {
2726
3978
  return deleted;
2727
3979
  }
2728
3980
  getSessionCount() {
2729
- const files = import_node_fs5.readdirSync(this.historyDir);
3981
+ const files = import_node_fs7.readdirSync(this.historyDir);
2730
3982
  return files.filter((f) => f.endsWith(".json")).length;
2731
3983
  }
2732
3984
  sessionExists(sessionId) {
2733
- return import_node_fs5.existsSync(this.getSessionPath(sessionId));
3985
+ return import_node_fs7.existsSync(this.getSessionPath(sessionId));
2734
3986
  }
2735
3987
  findSessionByPartialId(partialId) {
2736
3988
  const sessions = this.listSessions();
@@ -2744,12 +3996,12 @@ class HistoryManager {
2744
3996
  return this.historyDir;
2745
3997
  }
2746
3998
  clearAllSessions() {
2747
- const files = import_node_fs5.readdirSync(this.historyDir);
3999
+ const files = import_node_fs7.readdirSync(this.historyDir);
2748
4000
  let deleted = 0;
2749
4001
  for (const file of files) {
2750
4002
  if (file.endsWith(".json")) {
2751
4003
  try {
2752
- import_node_fs5.rmSync(import_node_path7.join(this.historyDir, file));
4004
+ import_node_fs7.rmSync(import_node_path9.join(this.historyDir, file));
2753
4005
  deleted++;
2754
4006
  } catch {}
2755
4007
  }
@@ -3014,12 +4266,14 @@ ${currentPrompt}`);
3014
4266
  }
3015
4267
  }
3016
4268
  // src/orchestrator.ts
3017
- var import_node_child_process3 = require("node:child_process");
3018
- var import_node_fs6 = require("node:fs");
3019
- var import_node_path8 = require("node:path");
4269
+ var import_node_child_process7 = require("node:child_process");
4270
+ var import_node_fs8 = require("node:fs");
4271
+ var import_node_path10 = require("node:path");
3020
4272
  var import_node_url = require("node:url");
3021
- var import_shared3 = require("@locusai/shared");
4273
+ var import_shared4 = require("@locusai/shared");
3022
4274
  var import_events4 = require("events");
4275
+ var MAX_AGENTS = 5;
4276
+
3023
4277
  class AgentOrchestrator extends import_events4.EventEmitter {
3024
4278
  client;
3025
4279
  config;
@@ -3027,6 +4281,8 @@ class AgentOrchestrator extends import_events4.EventEmitter {
3027
4281
  isRunning = false;
3028
4282
  processedTasks = new Set;
3029
4283
  resolvedSprintId = null;
4284
+ worktreeManager = null;
4285
+ heartbeatInterval = null;
3030
4286
  constructor(config) {
3031
4287
  super();
3032
4288
  this.config = config;
@@ -3035,6 +4291,15 @@ class AgentOrchestrator extends import_events4.EventEmitter {
3035
4291
  token: config.apiKey
3036
4292
  });
3037
4293
  }
4294
+ get agentCount() {
4295
+ return Math.min(Math.max(this.config.agentCount ?? 1, 1), MAX_AGENTS);
4296
+ }
4297
+ get useWorktrees() {
4298
+ return this.config.useWorktrees ?? true;
4299
+ }
4300
+ get worktreeCleanupPolicy() {
4301
+ return this.config.worktreeCleanupPolicy ?? "retain-on-failure";
4302
+ }
3038
4303
  async resolveSprintId() {
3039
4304
  if (this.config.sprintId) {
3040
4305
  return this.config.sprintId;
@@ -3078,6 +4343,12 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
3078
4343
  if (this.resolvedSprintId) {
3079
4344
  console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
3080
4345
  }
4346
+ console.log(`${c.bold("Agents:")} ${this.agentCount}`);
4347
+ console.log(`${c.bold("Worktrees:")} ${this.useWorktrees ? "enabled" : "disabled"}`);
4348
+ if (this.useWorktrees) {
4349
+ console.log(`${c.bold("Cleanup policy:")} ${this.worktreeCleanupPolicy}`);
4350
+ console.log(`${c.bold("Auto-push:")} ${this.config.autoPush ? "enabled" : "disabled"}`);
4351
+ }
3081
4352
  console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
3082
4353
  console.log(c.dim(`----------------------------------------------
3083
4354
  `));
@@ -3086,9 +4357,30 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
3086
4357
  console.log(c.dim("ℹ No available tasks found in the backlog."));
3087
4358
  return;
3088
4359
  }
3089
- await this.spawnAgent();
4360
+ if (tasks2.length > 0 && this.useWorktrees && !isGitAvailable()) {
4361
+ console.log(c.error("git is not installed. Worktree isolation requires git. Install from https://git-scm.com/"));
4362
+ return;
4363
+ }
4364
+ if (tasks2.length > 0 && this.config.autoPush && !isGhAvailable(this.config.projectPath)) {
4365
+ console.log(c.warning("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/"));
4366
+ }
4367
+ if (tasks2.length > 0 && this.useWorktrees) {
4368
+ this.worktreeManager = new WorktreeManager(this.config.projectPath, {
4369
+ cleanupPolicy: this.worktreeCleanupPolicy
4370
+ });
4371
+ }
4372
+ this.startHeartbeatMonitor();
4373
+ const agentsToSpawn = Math.min(this.agentCount, tasks2.length);
4374
+ const SPAWN_DELAY_MS = 5000;
4375
+ const spawnPromises = [];
4376
+ for (let i = 0;i < agentsToSpawn; i++) {
4377
+ if (i > 0) {
4378
+ await this.sleep(SPAWN_DELAY_MS);
4379
+ }
4380
+ spawnPromises.push(this.spawnAgent(i));
4381
+ }
4382
+ await Promise.all(spawnPromises);
3090
4383
  while (this.agents.size > 0 && this.isRunning) {
3091
- await this.reapAgents();
3092
4384
  if (this.agents.size === 0) {
3093
4385
  break;
3094
4386
  }
@@ -3097,8 +4389,8 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
3097
4389
  console.log(`
3098
4390
  ${c.success("✅ Orchestrator finished")}`);
3099
4391
  }
3100
- async spawnAgent() {
3101
- const agentId = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
4392
+ async spawnAgent(index) {
4393
+ const agentId = `agent-${index}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
3102
4394
  const agentState = {
3103
4395
  id: agentId,
3104
4396
  status: "IDLE",
@@ -3110,13 +4402,9 @@ ${c.success("✅ Orchestrator finished")}`);
3110
4402
  this.agents.set(agentId, agentState);
3111
4403
  console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
3112
4404
  `);
3113
- const potentialPaths = [];
3114
- const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
3115
- const currentModuleDir = import_node_path8.dirname(currentModulePath);
3116
- potentialPaths.push(import_node_path8.join(currentModuleDir, "agent", "worker.js"), import_node_path8.join(currentModuleDir, "worker.js"), import_node_path8.join(currentModuleDir, "agent", "worker.ts"));
3117
- const workerPath = potentialPaths.find((p) => import_node_fs6.existsSync(p));
4405
+ const workerPath = this.resolveWorkerPath();
3118
4406
  if (!workerPath) {
3119
- throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
4407
+ throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
3120
4408
  }
3121
4409
  const workerArgs = [
3122
4410
  "--agent-id",
@@ -3139,8 +4427,15 @@ ${c.success("✅ Orchestrator finished")}`);
3139
4427
  if (this.resolvedSprintId) {
3140
4428
  workerArgs.push("--sprint-id", this.resolvedSprintId);
3141
4429
  }
3142
- const agentProcess = import_node_child_process3.spawn(process.execPath, [workerPath, ...workerArgs], {
4430
+ if (this.useWorktrees) {
4431
+ workerArgs.push("--use-worktrees");
4432
+ }
4433
+ if (this.config.autoPush) {
4434
+ workerArgs.push("--auto-push");
4435
+ }
4436
+ const agentProcess = import_node_child_process7.spawn(process.execPath, [workerPath, ...workerArgs], {
3143
4437
  stdio: ["pipe", "pipe", "pipe"],
4438
+ detached: true,
3144
4439
  env: {
3145
4440
  ...process.env,
3146
4441
  FORCE_COLOR: "1",
@@ -3155,6 +4450,9 @@ ${c.success("✅ Orchestrator finished")}`);
3155
4450
  agentState.tasksCompleted = msg.tasksCompleted || 0;
3156
4451
  agentState.tasksFailed = msg.tasksFailed || 0;
3157
4452
  }
4453
+ if (msg.type === "heartbeat") {
4454
+ agentState.lastHeartbeat = new Date;
4455
+ }
3158
4456
  });
3159
4457
  agentProcess.stdout?.on("data", (data) => {
3160
4458
  process.stdout.write(data.toString());
@@ -3179,7 +4477,30 @@ ${agentId} finished (exit code: ${code})`);
3179
4477
  });
3180
4478
  this.emit("agent:spawned", { agentId });
3181
4479
  }
3182
- async reapAgents() {}
4480
+ resolveWorkerPath() {
4481
+ const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
4482
+ const currentModuleDir = import_node_path10.dirname(currentModulePath);
4483
+ const potentialPaths = [
4484
+ import_node_path10.join(currentModuleDir, "agent", "worker.js"),
4485
+ import_node_path10.join(currentModuleDir, "worker.js"),
4486
+ import_node_path10.join(currentModuleDir, "agent", "worker.ts")
4487
+ ];
4488
+ return potentialPaths.find((p) => import_node_fs8.existsSync(p));
4489
+ }
4490
+ startHeartbeatMonitor() {
4491
+ this.heartbeatInterval = setInterval(() => {
4492
+ const now = Date.now();
4493
+ for (const [agentId, agent] of this.agents.entries()) {
4494
+ if (agent.status === "WORKING" && now - agent.lastHeartbeat.getTime() > import_shared4.STALE_AGENT_TIMEOUT_MS) {
4495
+ console.log(c.error(`Agent ${agentId} is stale (no heartbeat for 10 minutes). Killing.`));
4496
+ if (agent.process && !agent.process.killed) {
4497
+ this.killProcessTree(agent.process);
4498
+ }
4499
+ this.emit("agent:stale", { agentId });
4500
+ }
4501
+ }
4502
+ }, 60000);
4503
+ }
3183
4504
  async getAvailableTasks() {
3184
4505
  try {
3185
4506
  const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
@@ -3196,10 +4517,10 @@ ${agentId} finished (exit code: ${code})`);
3196
4517
  try {
3197
4518
  const tasks2 = await this.getAvailableTasks();
3198
4519
  const priorityOrder = [
3199
- import_shared3.TaskPriority.CRITICAL,
3200
- import_shared3.TaskPriority.HIGH,
3201
- import_shared3.TaskPriority.MEDIUM,
3202
- import_shared3.TaskPriority.LOW
4520
+ import_shared4.TaskPriority.CRITICAL,
4521
+ import_shared4.TaskPriority.HIGH,
4522
+ import_shared4.TaskPriority.MEDIUM,
4523
+ import_shared4.TaskPriority.LOW
3203
4524
  ];
3204
4525
  let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
3205
4526
  if (!task && tasks2.length > 0) {
@@ -3223,7 +4544,7 @@ ${agentId} finished (exit code: ${code})`);
3223
4544
  async completeTask(taskId, agentId, summary) {
3224
4545
  try {
3225
4546
  await this.client.tasks.update(taskId, this.config.workspaceId, {
3226
- status: import_shared3.TaskStatus.VERIFICATION
4547
+ status: import_shared4.TaskStatus.IN_REVIEW
3227
4548
  });
3228
4549
  if (summary) {
3229
4550
  await this.client.tasks.addComment(taskId, this.config.workspaceId, {
@@ -3248,7 +4569,7 @@ ${summary}`
3248
4569
  async failTask(taskId, agentId, error) {
3249
4570
  try {
3250
4571
  await this.client.tasks.update(taskId, this.config.workspaceId, {
3251
- status: import_shared3.TaskStatus.BACKLOG,
4572
+ status: import_shared4.TaskStatus.BACKLOG,
3252
4573
  assignedTo: null
3253
4574
  });
3254
4575
  await this.client.tasks.addComment(taskId, this.config.workspaceId, {
@@ -3271,11 +4592,52 @@ ${summary}`
3271
4592
  await this.cleanup();
3272
4593
  this.emit("stopped", { timestamp: new Date });
3273
4594
  }
4595
+ stopAgent(agentId) {
4596
+ const agent = this.agents.get(agentId);
4597
+ if (!agent)
4598
+ return false;
4599
+ if (agent.process && !agent.process.killed) {
4600
+ this.killProcessTree(agent.process);
4601
+ }
4602
+ return true;
4603
+ }
4604
+ killProcessTree(proc) {
4605
+ if (!proc.pid || proc.killed)
4606
+ return;
4607
+ try {
4608
+ process.kill(-proc.pid, "SIGTERM");
4609
+ } catch {
4610
+ try {
4611
+ proc.kill("SIGTERM");
4612
+ } catch {}
4613
+ }
4614
+ }
3274
4615
  async cleanup() {
4616
+ if (this.heartbeatInterval) {
4617
+ clearInterval(this.heartbeatInterval);
4618
+ this.heartbeatInterval = null;
4619
+ }
3275
4620
  for (const [agentId, agent] of this.agents.entries()) {
3276
4621
  if (agent.process && !agent.process.killed) {
3277
4622
  console.log(`Killing agent: ${agentId}`);
3278
- agent.process.kill();
4623
+ this.killProcessTree(agent.process);
4624
+ }
4625
+ }
4626
+ if (this.worktreeManager) {
4627
+ try {
4628
+ if (this.worktreeCleanupPolicy === "auto") {
4629
+ const removed = this.worktreeManager.removeAll();
4630
+ if (removed > 0) {
4631
+ console.log(c.dim(`Cleaned up ${removed} worktree(s)`));
4632
+ }
4633
+ } else if (this.worktreeCleanupPolicy === "retain-on-failure") {
4634
+ this.worktreeManager.prune();
4635
+ console.log(c.dim("Retaining worktrees for failure analysis (cleanup policy: retain-on-failure)"));
4636
+ } else {
4637
+ console.log(c.dim("Skipping worktree cleanup (cleanup policy: manual)"));
4638
+ }
4639
+ } catch {
4640
+ console.log(c.dim("Could not clean up some worktrees"));
3279
4641
  }
3280
4642
  }
3281
4643
  this.agents.clear();
@@ -3283,12 +4645,543 @@ ${summary}`
3283
4645
  getStats() {
3284
4646
  return {
3285
4647
  activeAgents: this.agents.size,
4648
+ agentCount: this.agentCount,
4649
+ useWorktrees: this.useWorktrees,
3286
4650
  processedTasks: this.processedTasks.size,
3287
4651
  totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksCompleted, 0),
3288
4652
  totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksFailed, 0)
3289
4653
  };
3290
4654
  }
4655
+ getAgentStates() {
4656
+ return Array.from(this.agents.values());
4657
+ }
3291
4658
  sleep(ms) {
3292
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4659
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4660
+ }
4661
+ }
4662
+ // src/planning/plan-manager.ts
4663
+ var import_node_fs9 = require("node:fs");
4664
+ var import_node_path11 = require("node:path");
4665
+
4666
+ // src/planning/sprint-plan.ts
4667
+ var import_shared5 = require("@locusai/shared");
4668
+ function sprintPlanToMarkdown(plan) {
4669
+ const lines = [];
4670
+ lines.push(`# Sprint Plan: ${plan.name}`);
4671
+ lines.push("");
4672
+ lines.push(`**Status:** ${plan.status.toUpperCase()}`);
4673
+ lines.push(`**Created:** ${plan.createdAt}`);
4674
+ lines.push(`**Estimated Duration:** ${plan.estimatedDays} day(s)`);
4675
+ lines.push("");
4676
+ lines.push(`## Goal`);
4677
+ lines.push(plan.goal);
4678
+ lines.push("");
4679
+ lines.push(`## CEO Directive`);
4680
+ lines.push(`> ${plan.directive}`);
4681
+ lines.push("");
4682
+ if (plan.feedback) {
4683
+ lines.push(`## CEO Feedback`);
4684
+ lines.push(`> ${plan.feedback}`);
4685
+ lines.push("");
4686
+ }
4687
+ lines.push(`## Tasks (${plan.tasks.length})`);
4688
+ lines.push("");
4689
+ for (const task of plan.tasks) {
4690
+ lines.push(`### ${task.index}. ${task.title}`);
4691
+ lines.push(`- **Role:** ${task.assigneeRole}`);
4692
+ lines.push(`- **Priority:** ${task.priority}`);
4693
+ lines.push(`- **Complexity:** ${"█".repeat(task.complexity)}${"░".repeat(5 - task.complexity)} (${task.complexity}/5)`);
4694
+ if (task.labels.length > 0) {
4695
+ lines.push(`- **Labels:** ${task.labels.join(", ")}`);
4696
+ }
4697
+ lines.push("");
4698
+ lines.push(task.description);
4699
+ lines.push("");
4700
+ if (task.acceptanceCriteria.length > 0) {
4701
+ lines.push(`**Acceptance Criteria:**`);
4702
+ for (const ac of task.acceptanceCriteria) {
4703
+ lines.push(`- [ ] ${ac}`);
4704
+ }
4705
+ lines.push("");
4706
+ }
4707
+ }
4708
+ if (plan.risks.length > 0) {
4709
+ lines.push(`## Risks`);
4710
+ lines.push("");
4711
+ for (const risk of plan.risks) {
4712
+ lines.push(`- **[${risk.severity.toUpperCase()}]** ${risk.description}`);
4713
+ lines.push(` - Mitigation: ${risk.mitigation}`);
4714
+ }
4715
+ lines.push("");
4716
+ }
4717
+ lines.push(`---`);
4718
+ lines.push(`*Plan ID: ${plan.id}*`);
4719
+ return lines.join(`
4720
+ `);
4721
+ }
4722
+ function plannedTasksToCreatePayloads(plan, sprintId) {
4723
+ return plan.tasks.map((task) => ({
4724
+ title: task.title,
4725
+ description: task.description,
4726
+ status: import_shared5.TaskStatus.BACKLOG,
4727
+ assigneeRole: task.assigneeRole,
4728
+ priority: task.priority,
4729
+ labels: task.labels,
4730
+ sprintId,
4731
+ order: task.index * 10,
4732
+ acceptanceChecklist: task.acceptanceCriteria.map((text, i) => ({
4733
+ id: `ac-${i + 1}`,
4734
+ text,
4735
+ done: false
4736
+ }))
4737
+ }));
4738
+ }
4739
+ function parseSprintPlanFromAI(raw, directive) {
4740
+ let jsonStr = raw.trim();
4741
+ const jsonMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
4742
+ if (jsonMatch) {
4743
+ jsonStr = jsonMatch[1]?.trim() || "";
4744
+ }
4745
+ const parsed = JSON.parse(jsonStr);
4746
+ const now = new Date().toISOString();
4747
+ const id = `plan-${Date.now()}`;
4748
+ const tasks2 = (parsed.tasks || []).map((t, i) => ({
4749
+ index: i + 1,
4750
+ title: t.title || `Task ${i + 1}`,
4751
+ description: t.description || "",
4752
+ assigneeRole: t.assigneeRole || "BACKEND",
4753
+ priority: t.priority || "MEDIUM",
4754
+ complexity: t.complexity || 3,
4755
+ acceptanceCriteria: t.acceptanceCriteria || [],
4756
+ labels: t.labels || []
4757
+ }));
4758
+ return {
4759
+ id,
4760
+ name: parsed.name || "Unnamed Sprint",
4761
+ goal: parsed.goal || directive,
4762
+ directive,
4763
+ tasks: tasks2,
4764
+ risks: (parsed.risks || []).map((r) => ({
4765
+ description: r.description || "",
4766
+ mitigation: r.mitigation || "",
4767
+ severity: r.severity || "medium"
4768
+ })),
4769
+ estimatedDays: parsed.estimatedDays || 1,
4770
+ status: "pending",
4771
+ createdAt: now,
4772
+ updatedAt: now
4773
+ };
4774
+ }
4775
+
4776
+ // src/planning/plan-manager.ts
4777
+ class PlanManager {
4778
+ projectPath;
4779
+ plansDir;
4780
+ constructor(projectPath) {
4781
+ this.projectPath = projectPath;
4782
+ this.plansDir = getLocusPath(projectPath, "plansDir");
4783
+ }
4784
+ save(plan) {
4785
+ this.ensurePlansDir();
4786
+ const slug = this.slugify(plan.name);
4787
+ const jsonPath = import_node_path11.join(this.plansDir, `${slug}.json`);
4788
+ const mdPath = import_node_path11.join(this.plansDir, `sprint-${slug}.md`);
4789
+ import_node_fs9.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
4790
+ import_node_fs9.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
4791
+ return plan.id;
4792
+ }
4793
+ load(idOrSlug) {
4794
+ this.ensurePlansDir();
4795
+ const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
4796
+ for (const file of files) {
4797
+ const filePath = import_node_path11.join(this.plansDir, file);
4798
+ try {
4799
+ const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
4800
+ if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
4801
+ return plan;
4802
+ }
4803
+ } catch {}
4804
+ }
4805
+ return null;
4806
+ }
4807
+ list(status) {
4808
+ this.ensurePlansDir();
4809
+ const files = import_node_fs9.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
4810
+ const plans = [];
4811
+ for (const file of files) {
4812
+ try {
4813
+ const plan = JSON.parse(import_node_fs9.readFileSync(import_node_path11.join(this.plansDir, file), "utf-8"));
4814
+ if (!status || plan.status === status) {
4815
+ plans.push(plan);
4816
+ }
4817
+ } catch {}
4818
+ }
4819
+ plans.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
4820
+ return plans;
4821
+ }
4822
+ async approve(idOrSlug, client, workspaceId) {
4823
+ const plan = this.load(idOrSlug);
4824
+ if (!plan) {
4825
+ throw new Error(`Plan not found: ${idOrSlug}`);
4826
+ }
4827
+ if (plan.status !== "pending") {
4828
+ throw new Error(`Plan "${plan.name}" is ${plan.status}, can only approve pending plans`);
4829
+ }
4830
+ const sprint = await client.sprints.create(workspaceId, {
4831
+ name: plan.name
4832
+ });
4833
+ const payloads = plannedTasksToCreatePayloads(plan, sprint.id);
4834
+ const tasks2 = [];
4835
+ for (const payload of payloads) {
4836
+ const task = await client.tasks.create(workspaceId, payload);
4837
+ tasks2.push(task);
4838
+ }
4839
+ await client.sprints.start(sprint.id, workspaceId);
4840
+ plan.status = "approved";
4841
+ plan.updatedAt = new Date().toISOString();
4842
+ this.save(plan);
4843
+ const kb = new KnowledgeBase(this.projectPath);
4844
+ kb.updateProgress({
4845
+ type: "sprint_started",
4846
+ title: plan.name,
4847
+ details: `${tasks2.length} tasks created from planning meeting. Sprint goal: ${plan.goal}`
4848
+ });
4849
+ return { sprint, tasks: tasks2 };
4850
+ }
4851
+ reject(idOrSlug, feedback) {
4852
+ const plan = this.load(idOrSlug);
4853
+ if (!plan) {
4854
+ throw new Error(`Plan not found: ${idOrSlug}`);
4855
+ }
4856
+ if (plan.status !== "pending") {
4857
+ throw new Error(`Plan "${plan.name}" is ${plan.status}, can only reject pending plans`);
4858
+ }
4859
+ plan.status = "rejected";
4860
+ plan.feedback = feedback;
4861
+ plan.updatedAt = new Date().toISOString();
4862
+ this.save(plan);
4863
+ return plan;
4864
+ }
4865
+ cancel(idOrSlug) {
4866
+ const plan = this.load(idOrSlug);
4867
+ if (!plan) {
4868
+ throw new Error(`Plan not found: ${idOrSlug}`);
4869
+ }
4870
+ plan.status = "cancelled";
4871
+ plan.updatedAt = new Date().toISOString();
4872
+ this.save(plan);
4873
+ }
4874
+ delete(idOrSlug) {
4875
+ this.ensurePlansDir();
4876
+ const files = import_node_fs9.readdirSync(this.plansDir);
4877
+ for (const file of files) {
4878
+ const filePath = import_node_path11.join(this.plansDir, file);
4879
+ if (!file.endsWith(".json"))
4880
+ continue;
4881
+ try {
4882
+ const plan = JSON.parse(import_node_fs9.readFileSync(filePath, "utf-8"));
4883
+ if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
4884
+ import_node_fs9.unlinkSync(filePath);
4885
+ const mdPath = import_node_path11.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
4886
+ if (import_node_fs9.existsSync(mdPath)) {
4887
+ import_node_fs9.unlinkSync(mdPath);
4888
+ }
4889
+ return;
4890
+ }
4891
+ } catch {}
4892
+ }
4893
+ }
4894
+ getMarkdown(idOrSlug) {
4895
+ const plan = this.load(idOrSlug);
4896
+ if (!plan)
4897
+ return null;
4898
+ return sprintPlanToMarkdown(plan);
4899
+ }
4900
+ ensurePlansDir() {
4901
+ if (!import_node_fs9.existsSync(this.plansDir)) {
4902
+ import_node_fs9.mkdirSync(this.plansDir, { recursive: true });
4903
+ }
4904
+ }
4905
+ slugify(name) {
4906
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4907
+ }
4908
+ }
4909
+ // src/planning/planning-meeting.ts
4910
+ var import_node_fs10 = require("node:fs");
4911
+
4912
+ // src/planning/agents/architect.ts
4913
+ function buildArchitectPrompt(input) {
4914
+ let prompt = `# Role: Software Architect
4915
+
4916
+ You are a Software Architect participating in an async sprint planning meeting. The Tech Lead has produced an initial task breakdown. Your job is to refine it.
4917
+
4918
+ ## CEO Directive
4919
+ > ${input.directive}
4920
+ `;
4921
+ if (input.feedback) {
4922
+ prompt += `
4923
+ ## CEO Feedback on Previous Plan
4924
+ > ${input.feedback}
4925
+
4926
+ IMPORTANT: Ensure the refined plan addresses this feedback.
4927
+ `;
4928
+ }
4929
+ prompt += `
4930
+ ## Project Context
4931
+ ${input.projectContext || "No project context available."}
4932
+
4933
+ ## Tech Lead's Task Breakdown
4934
+ ${input.techLeadOutput}
4935
+
4936
+ ## Your Task
4937
+
4938
+ Review and refine the Tech Lead's breakdown:
4939
+
4940
+ 1. **Ordering** — Order tasks so that foundational work comes first. Tasks that produce outputs consumed by later tasks must appear earlier in the list. Foundation tasks (schemas, config, shared code) must be listed before tasks that build on them. The array index IS the execution order.
4941
+ 2. **Risk Assessment** — Flag tasks that are risky, underestimated, or have unknowns.
4942
+ 3. **Task Splitting** — If a task is too large (would take more than a day), split it.
4943
+ 4. **Task Merging** — If two tasks are trivially small and related, merge them.
4944
+ 5. **Complexity Scoring** — Rate each task 1-5 (1=trivial, 5=very complex).
4945
+ 6. **Missing Tasks** — Add any tasks the Tech Lead missed (database migrations, configuration, testing, etc.).
4946
+
4947
+ ## Output Format
4948
+
4949
+ Respond with ONLY a JSON object (no markdown code blocks, no explanation):
4950
+
4951
+ {
4952
+ "tasks": [
4953
+ {
4954
+ "title": "string",
4955
+ "description": "string (2-4 sentences)",
4956
+ "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
4957
+ "priority": "HIGH | MEDIUM | LOW | CRITICAL",
4958
+ "labels": ["string"],
4959
+ "acceptanceCriteria": ["string"],
4960
+ "complexity": 3
4961
+ }
4962
+ ],
4963
+ "risks": [
4964
+ {
4965
+ "description": "string",
4966
+ "mitigation": "string",
4967
+ "severity": "low | medium | high"
4968
+ }
4969
+ ],
4970
+ "architectureNotes": "string (notes for the Sprint Organizer about parallelism opportunities and constraints)"
4971
+ }`;
4972
+ return prompt;
4973
+ }
4974
+
4975
+ // src/planning/agents/sprint-organizer.ts
4976
+ function buildSprintOrganizerPrompt(input) {
4977
+ let prompt = `# Role: Sprint Organizer
4978
+
4979
+ You are a Sprint Organizer finalizing the sprint plan from a planning meeting. The Architect has refined the task breakdown. Your job is to produce the final sprint plan document.
4980
+
4981
+ ## CEO Directive
4982
+ > ${input.directive}
4983
+ `;
4984
+ if (input.feedback) {
4985
+ prompt += `
4986
+ ## CEO Feedback on Previous Plan
4987
+ > ${input.feedback}
4988
+
4989
+ IMPORTANT: The final plan must address this feedback.
4990
+ `;
4991
+ }
4992
+ prompt += `
4993
+ ## Architect's Refined Task Breakdown
4994
+ ${input.architectOutput}
4995
+
4996
+ ## Your Task
4997
+
4998
+ Produce the final sprint plan:
4999
+
5000
+ 1. **Sprint Name** — A concise, memorable name for this sprint (e.g., "User Authentication", "Payment Integration")
5001
+ 2. **Sprint Goal** — One paragraph describing what this sprint delivers
5002
+ 3. **Task Ordering** — Final ordering so that foundational work comes first. The position in the array IS the execution order — task at index 0 runs first, index 1 runs second, etc.
5003
+ 4. **Duration Estimate** — How many days this sprint will take with 2-3 agents working in parallel
5004
+ 5. **Final Task List** — Each task with all fields filled in, ordered by execution priority
5005
+
5006
+ Guidelines:
5007
+ - The order of tasks in the array determines execution order. Tasks are dispatched sequentially from first to last.
5008
+ - Foundation tasks (schemas, config, shared code) must appear before tasks that build on them
5009
+ - Group related tasks together when possible
5010
+ - Ensure acceptance criteria are specific and testable
5011
+ - Keep the sprint focused — if it's too large (>12 tasks), consider reducing scope
5012
+
5013
+ ## Output Format
5014
+
5015
+ Respond with ONLY a JSON object (no markdown code blocks, no explanation):
5016
+
5017
+ {
5018
+ "name": "string (2-4 words)",
5019
+ "goal": "string (1 paragraph)",
5020
+ "estimatedDays": 3,
5021
+ "tasks": [
5022
+ {
5023
+ "title": "string",
5024
+ "description": "string",
5025
+ "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
5026
+ "priority": "CRITICAL | HIGH | MEDIUM | LOW",
5027
+ "labels": ["string"],
5028
+ "acceptanceCriteria": ["string"],
5029
+ "complexity": 3
5030
+ }
5031
+ ],
5032
+ "risks": [
5033
+ {
5034
+ "description": "string",
5035
+ "mitigation": "string",
5036
+ "severity": "low | medium | high"
5037
+ }
5038
+ ]
5039
+ }`;
5040
+ return prompt;
5041
+ }
5042
+
5043
+ // src/planning/agents/tech-lead.ts
5044
+ function buildTechLeadPrompt(input) {
5045
+ let prompt = `# Role: Senior Tech Lead
5046
+
5047
+ You are a Senior Tech Lead participating in an async sprint planning meeting. Your job is to take the CEO's directive and produce an initial task breakdown.
5048
+
5049
+ ## CEO Directive
5050
+ > ${input.directive}
5051
+ `;
5052
+ if (input.feedback) {
5053
+ prompt += `
5054
+ ## CEO Feedback on Previous Plan
5055
+ > ${input.feedback}
5056
+
5057
+ IMPORTANT: Incorporate this feedback into your task breakdown. The CEO has reviewed a previous plan and wants changes.
5058
+ `;
5059
+ }
5060
+ prompt += `
5061
+ ## Project Context
5062
+ ${input.projectContext || "No project context available."}
5063
+
5064
+ ## Codebase Structure
5065
+ ${input.codebaseIndex || "No codebase index available."}
5066
+
5067
+ ## Your Task
5068
+
5069
+ Analyze the CEO's directive and produce a detailed task breakdown. For each task:
5070
+
5071
+ 1. **Title** — Clear, action-oriented (e.g., "Implement user registration API endpoint")
5072
+ 2. **Description** — What needs to be done technically, which files/modules are involved
5073
+ 3. **Assignee Role** — Who should work on this: BACKEND, FRONTEND, QA, PM, or DESIGN
5074
+ 4. **Priority** — HIGH, MEDIUM, or LOW based on business impact
5075
+ 5. **Labels** — Relevant tags (e.g., "api", "database", "ui", "auth")
5076
+ 6. **Acceptance Criteria** — Specific, testable conditions for completion
5077
+
5078
+ Think about:
5079
+ - What existing code can be reused or extended
5080
+ - Which tasks are independent vs. dependent
5081
+ - What the right granularity is (not too big, not too small)
5082
+ - What risks or unknowns exist
5083
+
5084
+ ## Output Format
5085
+
5086
+ Respond with ONLY a JSON object (no markdown code blocks, no explanation):
5087
+
5088
+ {
5089
+ "tasks": [
5090
+ {
5091
+ "title": "string",
5092
+ "description": "string (2-4 sentences)",
5093
+ "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
5094
+ "priority": "HIGH | MEDIUM | LOW",
5095
+ "labels": ["string"],
5096
+ "acceptanceCriteria": ["string"]
5097
+ }
5098
+ ],
5099
+ "technicalNotes": "string (brief notes on architecture decisions, risks, or considerations for the Architect phase)"
5100
+ }`;
5101
+ return prompt;
5102
+ }
5103
+
5104
+ // src/planning/planning-meeting.ts
5105
+ class PlanningMeeting {
5106
+ projectPath;
5107
+ aiRunner;
5108
+ log;
5109
+ constructor(config) {
5110
+ this.projectPath = config.projectPath;
5111
+ this.aiRunner = config.aiRunner;
5112
+ this.log = config.log ?? ((_msg) => {
5113
+ return;
5114
+ });
5115
+ }
5116
+ async run(directive, feedback) {
5117
+ const projectContext = this.getProjectContext();
5118
+ const codebaseIndex = this.getCodebaseIndex();
5119
+ this.log("Phase 1/3: Tech Lead analyzing directive...", "info");
5120
+ const techLeadPrompt = buildTechLeadPrompt({
5121
+ directive,
5122
+ projectContext,
5123
+ codebaseIndex,
5124
+ feedback
5125
+ });
5126
+ const techLeadOutput = await this.aiRunner.run(techLeadPrompt);
5127
+ this.log("Tech Lead phase complete.", "success");
5128
+ this.log("Phase 2/3: Architect refining task breakdown...", "info");
5129
+ const architectPrompt = buildArchitectPrompt({
5130
+ directive,
5131
+ projectContext,
5132
+ techLeadOutput,
5133
+ feedback
5134
+ });
5135
+ const architectOutput = await this.aiRunner.run(architectPrompt);
5136
+ this.log("Architect phase complete.", "success");
5137
+ this.log("Phase 3/3: Sprint Organizer finalizing plan...", "info");
5138
+ const sprintOrganizerPrompt = buildSprintOrganizerPrompt({
5139
+ directive,
5140
+ architectOutput,
5141
+ feedback
5142
+ });
5143
+ const sprintOrganizerOutput = await this.aiRunner.run(sprintOrganizerPrompt);
5144
+ this.log("Sprint Organizer phase complete.", "success");
5145
+ const plan = parseSprintPlanFromAI(sprintOrganizerOutput, directive);
5146
+ if (feedback) {
5147
+ plan.feedback = feedback;
5148
+ }
5149
+ return {
5150
+ plan,
5151
+ phaseOutputs: {
5152
+ techLead: techLeadOutput,
5153
+ architect: architectOutput,
5154
+ sprintOrganizer: sprintOrganizerOutput
5155
+ }
5156
+ };
5157
+ }
5158
+ getProjectContext() {
5159
+ const kb = new KnowledgeBase(this.projectPath);
5160
+ return kb.getFullContext();
5161
+ }
5162
+ getCodebaseIndex() {
5163
+ const indexPath = getLocusPath(this.projectPath, "indexFile");
5164
+ if (!import_node_fs10.existsSync(indexPath)) {
5165
+ return "";
5166
+ }
5167
+ try {
5168
+ const raw = import_node_fs10.readFileSync(indexPath, "utf-8");
5169
+ const index = JSON.parse(raw);
5170
+ const parts = [];
5171
+ if (index.responsibilities) {
5172
+ parts.push("### File Responsibilities");
5173
+ const entries = Object.entries(index.responsibilities);
5174
+ for (const [file, summary] of entries.slice(0, 50)) {
5175
+ parts.push(`- \`${file}\`: ${summary}`);
5176
+ }
5177
+ if (entries.length > 50) {
5178
+ parts.push(`... and ${entries.length - 50} more files`);
5179
+ }
5180
+ }
5181
+ return parts.join(`
5182
+ `);
5183
+ } catch {
5184
+ return "";
5185
+ }
3293
5186
  }
3294
5187
  }