@locusai/sdk 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts +2 -0
  2. package/dist/agent/__tests__/orchestrator.cleanup.test.d.ts.map +1 -0
  3. package/dist/agent/__tests__/worker.no-changes.test.d.ts +2 -0
  4. package/dist/agent/__tests__/worker.no-changes.test.d.ts.map +1 -0
  5. package/dist/agent/document-fetcher.d.ts.map +1 -1
  6. package/dist/agent/index.d.ts +1 -0
  7. package/dist/agent/index.d.ts.map +1 -1
  8. package/dist/agent/review-service.d.ts.map +1 -1
  9. package/dist/agent/reviewer-worker.d.ts +42 -0
  10. package/dist/agent/reviewer-worker.d.ts.map +1 -0
  11. package/dist/agent/task-executor.d.ts +1 -1
  12. package/dist/agent/task-executor.d.ts.map +1 -1
  13. package/dist/agent/worker.d.ts +47 -4
  14. package/dist/agent/worker.d.ts.map +1 -1
  15. package/dist/agent/worker.js +1102 -506
  16. package/dist/ai/claude-runner.d.ts +5 -0
  17. package/dist/ai/claude-runner.d.ts.map +1 -1
  18. package/dist/ai/codex-runner.d.ts +5 -0
  19. package/dist/ai/codex-runner.d.ts.map +1 -1
  20. package/dist/ai/runner.d.ts +5 -0
  21. package/dist/ai/runner.d.ts.map +1 -1
  22. package/dist/core/config.d.ts +10 -2
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/index.d.ts +1 -1
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/prompt-builder.d.ts +3 -6
  27. package/dist/core/prompt-builder.d.ts.map +1 -1
  28. package/dist/git/git-utils.d.ts +31 -0
  29. package/dist/git/git-utils.d.ts.map +1 -0
  30. package/dist/git/index.d.ts +3 -0
  31. package/dist/git/index.d.ts.map +1 -0
  32. package/dist/git/pr-service.d.ts +66 -0
  33. package/dist/git/pr-service.d.ts.map +1 -0
  34. package/dist/index-node.d.ts +5 -1
  35. package/dist/index-node.d.ts.map +1 -1
  36. package/dist/index-node.js +2560 -729
  37. package/dist/index.d.ts +0 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +17 -49
  40. package/dist/modules/auth.d.ts +3 -0
  41. package/dist/modules/auth.d.ts.map +1 -1
  42. package/dist/modules/tasks.d.ts +0 -5
  43. package/dist/modules/tasks.d.ts.map +1 -1
  44. package/dist/modules/workspaces.d.ts +10 -10
  45. package/dist/modules/workspaces.d.ts.map +1 -1
  46. package/dist/orchestrator.d.ts +38 -5
  47. package/dist/orchestrator.d.ts.map +1 -1
  48. package/dist/planning/agents/architect.d.ts +15 -0
  49. package/dist/planning/agents/architect.d.ts.map +1 -0
  50. package/dist/planning/agents/sprint-organizer.d.ts +14 -0
  51. package/dist/planning/agents/sprint-organizer.d.ts.map +1 -0
  52. package/dist/planning/agents/tech-lead.d.ts +15 -0
  53. package/dist/planning/agents/tech-lead.d.ts.map +1 -0
  54. package/dist/planning/index.d.ts +4 -0
  55. package/dist/planning/index.d.ts.map +1 -0
  56. package/dist/planning/plan-manager.d.ts +52 -0
  57. package/dist/planning/plan-manager.d.ts.map +1 -0
  58. package/dist/planning/planning-meeting.d.ts +36 -0
  59. package/dist/planning/planning-meeting.d.ts.map +1 -0
  60. package/dist/planning/sprint-plan.d.ts +47 -0
  61. package/dist/planning/sprint-plan.d.ts.map +1 -0
  62. package/dist/project/knowledge-base.d.ts +25 -0
  63. package/dist/project/knowledge-base.d.ts.map +1 -0
  64. package/dist/worktree/index.d.ts +3 -0
  65. package/dist/worktree/index.d.ts.map +1 -0
  66. package/dist/worktree/worktree-config.d.ts +58 -0
  67. package/dist/worktree/worktree-config.d.ts.map +1 -0
  68. package/dist/worktree/worktree-manager.d.ts +96 -0
  69. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  70. package/package.json +2 -2
  71. package/dist/modules/ai.d.ts +0 -55
  72. package/dist/modules/ai.d.ts.map +0 -1
@@ -52,8 +52,7 @@ __export(exports_src, {
52
52
  InvitationsModule: () => InvitationsModule,
53
53
  DocsModule: () => DocsModule,
54
54
  CiModule: () => CiModule,
55
- AuthModule: () => AuthModule,
56
- AIModule: () => AIModule
55
+ AuthModule: () => AuthModule
57
56
  });
58
57
  module.exports = __toCommonJS(exports_src);
59
58
  var import_axios = __toESM(require("axios"));
@@ -86,43 +85,6 @@ class BaseModule {
86
85
  }
87
86
  }
88
87
 
89
- // src/modules/ai.ts
90
- class AIModule extends BaseModule {
91
- async chat(workspaceId, body) {
92
- const { data } = await this.api.post(`/ai/${workspaceId}/chat`, body, { timeout: 300000 });
93
- return data;
94
- }
95
- async detectIntent(workspaceId, body) {
96
- const { data } = await this.api.post(`/ai/${workspaceId}/chat/intent`, body, { timeout: 300000 });
97
- return data;
98
- }
99
- async executeIntent(workspaceId, body) {
100
- const { data } = await this.api.post(`/ai/${workspaceId}/chat/execute`, body, { timeout: 300000 });
101
- return data;
102
- }
103
- async listSessions(workspaceId) {
104
- const { data } = await this.api.get(`/ai/${workspaceId}/sessions`);
105
- return data;
106
- }
107
- async getSession(workspaceId, sessionId) {
108
- const { data } = await this.api.get(`/ai/${workspaceId}/session/${sessionId}`);
109
- return data;
110
- }
111
- getChatStreamUrl(workspaceId, sessionId) {
112
- return `${this.api.defaults.baseURL}/ai/${workspaceId}/chat/stream?sessionId=${sessionId}`;
113
- }
114
- async deleteSession(workspaceId, sessionId) {
115
- await this.api.delete(`/ai/${workspaceId}/session/${sessionId}`);
116
- }
117
- async shareSession(workspaceId, sessionId, body) {
118
- await this.api.post(`/ai/${workspaceId}/session/${sessionId}/share`, body);
119
- }
120
- async getSharedSession(sessionId) {
121
- const { data } = await this.api.get(`/ai/shared/${sessionId}`);
122
- return data;
123
- }
124
- }
125
-
126
88
  // src/modules/auth.ts
127
89
  class AuthModule extends BaseModule {
128
90
  async getProfile() {
@@ -149,6 +111,10 @@ class AuthModule extends BaseModule {
149
111
  const { data } = await this.api.post("/auth/complete-registration", body);
150
112
  return data;
151
113
  }
114
+ async deleteAccount() {
115
+ const { data } = await this.api.delete("/auth/account");
116
+ return data;
117
+ }
152
118
  }
153
119
 
154
120
  // src/modules/ci.ts
@@ -346,10 +312,6 @@ class TasksModule extends BaseModule {
346
312
  updates
347
313
  });
348
314
  }
349
- async getContext(id, workspaceId) {
350
- const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}/context`);
351
- return data;
352
- }
353
315
  }
354
316
 
355
317
  // src/modules/workspaces.ts
@@ -386,10 +348,6 @@ class WorkspacesModule extends BaseModule {
386
348
  const { data } = await this.api.get(`/workspaces/${id}/stats`);
387
349
  return data;
388
350
  }
389
- async getManifestStatus(workspaceId) {
390
- const { data } = await this.api.get(`/workspaces/${workspaceId}/manifest-status`);
391
- return data;
392
- }
393
351
  async getActivity(id, limit) {
394
352
  const { data } = await this.api.get(`/workspaces/${id}/activity`, {
395
353
  params: { limit }
@@ -400,6 +358,18 @@ class WorkspacesModule extends BaseModule {
400
358
  const { data } = await this.api.post(`/workspaces/${id}/dispatch`, { workerId, sprintId });
401
359
  return data.task;
402
360
  }
361
+ async heartbeat(workspaceId, agentId, currentTaskId, status) {
362
+ const { data } = await this.api.post(`/workspaces/${workspaceId}/agents/heartbeat`, {
363
+ agentId,
364
+ currentTaskId: currentTaskId ?? null,
365
+ status: status ?? "WORKING"
366
+ });
367
+ return data.agent;
368
+ }
369
+ async getAgents(workspaceId) {
370
+ const { data } = await this.api.get(`/workspaces/${workspaceId}/agents`);
371
+ return data.agents;
372
+ }
403
373
  async listApiKeys(workspaceId) {
404
374
  const { data } = await this.api.get(`/workspaces/${workspaceId}/api-keys`);
405
375
  return data.apiKeys;
@@ -418,7 +388,6 @@ class LocusClient {
418
388
  api;
419
389
  emitter;
420
390
  auth;
421
- ai;
422
391
  tasks;
423
392
  sprints;
424
393
  workspaces;
@@ -438,7 +407,6 @@ class LocusClient {
438
407
  });
439
408
  this.setupInterceptors();
440
409
  this.auth = new AuthModule(this.api, this.emitter);
441
- this.ai = new AIModule(this.api, this.emitter);
442
410
  this.tasks = new TasksModule(this.api, this.emitter);
443
411
  this.sprints = new SprintsModule(this.api, this.emitter);
444
412
  this.workspaces = new WorkspacesModule(this.api, this.emitter);
@@ -512,8 +480,7 @@ __export(exports_worker, {
512
480
  AgentWorker: () => AgentWorker
513
481
  });
514
482
  module.exports = __toCommonJS(exports_worker);
515
- var import_node_fs5 = require("node:fs");
516
- var import_node_path7 = require("node:path");
483
+ var import_shared3 = require("@locusai/shared");
517
484
 
518
485
  // src/core/config.ts
519
486
  var import_node_path = require("node:path");
@@ -523,19 +490,27 @@ var PROVIDER = {
523
490
  };
524
491
  var DEFAULT_MODEL = {
525
492
  [PROVIDER.CLAUDE]: "opus",
526
- [PROVIDER.CODEX]: "gpt-5.2-codex"
493
+ [PROVIDER.CODEX]: "gpt-5.3-codex"
494
+ };
495
+ var LOCUS_SCHEMA_BASE_URL = "https://locusai.dev/schemas";
496
+ var LOCUS_SCHEMAS = {
497
+ config: `${LOCUS_SCHEMA_BASE_URL}/config.schema.json`,
498
+ settings: `${LOCUS_SCHEMA_BASE_URL}/settings.schema.json`
527
499
  };
528
500
  var LOCUS_CONFIG = {
529
501
  dir: ".locus",
530
502
  configFile: "config.json",
503
+ settingsFile: "settings.json",
531
504
  indexFile: "codebase-index.json",
532
- contextFile: "CLAUDE.md",
505
+ contextFile: "LOCUS.md",
533
506
  artifactsDir: "artifacts",
534
507
  documentsDir: "documents",
535
- agentSkillsDir: ".agent/skills",
536
508
  sessionsDir: "sessions",
537
509
  reviewsDir: "reviews",
538
- plansDir: "plans"
510
+ plansDir: "plans",
511
+ projectDir: "project",
512
+ projectContextFile: "context.md",
513
+ projectProgressFile: "progress.md"
539
514
  };
540
515
  var LOCUS_GITIGNORE_PATTERNS = [
541
516
  "# Locus AI - Session data (user-specific, can grow large)",
@@ -548,11 +523,17 @@ var LOCUS_GITIGNORE_PATTERNS = [
548
523
  ".locus/reviews/",
549
524
  "",
550
525
  "# Locus AI - Plans (generated per task)",
551
- ".locus/plans/"
526
+ ".locus/plans/",
527
+ "",
528
+ "# Locus AI - Agent worktrees (parallel execution)",
529
+ ".locus-worktrees/",
530
+ "",
531
+ "# Locus AI - Settings (contains API key, telegram config, etc.)",
532
+ ".locus/settings.json"
552
533
  ];
553
534
  function getLocusPath(projectPath, fileName) {
554
- if (fileName === "contextFile") {
555
- return import_node_path.join(projectPath, LOCUS_CONFIG.contextFile);
535
+ if (fileName === "projectContextFile" || fileName === "projectProgressFile") {
536
+ return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.projectDir, LOCUS_CONFIG[fileName]);
556
537
  }
557
538
  return import_node_path.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG[fileName]);
558
539
  }
@@ -628,6 +609,14 @@ var c = {
628
609
  };
629
610
 
630
611
  // src/ai/claude-runner.ts
612
+ var SANDBOX_SETTINGS = JSON.stringify({
613
+ sandbox: {
614
+ enabled: true,
615
+ autoAllow: true,
616
+ allowUnsandboxedCommands: false
617
+ }
618
+ });
619
+
631
620
  class ClaudeRunner {
632
621
  model;
633
622
  log;
@@ -635,6 +624,7 @@ class ClaudeRunner {
635
624
  eventEmitter;
636
625
  currentToolName;
637
626
  activeTools = new Map;
627
+ activeProcess = null;
638
628
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
639
629
  this.model = model;
640
630
  this.log = log;
@@ -643,6 +633,12 @@ class ClaudeRunner {
643
633
  setEventEmitter(emitter) {
644
634
  this.eventEmitter = emitter;
645
635
  }
636
+ abort() {
637
+ if (this.activeProcess && !this.activeProcess.killed) {
638
+ this.activeProcess.kill("SIGTERM");
639
+ this.activeProcess = null;
640
+ }
641
+ }
646
642
  async run(prompt) {
647
643
  const maxRetries = 3;
648
644
  let lastError = null;
@@ -671,7 +667,9 @@ class ClaudeRunner {
671
667
  "stream-json",
672
668
  "--include-partial-messages",
673
669
  "--model",
674
- this.model
670
+ this.model,
671
+ "--settings",
672
+ SANDBOX_SETTINGS
675
673
  ];
676
674
  const env = {
677
675
  ...process.env,
@@ -688,6 +686,7 @@ class ClaudeRunner {
688
686
  stdio: ["pipe", "pipe", "pipe"],
689
687
  env
690
688
  });
689
+ this.activeProcess = claude;
691
690
  let buffer = "";
692
691
  let stderrBuffer = "";
693
692
  let resolveChunk = null;
@@ -755,6 +754,7 @@ class ClaudeRunner {
755
754
  signalEnd();
756
755
  });
757
756
  claude.on("close", (code) => {
757
+ this.activeProcess = null;
758
758
  if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
759
759
  process.stderr.write(`${stderrBuffer}
760
760
  `);
@@ -912,7 +912,9 @@ class ClaudeRunner {
912
912
  "stream-json",
913
913
  "--include-partial-messages",
914
914
  "--model",
915
- this.model
915
+ this.model,
916
+ "--settings",
917
+ SANDBOX_SETTINGS
916
918
  ];
917
919
  const env = {
918
920
  ...process.env,
@@ -924,6 +926,7 @@ class ClaudeRunner {
924
926
  stdio: ["pipe", "pipe", "pipe"],
925
927
  env
926
928
  });
929
+ this.activeProcess = claude;
927
930
  let finalResult = "";
928
931
  let errorOutput = "";
929
932
  let buffer = "";
@@ -957,6 +960,7 @@ class ClaudeRunner {
957
960
  reject(new Error(`Failed to start Claude CLI: ${err.message}. Please ensure the 'claude' command is available in your PATH.`));
958
961
  });
959
962
  claude.on("close", (code) => {
963
+ this.activeProcess = null;
960
964
  if (stderrBuffer && !this.shouldSuppressLine(stderrBuffer)) {
961
965
  process.stderr.write(`${stderrBuffer}
962
966
  `);
@@ -1023,11 +1027,18 @@ class CodexRunner {
1023
1027
  projectPath;
1024
1028
  model;
1025
1029
  log;
1030
+ activeProcess = null;
1026
1031
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
1027
1032
  this.projectPath = projectPath;
1028
1033
  this.model = model;
1029
1034
  this.log = log;
1030
1035
  }
1036
+ abort() {
1037
+ if (this.activeProcess && !this.activeProcess.killed) {
1038
+ this.activeProcess.kill("SIGTERM");
1039
+ this.activeProcess = null;
1040
+ }
1041
+ }
1031
1042
  async run(prompt) {
1032
1043
  const maxRetries = 3;
1033
1044
  let lastError = null;
@@ -1054,6 +1065,7 @@ class CodexRunner {
1054
1065
  env: process.env,
1055
1066
  shell: false
1056
1067
  });
1068
+ this.activeProcess = codex;
1057
1069
  let resolveChunk = null;
1058
1070
  const chunkQueue = [];
1059
1071
  let processEnded = false;
@@ -1103,6 +1115,7 @@ class CodexRunner {
1103
1115
  signalEnd();
1104
1116
  });
1105
1117
  codex.on("close", (code) => {
1118
+ this.activeProcess = null;
1106
1119
  this.cleanupTempFile(outputPath);
1107
1120
  if (code === 0) {
1108
1121
  const result = this.readOutput(outputPath, finalOutput);
@@ -1148,6 +1161,7 @@ class CodexRunner {
1148
1161
  env: process.env,
1149
1162
  shell: false
1150
1163
  });
1164
+ this.activeProcess = codex;
1151
1165
  let output = "";
1152
1166
  let errorOutput = "";
1153
1167
  const handleOutput = (data) => {
@@ -1165,6 +1179,7 @@ class CodexRunner {
1165
1179
  reject(new Error(`Failed to start Codex CLI: ${err.message}. ` + `Ensure 'codex' is installed and available in PATH.`));
1166
1180
  });
1167
1181
  codex.on("close", (code) => {
1182
+ this.activeProcess = null;
1168
1183
  this.cleanupTempFile(outputPath);
1169
1184
  if (code === 0) {
1170
1185
  resolve2(this.readOutput(outputPath, output));
@@ -1179,7 +1194,8 @@ class CodexRunner {
1179
1194
  buildArgs(outputPath) {
1180
1195
  const args = [
1181
1196
  "exec",
1182
- "--full-auto",
1197
+ "--sandbox",
1198
+ "workspace-write",
1183
1199
  "--skip-git-repo-check",
1184
1200
  "--output-last-message",
1185
1201
  outputPath
@@ -1246,435 +1262,805 @@ function createAiRunner(provider, config) {
1246
1262
  }
1247
1263
  }
1248
1264
 
1249
- // src/core/indexer.ts
1250
- var import_node_crypto2 = require("node:crypto");
1251
- var import_node_fs2 = require("node:fs");
1252
- var import_node_path4 = require("node:path");
1253
- var import_globby = require("globby");
1265
+ // src/git/git-utils.ts
1266
+ var import_node_child_process3 = require("node:child_process");
1267
+ function isGitAvailable() {
1268
+ try {
1269
+ import_node_child_process3.execFileSync("git", ["--version"], {
1270
+ encoding: "utf-8",
1271
+ stdio: ["pipe", "pipe", "pipe"]
1272
+ });
1273
+ return true;
1274
+ } catch {
1275
+ return false;
1276
+ }
1277
+ }
1278
+ function isGhAvailable(projectPath) {
1279
+ try {
1280
+ import_node_child_process3.execFileSync("gh", ["auth", "status"], {
1281
+ cwd: projectPath,
1282
+ encoding: "utf-8",
1283
+ stdio: ["pipe", "pipe", "pipe"]
1284
+ });
1285
+ return true;
1286
+ } catch {
1287
+ return false;
1288
+ }
1289
+ }
1290
+ function getGhUsername() {
1291
+ try {
1292
+ const output = import_node_child_process3.execFileSync("gh", ["api", "user", "--jq", ".login"], {
1293
+ encoding: "utf-8",
1294
+ stdio: ["pipe", "pipe", "pipe"]
1295
+ }).trim();
1296
+ return output || null;
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ }
1301
+ function detectRemoteProvider(projectPath) {
1302
+ const url = getRemoteUrl(projectPath);
1303
+ if (!url)
1304
+ return "unknown";
1305
+ if (url.includes("github.com"))
1306
+ return "github";
1307
+ if (url.includes("gitlab.com") || url.includes("gitlab"))
1308
+ return "gitlab";
1309
+ if (url.includes("bitbucket.org"))
1310
+ return "bitbucket";
1311
+ return "unknown";
1312
+ }
1313
+ function getRemoteUrl(projectPath, remote = "origin") {
1314
+ try {
1315
+ return import_node_child_process3.execFileSync("git", ["remote", "get-url", remote], {
1316
+ cwd: projectPath,
1317
+ encoding: "utf-8",
1318
+ stdio: ["pipe", "pipe", "pipe"]
1319
+ }).trim();
1320
+ } catch {
1321
+ return null;
1322
+ }
1323
+ }
1324
+ function getCurrentBranch(projectPath) {
1325
+ return import_node_child_process3.execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1326
+ cwd: projectPath,
1327
+ encoding: "utf-8",
1328
+ stdio: ["pipe", "pipe", "pipe"]
1329
+ }).trim();
1330
+ }
1331
+ function getDefaultBranch(projectPath, remote = "origin") {
1332
+ try {
1333
+ const ref = import_node_child_process3.execFileSync("git", ["symbolic-ref", `refs/remotes/${remote}/HEAD`], {
1334
+ cwd: projectPath,
1335
+ encoding: "utf-8",
1336
+ stdio: ["pipe", "pipe", "pipe"]
1337
+ }).trim();
1338
+ return ref.replace(`refs/remotes/${remote}/`, "");
1339
+ } catch {
1340
+ for (const candidate of ["main", "master"]) {
1341
+ try {
1342
+ import_node_child_process3.execFileSync("git", ["ls-remote", "--exit-code", "--heads", remote, candidate], {
1343
+ cwd: projectPath,
1344
+ encoding: "utf-8",
1345
+ stdio: ["pipe", "pipe", "pipe"]
1346
+ });
1347
+ return candidate;
1348
+ } catch {}
1349
+ }
1350
+ try {
1351
+ return getCurrentBranch(projectPath);
1352
+ } catch {
1353
+ return "main";
1354
+ }
1355
+ }
1356
+ }
1254
1357
 
1255
- class CodebaseIndexer {
1358
+ // src/git/pr-service.ts
1359
+ var import_node_child_process4 = require("node:child_process");
1360
+ class PrService {
1256
1361
  projectPath;
1257
- indexPath;
1258
- fullReindexRatioThreshold = 0.2;
1259
- constructor(projectPath) {
1362
+ log;
1363
+ constructor(projectPath, log) {
1260
1364
  this.projectPath = projectPath;
1261
- this.indexPath = import_node_path4.join(projectPath, ".locus", "codebase-index.json");
1365
+ this.log = log;
1262
1366
  }
1263
- async index(onProgress, treeSummarizer, force = false) {
1264
- if (!treeSummarizer) {
1265
- 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})`);
1378
+ }
1379
+ if (!isGhAvailable(this.projectPath)) {
1380
+ throw new Error("GitHub CLI (gh) is not installed or not authenticated. Install from https://cli.github.com/");
1381
+ }
1382
+ const title = `[Locus] ${task.title}`;
1383
+ const body = this.buildPrBody(task, agentId, summary);
1384
+ const baseBranch = requestedBaseBranch ?? getDefaultBranch(this.projectPath);
1385
+ this.validateCreatePrInputs(baseBranch, branch);
1386
+ this.log(`Creating PR: ${title} (${branch} → ${baseBranch})`, "info");
1387
+ const output = import_node_child_process4.execFileSync("gh", [
1388
+ "pr",
1389
+ "create",
1390
+ "--title",
1391
+ title,
1392
+ "--body",
1393
+ body,
1394
+ "--base",
1395
+ baseBranch,
1396
+ "--head",
1397
+ branch
1398
+ ], {
1399
+ cwd: this.projectPath,
1400
+ encoding: "utf-8",
1401
+ stdio: ["pipe", "pipe", "pipe"]
1402
+ }).trim();
1403
+ const url = output;
1404
+ const prNumber = this.extractPrNumber(url);
1405
+ this.log(`PR created: ${url}`, "success");
1406
+ return { url, number: prNumber };
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.`);
1266
1411
  }
1267
- onProgress?.("Generating file tree...");
1268
- const currentFiles = await this.getFileTree();
1269
- const treeString = currentFiles.join(`
1270
- `);
1271
- const newTreeHash = this.hashTree(treeString);
1272
- const existingIndex = this.loadIndex();
1273
- const currentHashes = this.computeFileHashes(currentFiles);
1274
- const existingHashes = existingIndex?.fileHashes;
1275
- const hasExistingContent = existingIndex && (Object.keys(existingIndex.symbols).length > 0 || Object.keys(existingIndex.responsibilities).length > 0);
1276
- const canIncremental = !force && existingIndex && existingHashes && hasExistingContent;
1277
- if (canIncremental) {
1278
- onProgress?.("Performing incremental update");
1279
- const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
1280
- const changedFiles = [...added, ...modified];
1281
- const totalChanges = changedFiles.length + deleted.length;
1282
- const existingFileCount = Object.keys(existingHashes).length;
1283
- onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
1284
- if (existingFileCount > 0) {
1285
- const changeRatio = totalChanges / existingFileCount;
1286
- if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
1287
- onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
1288
- const incrementalIndex = await treeSummarizer(changedFiles.join(`
1289
- `));
1290
- const updatedIndex = this.cloneIndex(existingIndex);
1291
- this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
1292
- return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
1293
- }
1294
- if (changedFiles.length === 0 && deleted.length > 0) {
1295
- onProgress?.(`Removing ${deleted.length} deleted files from index`);
1296
- const updatedIndex = this.cloneIndex(existingIndex);
1297
- this.removeFilesFromIndex(updatedIndex, deleted);
1298
- return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
1299
- }
1300
- if (changedFiles.length === 0 && deleted.length === 0) {
1301
- onProgress?.("No actual file changes, updating hashes only");
1302
- const updatedIndex = this.cloneIndex(existingIndex);
1303
- return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
1304
- }
1305
- onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
1306
- }
1412
+ if (!this.hasRemoteBranch(headBranch)) {
1413
+ throw new Error(`Head branch "${headBranch}" is not available on origin. Ensure it is pushed before PR creation.`);
1307
1414
  }
1308
- onProgress?.("AI is analyzing codebase structure...");
1309
- try {
1310
- const index = await treeSummarizer(treeString);
1311
- return this.applyIndexMetadata(index, currentHashes, newTreeHash);
1312
- } catch (error) {
1313
- throw new Error(`AI analysis failed: ${error instanceof Error ? error.message : String(error)}`);
1415
+ const baseRef = this.resolveBranchRef(baseBranch);
1416
+ const headRef = this.resolveBranchRef(headBranch);
1417
+ if (!baseRef) {
1418
+ throw new Error(`Could not resolve base branch "${baseBranch}" locally.`);
1314
1419
  }
1315
- }
1316
- async getFileTree() {
1317
- const gitmodulesPath = import_node_path4.join(this.projectPath, ".gitmodules");
1318
- const submoduleIgnores = [];
1319
- if (import_node_fs2.existsSync(gitmodulesPath)) {
1320
- try {
1321
- const content = import_node_fs2.readFileSync(gitmodulesPath, "utf-8");
1322
- const lines = content.split(`
1323
- `);
1324
- for (const line of lines) {
1325
- const match = line.match(/^\s*path\s*=\s*(.*)$/);
1326
- const path = match?.[1]?.trim();
1327
- if (path) {
1328
- submoduleIgnores.push(`${path}/**`);
1329
- submoduleIgnores.push(`**/${path}/**`);
1330
- }
1331
- }
1332
- } catch {}
1420
+ if (!headRef) {
1421
+ throw new Error(`Could not resolve head branch "${headBranch}" locally.`);
1333
1422
  }
1334
- return import_globby.globby(["**/*"], {
1423
+ const commitsAhead = this.countCommitsAhead(baseRef, headRef);
1424
+ if (commitsAhead <= 0) {
1425
+ throw new Error(`No commits between "${baseBranch}" and "${headBranch}". Skipping PR creation.`);
1426
+ }
1427
+ }
1428
+ countCommitsAhead(baseRef, headRef) {
1429
+ const output = import_node_child_process4.execFileSync("git", ["rev-list", "--count", `${baseRef}..${headRef}`], {
1335
1430
  cwd: this.projectPath,
1336
- gitignore: true,
1337
- ignore: [
1338
- ...submoduleIgnores,
1339
- "**/node_modules/**",
1340
- "**/dist/**",
1341
- "**/build/**",
1342
- "**/target/**",
1343
- "**/bin/**",
1344
- "**/obj/**",
1345
- "**/.next/**",
1346
- "**/.svelte-kit/**",
1347
- "**/.nuxt/**",
1348
- "**/.cache/**",
1349
- "**/out/**",
1350
- "**/__tests__/**",
1351
- "**/coverage/**",
1352
- "**/*.test.*",
1353
- "**/*.spec.*",
1354
- "**/*.d.ts",
1355
- "**/tsconfig.tsbuildinfo",
1356
- "**/.locus/*.json",
1357
- "**/.locus/*.md",
1358
- "**/.locus/!(artifacts)/**",
1359
- "**/.git/**",
1360
- "**/.svn/**",
1361
- "**/.hg/**",
1362
- "**/.vscode/**",
1363
- "**/.idea/**",
1364
- "**/.DS_Store",
1365
- "**/bun.lock",
1366
- "**/package-lock.json",
1367
- "**/yarn.lock",
1368
- "**/pnpm-lock.yaml",
1369
- "**/Cargo.lock",
1370
- "**/go.sum",
1371
- "**/poetry.lock",
1372
- "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
1373
- ]
1374
- });
1431
+ encoding: "utf-8",
1432
+ stdio: ["pipe", "pipe", "pipe"]
1433
+ }).trim();
1434
+ const value = Number.parseInt(output || "0", 10);
1435
+ return Number.isNaN(value) ? 0 : value;
1375
1436
  }
1376
- loadIndex() {
1377
- if (import_node_fs2.existsSync(this.indexPath)) {
1378
- try {
1379
- return JSON.parse(import_node_fs2.readFileSync(this.indexPath, "utf-8"));
1380
- } catch {
1381
- return null;
1382
- }
1437
+ resolveBranchRef(branch) {
1438
+ if (this.hasLocalBranch(branch)) {
1439
+ return branch;
1440
+ }
1441
+ if (this.hasRemoteTrackingBranch(branch)) {
1442
+ return `origin/${branch}`;
1383
1443
  }
1384
1444
  return null;
1385
1445
  }
1386
- saveIndex(index) {
1387
- const dir = import_node_path4.dirname(this.indexPath);
1388
- if (!import_node_fs2.existsSync(dir)) {
1389
- import_node_fs2.mkdirSync(dir, { recursive: true });
1446
+ hasLocalBranch(branch) {
1447
+ try {
1448
+ import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
1449
+ cwd: this.projectPath,
1450
+ stdio: ["pipe", "pipe", "pipe"]
1451
+ });
1452
+ return true;
1453
+ } catch {
1454
+ return false;
1390
1455
  }
1391
- import_node_fs2.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
1392
1456
  }
1393
- cloneIndex(index) {
1394
- return JSON.parse(JSON.stringify(index));
1457
+ hasRemoteTrackingBranch(branch) {
1458
+ try {
1459
+ import_node_child_process4.execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], {
1460
+ cwd: this.projectPath,
1461
+ stdio: ["pipe", "pipe", "pipe"]
1462
+ });
1463
+ return true;
1464
+ } catch {
1465
+ return false;
1466
+ }
1395
1467
  }
1396
- applyIndexMetadata(index, fileHashes, treeHash) {
1397
- index.lastIndexed = new Date().toISOString();
1398
- index.treeHash = treeHash;
1399
- index.fileHashes = fileHashes;
1400
- return index;
1468
+ hasRemoteBranch(branch) {
1469
+ try {
1470
+ import_node_child_process4.execFileSync("git", ["ls-remote", "--exit-code", "--heads", "origin", branch], {
1471
+ cwd: this.projectPath,
1472
+ stdio: ["pipe", "pipe", "pipe"]
1473
+ });
1474
+ return true;
1475
+ } catch {
1476
+ return false;
1477
+ }
1401
1478
  }
1402
- hashTree(tree) {
1403
- 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
+ });
1404
1486
  }
1405
- hashFile(filePath) {
1487
+ submitReview(prIdentifier, body, event) {
1406
1488
  try {
1407
- const content = import_node_fs2.readFileSync(import_node_path4.join(this.projectPath, filePath), "utf-8");
1408
- return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
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
+ }));
1409
1537
  } catch {
1410
- return null;
1538
+ this.log("Failed to list Locus PRs", "warn");
1539
+ return [];
1411
1540
  }
1412
1541
  }
1413
- computeFileHashes(files) {
1414
- const hashes = {};
1415
- for (const file of files) {
1416
- const hash = this.hashFile(file);
1417
- if (hash !== null) {
1418
- hashes[file] = hash;
1419
- }
1542
+ hasLocusReview(prNumber) {
1543
+ try {
1544
+ const output = import_node_child_process4.execFileSync("gh", ["pr", "view", prNumber, "--json", "reviews"], {
1545
+ cwd: this.projectPath,
1546
+ encoding: "utf-8",
1547
+ stdio: ["pipe", "pipe", "pipe"]
1548
+ }).trim();
1549
+ const data = JSON.parse(output || "{}");
1550
+ return data.reviews?.some((r) => r.body?.includes("## Locus Agent Review")) ?? false;
1551
+ } catch {
1552
+ return false;
1420
1553
  }
1421
- return hashes;
1422
1554
  }
1423
- diffFiles(currentHashes, existingHashes) {
1424
- const currentFiles = Object.keys(currentHashes);
1425
- const existingFiles = Object.keys(existingHashes);
1426
- const existingSet = new Set(existingFiles);
1427
- const currentSet = new Set(currentFiles);
1428
- const added = currentFiles.filter((f) => !existingSet.has(f));
1429
- const deleted = existingFiles.filter((f) => !currentSet.has(f));
1430
- const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
1431
- return { added, deleted, modified };
1555
+ listUnreviewedLocusPrs() {
1556
+ const allPrs = this.listLocusPrs();
1557
+ return allPrs.filter((pr) => !this.hasLocusReview(String(pr.number)));
1432
1558
  }
1433
- removeFilesFromIndex(index, files) {
1434
- const fileSet = new Set(files);
1435
- for (const file of files) {
1436
- delete index.responsibilities[file];
1559
+ buildPrBody(task, agentId, summary) {
1560
+ const sections = [];
1561
+ sections.push(`## Task: ${task.title}`);
1562
+ sections.push("");
1563
+ if (task.description) {
1564
+ sections.push(task.description);
1565
+ sections.push("");
1437
1566
  }
1438
- for (const [symbol, paths] of Object.entries(index.symbols)) {
1439
- index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
1440
- if (index.symbols[symbol].length === 0) {
1441
- delete index.symbols[symbol];
1567
+ if (task.acceptanceChecklist?.length > 0) {
1568
+ sections.push("## Acceptance Criteria");
1569
+ for (const item of task.acceptanceChecklist) {
1570
+ sections.push(`- [ ] ${item.text}`);
1442
1571
  }
1572
+ sections.push("");
1443
1573
  }
1444
- }
1445
- mergeIndex(existing, incremental, newHashes, newTreeHash) {
1446
- const mergedSymbols = { ...existing.symbols };
1447
- for (const [symbol, paths] of Object.entries(incremental.symbols)) {
1448
- if (mergedSymbols[symbol]) {
1449
- mergedSymbols[symbol] = [
1450
- ...new Set([...mergedSymbols[symbol], ...paths])
1451
- ];
1452
- } else {
1453
- mergedSymbols[symbol] = paths;
1454
- }
1574
+ if (summary) {
1575
+ sections.push("## Agent Summary");
1576
+ sections.push(summary);
1577
+ sections.push("");
1455
1578
  }
1456
- const merged = {
1457
- symbols: mergedSymbols,
1458
- responsibilities: {
1459
- ...existing.responsibilities,
1460
- ...incremental.responsibilities
1461
- },
1462
- lastIndexed: ""
1463
- };
1464
- return this.applyIndexMetadata(merged, newHashes, newTreeHash);
1579
+ sections.push("---");
1580
+ sections.push(`*Created by Locus Agent \`${agentId.slice(-8)}\`* | Task ID: \`${task.id}\``);
1581
+ return sections.join(`
1582
+ `);
1583
+ }
1584
+ extractPrNumber(url) {
1585
+ const match = url.match(/\/pull\/(\d+)/);
1586
+ return match ? Number.parseInt(match[1], 10) : 0;
1465
1587
  }
1466
1588
  }
1467
1589
 
1468
- // src/agent/codebase-indexer-service.ts
1469
- class CodebaseIndexerService {
1470
- deps;
1471
- indexer;
1472
- constructor(deps) {
1473
- this.deps = deps;
1474
- this.indexer = new CodebaseIndexer(deps.projectPath);
1590
+ // src/project/knowledge-base.ts
1591
+ var import_node_fs2 = require("node:fs");
1592
+ var import_node_path4 = require("node:path");
1593
+ class KnowledgeBase {
1594
+ contextPath;
1595
+ progressPath;
1596
+ constructor(projectPath) {
1597
+ this.contextPath = getLocusPath(projectPath, "projectContextFile");
1598
+ this.progressPath = getLocusPath(projectPath, "projectProgressFile");
1475
1599
  }
1476
- async reindex(force = false) {
1477
- try {
1478
- const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
1479
- const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
1480
- 1. Key symbols (classes, functions, types) and their locations
1481
- 2. Responsibilities of each directory/file
1482
- 3. Overall project structure
1483
-
1484
- Analyze this file tree and provide a JSON response with:
1485
- - "symbols": object mapping symbol names to file paths (array)
1486
- - "responsibilities": object mapping paths to brief descriptions
1487
-
1488
- File tree:
1489
- ${tree}
1490
-
1491
- Return ONLY valid JSON, no markdown formatting.`;
1492
- const response = await this.deps.aiRunner.run(prompt);
1493
- const jsonMatch = response.match(/\{[\s\S]*\}/);
1494
- if (jsonMatch) {
1495
- return JSON.parse(jsonMatch[0]);
1496
- }
1497
- return { symbols: {}, responsibilities: {}, lastIndexed: "" };
1498
- }, force);
1499
- if (index === null) {
1500
- this.deps.log("No changes detected, skipping reindex", "info");
1501
- return;
1502
- }
1503
- this.indexer.saveIndex(index);
1504
- this.deps.log("Codebase reindexed successfully", "success");
1505
- } catch (error) {
1506
- this.deps.log(`Failed to reindex codebase: ${error}`, "error");
1600
+ readContext() {
1601
+ if (!import_node_fs2.existsSync(this.contextPath)) {
1602
+ return "";
1507
1603
  }
1604
+ return import_node_fs2.readFileSync(this.contextPath, "utf-8");
1508
1605
  }
1509
- }
1510
-
1511
- // src/agent/document-fetcher.ts
1512
- var import_node_fs3 = require("node:fs");
1513
- var import_node_path5 = require("node:path");
1514
- class DocumentFetcher {
1515
- deps;
1516
- constructor(deps) {
1517
- this.deps = deps;
1606
+ readProgress() {
1607
+ if (!import_node_fs2.existsSync(this.progressPath)) {
1608
+ return "";
1609
+ }
1610
+ return import_node_fs2.readFileSync(this.progressPath, "utf-8");
1518
1611
  }
1519
- async fetch() {
1520
- const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
1521
- if (!import_node_fs3.existsSync(documentsDir)) {
1522
- import_node_fs3.mkdirSync(documentsDir, { recursive: true });
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;
1523
1651
  }
1524
- try {
1525
- const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
1526
- const groupMap = new Map(groups.map((g) => [g.id, g.name]));
1527
- const docs2 = await this.deps.client.docs.list(this.deps.workspaceId);
1528
- const artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
1529
- let fetchedCount = 0;
1530
- for (const doc of docs2) {
1531
- if (doc.groupId === artifactsGroupId) {
1532
- continue;
1533
- }
1534
- const groupName = groupMap.get(doc.groupId || "") || "General";
1535
- const groupDir = import_node_path5.join(documentsDir, groupName);
1536
- if (!import_node_fs3.existsSync(groupDir)) {
1537
- import_node_fs3.mkdirSync(groupDir, { recursive: true });
1538
- }
1539
- const fileName = `${doc.title}.md`;
1540
- const filePath = import_node_path5.join(groupDir, fileName);
1541
- if (!import_node_fs3.existsSync(filePath) || import_node_fs3.readFileSync(filePath, "utf-8") !== doc.content) {
1542
- import_node_fs3.writeFileSync(filePath, doc.content || "");
1543
- fetchedCount++;
1544
- }
1545
- }
1546
- if (fetchedCount > 0) {
1547
- this.deps.log(`Fetched ${fetchedCount} document(s) from server`, "info");
1548
- }
1549
- } catch (error) {
1550
- this.deps.log(`Failed to fetch documents: ${error}`, "error");
1652
+ if (event.details) {
1653
+ entry += `
1654
+ ${event.details}`;
1551
1655
  }
1552
- }
1553
- }
1554
-
1555
- // src/agent/review-service.ts
1556
- var import_node_child_process3 = require("node:child_process");
1656
+ const updated = existing ? `${existing}
1657
+ ${entry}` : `# Project Progress
1557
1658
 
1558
- class ReviewService {
1559
- deps;
1560
- constructor(deps) {
1561
- this.deps = deps;
1659
+ ${entry}`;
1660
+ import_node_fs2.writeFileSync(this.progressPath, updated);
1562
1661
  }
1563
- async reviewStagedChanges(sprint) {
1564
- const { projectPath, log } = this.deps;
1565
- try {
1566
- import_node_child_process3.execSync("git add -A", { cwd: projectPath, stdio: "pipe" });
1567
- log("Staged all changes for review.", "info");
1568
- } catch (err) {
1569
- log(`Failed to stage changes: ${err instanceof Error ? err.message : String(err)}`, "error");
1570
- return null;
1571
- }
1572
- let diff;
1573
- try {
1574
- diff = import_node_child_process3.execSync("git diff --cached --stat && echo '---' && git diff --cached", {
1575
- cwd: projectPath,
1576
- maxBuffer: 10 * 1024 * 1024
1577
- }).toString();
1578
- } catch (err) {
1579
- log(`Failed to get staged diff: ${err instanceof Error ? err.message : String(err)}`, "error");
1580
- return null;
1662
+ getFullContext() {
1663
+ const context = this.readContext();
1664
+ const progress = this.readProgress();
1665
+ const parts = [];
1666
+ if (context.trim()) {
1667
+ parts.push(context.trim());
1581
1668
  }
1582
- if (!diff.trim()) {
1583
- return null;
1669
+ if (progress.trim()) {
1670
+ parts.push(progress.trim());
1584
1671
  }
1585
- const sprintInfo = sprint ? `Sprint: ${sprint.name} (${sprint.id})` : "No active sprint";
1586
- const reviewPrompt = `# Code Review Request
1672
+ return parts.join(`
1587
1673
 
1588
- ## Context
1589
- ${sprintInfo}
1590
- Date: ${new Date().toISOString()}
1674
+ ---
1591
1675
 
1592
- ## Staged Changes (git diff)
1593
- \`\`\`diff
1594
- ${diff}
1595
- \`\`\`
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}
1596
1684
 
1597
- ## Instructions
1598
- You are reviewing the staged changes at the end of a sprint. Produce a thorough markdown review report with the following sections:
1685
+ ## Mission
1686
+ ${info.mission}
1599
1687
 
1600
- 1. **Summary** — Brief overview of what changed and why.
1601
- 2. **Files Changed** — List each file with a short description of changes.
1602
- 3. **Code Quality** — Note any code quality concerns (naming, structure, complexity).
1603
- 4. **Potential Issues** — Identify bugs, security issues, edge cases, or regressions.
1604
- 5. **Recommendations** — Actionable suggestions for improvement.
1605
- 6. **Overall Assessment** — A short verdict (e.g., "Looks good", "Needs attention", "Critical issues found").
1688
+ ## Tech Stack
1689
+ ${techStackList}
1606
1690
 
1607
- Keep the review concise but thorough. Focus on substance over style.
1608
- Do NOT output <promise>COMPLETE</promise> just output the review report as markdown.`;
1609
- log("Running AI review on staged changes...", "info");
1610
- const report = await this.deps.aiRunner.run(reviewPrompt);
1611
- return report;
1612
- }
1613
- }
1691
+ ## Architecture
1692
+ <!-- Describe your high-level architecture here -->
1614
1693
 
1615
- // src/core/prompt-builder.ts
1616
- var import_node_fs4 = require("node:fs");
1617
- var import_node_os2 = require("node:os");
1618
- var import_node_path6 = require("node:path");
1619
- var import_shared2 = require("@locusai/shared");
1620
- class PromptBuilder {
1621
- projectPath;
1622
- constructor(projectPath) {
1623
- this.projectPath = projectPath;
1624
- }
1625
- async build(task, options = {}) {
1626
- let prompt = `# Task: ${task.title}
1694
+ ## Key Decisions
1695
+ <!-- Document important technical decisions and their rationale -->
1627
1696
 
1697
+ ## Feature Areas
1698
+ <!-- List your main feature areas and their status -->
1628
1699
  `;
1629
- const roleText = this.roleToText(task.assigneeRole);
1630
- if (roleText) {
1631
- prompt += `## Role
1632
- You are acting as a ${roleText}.
1700
+ const progressContent = `# Project Progress
1633
1701
 
1702
+ No sprints started yet.
1634
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 });
1635
1714
  }
1636
- prompt += `## Description
1637
- ${task.description || "No description provided."}
1715
+ }
1716
+ }
1638
1717
 
1639
- `;
1640
- const projectConfig = this.getProjectConfig();
1641
- if (projectConfig) {
1642
- prompt += `## Project Metadata
1643
- `;
1644
- prompt += `- Version: ${projectConfig.version || "Unknown"}
1645
- `;
1646
- prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
1718
+ // src/worktree/worktree-manager.ts
1719
+ var import_node_child_process5 = require("node:child_process");
1720
+ var import_node_fs3 = require("node:fs");
1721
+ var import_node_path5 = require("node:path");
1647
1722
 
1648
- `;
1649
- }
1650
- let serverContext = null;
1651
- if (options.taskContext) {
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
+ });
1743
+ }
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");
1652
1760
  try {
1653
- serverContext = JSON.parse(options.taskContext);
1761
+ this.git(`worktree remove "${worktreePath}" --force`, this.projectPath);
1654
1762
  } catch {
1655
- serverContext = { context: options.taskContext };
1763
+ import_node_fs3.rmSync(worktreePath, { recursive: true, force: true });
1764
+ this.git("worktree prune", this.projectPath);
1656
1765
  }
1657
1766
  }
1658
- const contextPath = getLocusPath(this.projectPath, "contextFile");
1659
- let hasLocalContext = false;
1660
- if (import_node_fs4.existsSync(contextPath)) {
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.`);
1774
+ }
1775
+ this.log(`Removing existing worktree for branch: ${branch} (${worktreePath2})`, "warn");
1776
+ this.remove(worktreePath2, false);
1777
+ }
1661
1778
  try {
1662
- const context = import_node_fs4.readFileSync(contextPath, "utf-8");
1663
- if (context.trim().length > 20) {
1664
- prompt += `## Project Context (Local)
1665
- ${context}
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(`
1666
1804
 
1667
- `;
1668
- hasLocalContext = true;
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)";
1669
1829
  }
1670
- } catch (err) {
1671
- console.warn(`Warning: Could not read context file: ${err}`);
1830
+ }
1831
+ if (import_node_path5.resolve(path) === this.projectPath) {
1832
+ isMain = true;
1833
+ }
1834
+ if (path) {
1835
+ worktrees.push({ path, branch, head, isMain, isPrunable });
1672
1836
  }
1673
1837
  }
1674
- if (!hasLocalContext) {
1675
- const fallback = this.getFallbackContext();
1676
- if (fallback) {
1677
- prompt += `## Project Context (README Fallback)
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");
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;
1917
+ } catch (error) {
1918
+ if (!this.isNonFastForwardPushError(error)) {
1919
+ throw error;
1920
+ }
1921
+ this.log(`Push rejected for ${branch} (non-fast-forward). Retrying with --force-with-lease.`, "warn");
1922
+ try {
1923
+ this.gitExec(["fetch", remote, branch], worktreePath);
1924
+ } catch {}
1925
+ this.gitExec(["push", "--force-with-lease", "-u", remote, branch], worktreePath);
1926
+ this.log(`Pushed ${branch} to ${remote} with --force-with-lease`, "success");
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 {}
1980
+ }
1981
+ }
1982
+ isNonFastForwardPushError(error) {
1983
+ const message = error instanceof Error ? error.message : String(error);
1984
+ return message.includes("non-fast-forward") || message.includes("[rejected]") || message.includes("fetch first");
1985
+ }
1986
+ git(args, cwd) {
1987
+ return import_node_child_process5.execSync(`git ${args}`, {
1988
+ cwd,
1989
+ encoding: "utf-8",
1990
+ stdio: ["pipe", "pipe", "pipe"]
1991
+ });
1992
+ }
1993
+ gitExec(args, cwd) {
1994
+ return import_node_child_process5.execFileSync("git", args, {
1995
+ cwd,
1996
+ encoding: "utf-8",
1997
+ stdio: ["pipe", "pipe", "pipe"]
1998
+ });
1999
+ }
2000
+ }
2001
+
2002
+ // src/core/prompt-builder.ts
2003
+ var import_node_fs4 = require("node:fs");
2004
+ var import_node_path6 = require("node:path");
2005
+ var import_shared2 = require("@locusai/shared");
2006
+ class PromptBuilder {
2007
+ projectPath;
2008
+ constructor(projectPath) {
2009
+ this.projectPath = projectPath;
2010
+ }
2011
+ async build(task, options = {}) {
2012
+ let prompt = `# Task: ${task.title}
2013
+
2014
+ `;
2015
+ const roleText = this.roleToText(task.assigneeRole);
2016
+ if (roleText) {
2017
+ prompt += `## Role
2018
+ You are acting as a ${roleText}.
2019
+
2020
+ `;
2021
+ }
2022
+ prompt += `## Description
2023
+ ${task.description || "No description provided."}
2024
+
2025
+ `;
2026
+ const projectConfig = this.getProjectConfig();
2027
+ if (projectConfig) {
2028
+ prompt += `## Project Metadata
2029
+ `;
2030
+ prompt += `- Version: ${projectConfig.version || "Unknown"}
2031
+ `;
2032
+ prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
2033
+
2034
+ `;
2035
+ }
2036
+ let serverContext = null;
2037
+ if (options.taskContext) {
2038
+ try {
2039
+ serverContext = JSON.parse(options.taskContext);
2040
+ } catch {
2041
+ serverContext = { context: options.taskContext };
2042
+ }
2043
+ }
2044
+ const contextPath = getLocusPath(this.projectPath, "contextFile");
2045
+ let hasLocalContext = false;
2046
+ if (import_node_fs4.existsSync(contextPath)) {
2047
+ try {
2048
+ const context = import_node_fs4.readFileSync(contextPath, "utf-8");
2049
+ if (context.trim().length > 20) {
2050
+ prompt += `## Project Context (Local)
2051
+ ${context}
2052
+
2053
+ `;
2054
+ hasLocalContext = true;
2055
+ }
2056
+ } catch (err) {
2057
+ console.warn(`Warning: Could not read context file: ${err}`);
2058
+ }
2059
+ }
2060
+ if (!hasLocalContext) {
2061
+ const fallback = this.getFallbackContext();
2062
+ if (fallback) {
2063
+ prompt += `## Project Context (README Fallback)
1678
2064
  ${fallback}
1679
2065
 
1680
2066
  `;
@@ -1683,11 +2069,12 @@ ${fallback}
1683
2069
  if (serverContext) {
1684
2070
  prompt += `## Project Context (Server)
1685
2071
  `;
1686
- if (serverContext.project) {
1687
- prompt += `- Project: ${serverContext.project.name || "Unknown"}
2072
+ const project = serverContext.project;
2073
+ if (project) {
2074
+ prompt += `- Project: ${project.name || "Unknown"}
1688
2075
  `;
1689
- if (!hasLocalContext && serverContext.project.techStack?.length) {
1690
- prompt += `- Tech Stack: ${serverContext.project.techStack.join(", ")}
2076
+ if (!hasLocalContext && project.techStack?.length) {
2077
+ prompt += `- Tech Stack: ${project.techStack.join(", ")}
1691
2078
  `;
1692
2079
  }
1693
2080
  }
@@ -1700,12 +2087,11 @@ ${serverContext.context}
1700
2087
  `;
1701
2088
  }
1702
2089
  prompt += this.getProjectStructure();
1703
- prompt += this.getSkillsInfo();
1704
2090
  prompt += `## Project Knowledge Base
1705
2091
  `;
1706
2092
  prompt += `You have access to the following documentation directories for context:
1707
2093
  `;
1708
- prompt += `- Artifacts: \`.locus/artifacts\`)
2094
+ prompt += `- Artifacts: \`.locus/artifacts\`
1709
2095
  `;
1710
2096
  prompt += `- Documents: \`.locus/documents\`
1711
2097
  `;
@@ -1763,11 +2149,9 @@ ${comment.text}
1763
2149
  }
1764
2150
  }
1765
2151
  prompt += `## Instructions
1766
- 1. Complete this task.
2152
+ 1. Complete this task.
1767
2153
  2. **Artifact Management**: If you create any high-level documentation (PRDs, technical drafts, architecture docs), you MUST save them in \`.locus/artifacts/\`. Do NOT create them in the root directory.
1768
- 3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
1769
- 4. When finished successfully, output: <promise>COMPLETE</promise>
1770
- `;
2154
+ 3. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
1771
2155
  return prompt;
1772
2156
  }
1773
2157
  async buildGenericPrompt(query) {
@@ -1814,7 +2198,6 @@ ${fallback}
1814
2198
  }
1815
2199
  }
1816
2200
  prompt += this.getProjectStructure();
1817
- prompt += this.getSkillsInfo();
1818
2201
  prompt += `## Project Knowledge Base
1819
2202
  `;
1820
2203
  prompt += `You have access to the following documentation directories for context:
@@ -1835,9 +2218,7 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1835
2218
  }
1836
2219
  prompt += `## Instructions
1837
2220
  1. Execute the prompt based on the provided project context.
1838
- 2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).
1839
- 3. When finished successfully, output: <promise>COMPLETE</promise>
1840
- `;
2221
+ 2. **Paths**: Use relative paths from the project root at all times. Do NOT use absolute local paths (e.g., /Users/...).`;
1841
2222
  return prompt;
1842
2223
  }
1843
2224
  getProjectConfig() {
@@ -1887,134 +2268,981 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1887
2268
  structure += `- \`${folder}/\`
1888
2269
  `;
1889
2270
  }
1890
- return `${structure}
1891
- `;
1892
- } catch {
1893
- return "";
2271
+ return `${structure}
2272
+ `;
2273
+ } catch {
2274
+ return "";
2275
+ }
2276
+ }
2277
+ roleToText(role) {
2278
+ if (!role) {
2279
+ return null;
2280
+ }
2281
+ switch (role) {
2282
+ case import_shared2.AssigneeRole.BACKEND:
2283
+ return "Backend Engineer";
2284
+ case import_shared2.AssigneeRole.FRONTEND:
2285
+ return "Frontend Engineer";
2286
+ case import_shared2.AssigneeRole.PM:
2287
+ return "Product Manager";
2288
+ case import_shared2.AssigneeRole.QA:
2289
+ return "QA Engineer";
2290
+ case import_shared2.AssigneeRole.DESIGN:
2291
+ return "Product Designer";
2292
+ default:
2293
+ return "engineer";
2294
+ }
2295
+ }
2296
+ }
2297
+
2298
+ // src/agent/task-executor.ts
2299
+ class TaskExecutor {
2300
+ deps;
2301
+ promptBuilder;
2302
+ constructor(deps) {
2303
+ this.deps = deps;
2304
+ this.promptBuilder = new PromptBuilder(deps.projectPath);
2305
+ }
2306
+ async execute(task) {
2307
+ this.deps.log(`Executing: ${task.title}`, "info");
2308
+ const basePrompt = await this.promptBuilder.build(task);
2309
+ try {
2310
+ this.deps.log("Starting Execution...", "info");
2311
+ await this.deps.aiRunner.run(basePrompt);
2312
+ return {
2313
+ success: true,
2314
+ summary: "Task completed by the agent"
2315
+ };
2316
+ } catch (error) {
2317
+ return { success: false, summary: `Error: ${error}` };
2318
+ }
2319
+ }
2320
+ }
2321
+
2322
+ // src/agent/worker.ts
2323
+ function resolveProvider(value) {
2324
+ if (!value || value.startsWith("--")) {
2325
+ console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
2326
+ return PROVIDER.CLAUDE;
2327
+ }
2328
+ if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
2329
+ return value;
2330
+ console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
2331
+ return PROVIDER.CLAUDE;
2332
+ }
2333
+
2334
+ class AgentWorker {
2335
+ config;
2336
+ client;
2337
+ aiRunner;
2338
+ taskExecutor;
2339
+ knowledgeBase;
2340
+ worktreeManager = null;
2341
+ prService = null;
2342
+ maxTasks = 50;
2343
+ tasksCompleted = 0;
2344
+ heartbeatInterval = null;
2345
+ currentTaskId = null;
2346
+ currentWorktreePath = null;
2347
+ postCleanupDelayMs = 5000;
2348
+ ghUsername = null;
2349
+ constructor(config) {
2350
+ this.config = config;
2351
+ const projectPath = config.projectPath || process.cwd();
2352
+ this.client = new LocusClient({
2353
+ baseUrl: config.apiBase,
2354
+ token: config.apiKey,
2355
+ retryOptions: {
2356
+ maxRetries: 3,
2357
+ initialDelay: 1000,
2358
+ maxDelay: 5000,
2359
+ factor: 2
2360
+ }
2361
+ });
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
+ }
2376
+ const provider = config.provider ?? PROVIDER.CLAUDE;
2377
+ this.aiRunner = createAiRunner(provider, {
2378
+ projectPath,
2379
+ model: config.model,
2380
+ log
2381
+ });
2382
+ this.taskExecutor = new TaskExecutor({
2383
+ aiRunner: this.aiRunner,
2384
+ projectPath,
2385
+ log
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
+ }
2396
+ const providerLabel = provider === "codex" ? "Codex" : "Claude";
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
+ }
2404
+ }
2405
+ log(message, level = "info") {
2406
+ const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
2407
+ const colorFn = {
2408
+ info: c.cyan,
2409
+ success: c.green,
2410
+ warn: c.yellow,
2411
+ error: c.red
2412
+ }[level];
2413
+ const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
2414
+ console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
2415
+ }
2416
+ async getActiveSprint() {
2417
+ try {
2418
+ if (this.config.sprintId) {
2419
+ return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
2420
+ }
2421
+ return await this.client.sprints.getActive(this.config.workspaceId);
2422
+ } catch (_error) {
2423
+ return null;
2424
+ }
2425
+ }
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
+ }
2488
+ try {
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");
2574
+ }
2575
+ this.currentWorktreePath = null;
2576
+ }
2577
+ async executeTask(task) {
2578
+ const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
2579
+ const { worktreePath, baseBranch, executor } = this.createTaskWorktree(fullTask);
2580
+ this.currentWorktreePath = worktreePath;
2581
+ let branchPushed = false;
2582
+ let keepBranch = false;
2583
+ let preserveWorktree = false;
2584
+ try {
2585
+ const result = await executor.execute(fullTask);
2586
+ let taskBranch = null;
2587
+ let prUrl = null;
2588
+ let prError = null;
2589
+ let noChanges = false;
2590
+ if (result.success && worktreePath) {
2591
+ const commitResult = this.commitAndPushWorktree(worktreePath, fullTask);
2592
+ taskBranch = commitResult.branch;
2593
+ branchPushed = commitResult.pushed;
2594
+ keepBranch = taskBranch !== null;
2595
+ noChanges = Boolean(commitResult.noChanges);
2596
+ if (commitResult.pushFailed) {
2597
+ preserveWorktree = true;
2598
+ prError = commitResult.pushError ?? "Git push failed before PR creation. Please retry manually.";
2599
+ this.log(`Preserving worktree after push failure: ${worktreePath}`, "warn");
2600
+ }
2601
+ if (branchPushed && taskBranch) {
2602
+ const prResult = this.createPullRequest(fullTask, taskBranch, result.summary, baseBranch ?? undefined);
2603
+ prUrl = prResult.url;
2604
+ prError = prResult.error ?? null;
2605
+ if (!prUrl) {
2606
+ preserveWorktree = true;
2607
+ this.log(`Preserving worktree for manual follow-up: ${worktreePath}`, "warn");
2608
+ }
2609
+ } else if (commitResult.skipReason) {
2610
+ this.log(`Skipping PR creation: ${commitResult.skipReason}`, "info");
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) {
2631
+ try {
2632
+ if (success) {
2633
+ this.knowledgeBase.updateProgress({
2634
+ type: "task_completed",
2635
+ title: task.title,
2636
+ details: `Agent: ${this.config.agentId.slice(-8)}`
2637
+ });
2638
+ this.log(`Updated progress.md: ${task.title}`, "info");
2639
+ }
2640
+ } catch (err) {
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;
2654
+ }
2655
+ }
2656
+ sendHeartbeat() {
2657
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, this.currentTaskId, this.currentTaskId ? "WORKING" : "IDLE").catch((err) => {
2658
+ this.log(`Heartbeat failed: ${err instanceof Error ? err.message : String(err)}`, "warn");
2659
+ });
2660
+ }
2661
+ async delayAfterCleanup() {
2662
+ if (!this.config.useWorktrees || this.postCleanupDelayMs <= 0)
2663
+ return;
2664
+ this.log(`Waiting ${Math.floor(this.postCleanupDelayMs / 1000)}s after worktree cleanup before next dispatch`, "info");
2665
+ await new Promise((resolve3) => setTimeout(resolve3, this.postCleanupDelayMs));
2666
+ }
2667
+ async run() {
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();
2679
+ const sprint = await this.getActiveSprint();
2680
+ if (sprint) {
2681
+ this.log(`Active sprint found: ${sprint.name}`, "info");
2682
+ } else {
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
+ }
1894
3058
  }
1895
3059
  }
1896
- getSkillsInfo() {
1897
- const projectSkillsDirs = [
1898
- LOCUS_CONFIG.agentSkillsDir,
1899
- ".cursor/skills",
1900
- ".claude/skills",
1901
- ".codex/skills",
1902
- ".gemini/skills"
1903
- ];
1904
- const globalHome = import_node_os2.homedir();
1905
- const globalSkillsDirs = [
1906
- import_node_path6.join(globalHome, ".cursor/skills"),
1907
- import_node_path6.join(globalHome, ".claude/skills"),
1908
- import_node_path6.join(globalHome, ".codex/skills"),
1909
- import_node_path6.join(globalHome, ".gemini/skills")
1910
- ];
1911
- const allSkillNames = new Set;
1912
- for (const relativePath of projectSkillsDirs) {
1913
- const fullPath = import_node_path6.join(this.projectPath, relativePath);
1914
- this.scanSkillsInDirectory(fullPath, allSkillNames);
1915
- }
1916
- for (const fullPath of globalSkillsDirs) {
1917
- this.scanSkillsInDirectory(fullPath, allSkillNames);
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
+ }
1918
3070
  }
1919
- const uniqueSkills = Array.from(allSkillNames).sort();
1920
- if (uniqueSkills.length === 0)
1921
- return "";
1922
- return `## Available Agent Skills
1923
- ` + `The project has the following specialized skills available (from project or global locations):
1924
- ` + uniqueSkills.map((s) => `- ${s}`).join(`
1925
- `) + `
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
+ }
1926
3082
 
1927
- `;
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);
1928
3090
  }
1929
- scanSkillsInDirectory(dirPath, skillSet) {
1930
- if (!import_node_fs4.existsSync(dirPath))
1931
- return;
3091
+ async reindex(force = false) {
1932
3092
  try {
1933
- const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
1934
- try {
1935
- return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
1936
- } catch {
1937
- return false;
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]);
1938
3111
  }
1939
- });
1940
- for (const entry of entries) {
1941
- skillSet.add(entry);
3112
+ return { symbols: {}, responsibilities: {}, lastIndexed: "" };
3113
+ }, force);
3114
+ if (index === null) {
3115
+ this.deps.log("No changes detected, skipping reindex", "info");
3116
+ return;
1942
3117
  }
1943
- } catch {}
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
+ }
1944
3123
  }
1945
- roleToText(role) {
1946
- if (!role) {
1947
- return null;
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 });
1948
3137
  }
1949
- switch (role) {
1950
- case import_shared2.AssigneeRole.BACKEND:
1951
- return "Backend Engineer";
1952
- case import_shared2.AssigneeRole.FRONTEND:
1953
- return "Frontend Engineer";
1954
- case import_shared2.AssigneeRole.PM:
1955
- return "Product Manager";
1956
- case import_shared2.AssigneeRole.QA:
1957
- return "QA Engineer";
1958
- case import_shared2.AssigneeRole.DESIGN:
1959
- return "Product Designer";
1960
- default:
1961
- return "engineer";
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;
1962
3166
  }
1963
3167
  }
1964
3168
  }
3169
+ // src/agent/review-service.ts
3170
+ var import_node_child_process6 = require("node:child_process");
1965
3171
 
1966
- // src/agent/task-executor.ts
1967
- class TaskExecutor {
3172
+ class ReviewService {
1968
3173
  deps;
1969
- promptBuilder;
1970
3174
  constructor(deps) {
1971
3175
  this.deps = deps;
1972
- this.promptBuilder = new PromptBuilder(deps.projectPath);
1973
3176
  }
1974
- async execute(task, context) {
1975
- this.deps.log(`Executing: ${task.title}`, "info");
1976
- const basePrompt = await this.promptBuilder.build(task, {
1977
- taskContext: context
1978
- });
3177
+ async reviewStagedChanges(sprint) {
3178
+ const { projectPath, log } = this.deps;
1979
3179
  try {
1980
- this.deps.log("Starting Execution...", "info");
1981
- const executionPrompt = `${basePrompt}
1982
-
1983
- When finished, output: <promise>COMPLETE</promise>`;
1984
- const output = await this.deps.aiRunner.run(executionPrompt);
1985
- const success = output.includes("<promise>COMPLETE</promise>");
1986
- return {
1987
- success,
1988
- summary: success ? "Task completed by the agent" : "The agent did not signal completion"
1989
- };
1990
- } catch (error) {
1991
- return { success: false, summary: `Error: ${error}` };
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;
1992
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;
1993
3225
  }
1994
3226
  }
1995
-
1996
- // src/agent/worker.ts
1997
- function resolveProvider(value) {
1998
- if (!value || value.startsWith("--")) {
1999
- console.warn("Warning: --provider requires a value. Falling back to 'claude'.");
3227
+ // src/agent/reviewer-worker.ts
3228
+ function resolveProvider2(value) {
3229
+ if (!value || value.startsWith("--"))
2000
3230
  return PROVIDER.CLAUDE;
2001
- }
2002
3231
  if (value === PROVIDER.CLAUDE || value === PROVIDER.CODEX)
2003
3232
  return value;
2004
- console.warn(`Warning: invalid --provider value '${value}'. Falling back to 'claude'.`);
2005
3233
  return PROVIDER.CLAUDE;
2006
3234
  }
2007
3235
 
2008
- class AgentWorker {
3236
+ class ReviewerWorker {
2009
3237
  config;
2010
3238
  client;
2011
3239
  aiRunner;
2012
- indexerService;
2013
- documentFetcher;
2014
- taskExecutor;
2015
- reviewService;
2016
- maxTasks = 50;
2017
- tasksCompleted = 0;
3240
+ prService;
3241
+ knowledgeBase;
3242
+ heartbeatInterval = null;
3243
+ currentTaskId = null;
3244
+ maxReviews = 50;
3245
+ reviewsCompleted = 0;
2018
3246
  constructor(config) {
2019
3247
  this.config = config;
2020
3248
  const projectPath = config.projectPath || process.cwd();
@@ -2035,29 +3263,10 @@ class AgentWorker {
2035
3263
  model: config.model,
2036
3264
  log
2037
3265
  });
2038
- this.indexerService = new CodebaseIndexerService({
2039
- aiRunner: this.aiRunner,
2040
- projectPath,
2041
- log
2042
- });
2043
- this.documentFetcher = new DocumentFetcher({
2044
- client: this.client,
2045
- workspaceId: config.workspaceId,
2046
- projectPath,
2047
- log
2048
- });
2049
- this.taskExecutor = new TaskExecutor({
2050
- aiRunner: this.aiRunner,
2051
- projectPath,
2052
- log
2053
- });
2054
- this.reviewService = new ReviewService({
2055
- aiRunner: this.aiRunner,
2056
- projectPath,
2057
- log
2058
- });
3266
+ this.prService = new PrService(projectPath, log);
3267
+ this.knowledgeBase = new KnowledgeBase(projectPath);
2059
3268
  const providerLabel = provider === "codex" ? "Codex" : "Claude";
2060
- this.log(`Using ${providerLabel} CLI for all phases`, "info");
3269
+ this.log(`Reviewer agent using ${providerLabel} CLI`, "info");
2061
3270
  }
2062
3271
  log(message, level = "info") {
2063
3272
  const timestamp = new Date().toISOString().split("T")[1]?.slice(0, 8) ?? "";
@@ -2068,109 +3277,131 @@ class AgentWorker {
2068
3277
  error: c.red
2069
3278
  }[level];
2070
3279
  const prefix = { info: "ℹ", success: "✓", warn: "⚠", error: "✗" }[level];
2071
- console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
2072
- }
2073
- async getActiveSprint() {
2074
- try {
2075
- if (this.config.sprintId) {
2076
- return await this.client.sprints.getById(this.config.sprintId, this.config.workspaceId);
2077
- }
2078
- return await this.client.sprints.getActive(this.config.workspaceId);
2079
- } catch (_error) {
2080
- return null;
2081
- }
3280
+ console.log(`${c.dim(`[${timestamp}]`)} ${c.bold(`[R:${this.config.agentId.slice(-8)}]`)} ${colorFn(`${prefix} ${message}`)}`);
2082
3281
  }
2083
- async getNextTask() {
2084
- try {
2085
- const task = await this.client.workspaces.dispatch(this.config.workspaceId, this.config.agentId, this.config.sprintId);
2086
- return task;
2087
- } catch (error) {
2088
- this.log(`No task dispatched: ${error instanceof Error ? error.message : String(error)}`, "info");
2089
- return null;
2090
- }
3282
+ getNextUnreviewedPr() {
3283
+ const prs = this.prService.listUnreviewedLocusPrs();
3284
+ return prs.length > 0 ? prs[0] : null;
2091
3285
  }
2092
- async executeTask(task) {
2093
- const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
2094
- let context = "";
3286
+ async reviewPr(pr) {
3287
+ const prNumber = String(pr.number);
3288
+ this.log(`Reviewing PR #${prNumber}: ${pr.title}`, "info");
3289
+ let diff;
2095
3290
  try {
2096
- context = await this.client.tasks.getContext(task.id, this.config.workspaceId);
3291
+ diff = this.prService.getPrDiff(prNumber);
2097
3292
  } catch (err) {
2098
- this.log(`Failed to fetch task context: ${err}`, "warn");
3293
+ return {
3294
+ reviewed: false,
3295
+ approved: false,
3296
+ summary: `Failed to get PR diff: ${err instanceof Error ? err.message : String(err)}`
3297
+ };
2099
3298
  }
2100
- const result = await this.taskExecutor.execute(fullTask, context);
2101
- await this.indexerService.reindex();
2102
- return result;
2103
- }
2104
- async runStagedChangesReview(sprint) {
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();
2105
3330
  try {
2106
- const report = await this.reviewService.reviewStagedChanges(sprint);
2107
- if (report) {
2108
- const reviewsDir = import_node_path7.join(this.config.projectPath, LOCUS_CONFIG.dir, "reviews");
2109
- if (!import_node_fs5.existsSync(reviewsDir)) {
2110
- import_node_fs5.mkdirSync(reviewsDir, { recursive: true });
2111
- }
2112
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2113
- const sprintSlug = sprint?.name ? sprint.name.toLowerCase().replace(/\s+/g, "-").slice(0, 40) : "no-sprint";
2114
- const fileName = `review-${sprintSlug}-${timestamp}.md`;
2115
- const filePath = import_node_path7.join(reviewsDir, fileName);
2116
- import_node_fs5.writeFileSync(filePath, report);
2117
- this.log(`Review report saved to .locus/reviews/${fileName}`, "success");
2118
- } else {
2119
- this.log("No staged changes to review.", "info");
2120
- }
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");
2121
3337
  } catch (err) {
2122
- this.log(`Review failed: ${err instanceof Error ? err.message : String(err)}`, "error");
3338
+ this.log(`Failed to post PR review: ${err instanceof Error ? err.message : String(err)}`, "error");
2123
3339
  }
3340
+ return { reviewed: true, approved, summary };
2124
3341
  }
2125
- async run() {
2126
- this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
2127
- const sprint = await this.getActiveSprint();
2128
- if (sprint) {
2129
- this.log(`Active sprint found: ${sprint.name}`, "info");
2130
- } else {
2131
- this.log("No active sprint found.", "warn");
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;
2132
3350
  }
2133
- while (this.tasksCompleted < this.maxTasks) {
2134
- const task = await this.getNextTask();
2135
- if (!task) {
2136
- this.log("No tasks remaining. Running review on staged changes...", "info");
2137
- await this.runStagedChangesReview(sprint);
2138
- break;
2139
- }
2140
- this.log(`Claimed: ${task.title}`, "success");
2141
- const result = await this.executeTask(task);
2142
- try {
2143
- await this.documentFetcher.fetch();
2144
- } catch (err) {
2145
- this.log(`Document fetch failed: ${err}`, "error");
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);
3362
+ }
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));
3377
+ continue;
2146
3378
  }
2147
- if (result.success) {
2148
- this.log(`Completed: ${task.title}`, "success");
2149
- await this.client.tasks.update(task.id, this.config.workspaceId, {
2150
- status: "VERIFICATION"
2151
- });
2152
- await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2153
- author: this.config.agentId,
2154
- text: `✅ ${result.summary}`
2155
- });
2156
- this.tasksCompleted++;
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++;
2157
3392
  } else {
2158
- this.log(`Failed: ${task.title} - ${result.summary}`, "error");
2159
- await this.client.tasks.update(task.id, this.config.workspaceId, {
2160
- status: "BACKLOG",
2161
- assignedTo: null
2162
- });
2163
- await this.client.tasks.addComment(task.id, this.config.workspaceId, {
2164
- author: this.config.agentId,
2165
- text: `❌ ${result.summary}`
2166
- });
3393
+ this.log(`Review skipped: ${result.summary}`, "warn");
2167
3394
  }
3395
+ this.currentTaskId = null;
2168
3396
  }
3397
+ this.stopHeartbeat();
3398
+ this.client.workspaces.heartbeat(this.config.workspaceId, this.config.agentId, null, "COMPLETED").catch(() => {});
2169
3399
  process.exit(0);
2170
3400
  }
2171
3401
  }
2172
- if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("worker")) {
2173
- 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";
2174
3405
  const args = process.argv.slice(2);
2175
3406
  const config = {};
2176
3407
  for (let i = 0;i < args.length; i++) {
@@ -2193,60 +3424,19 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
2193
3424
  const value = args[i + 1];
2194
3425
  if (value && !value.startsWith("--"))
2195
3426
  i++;
2196
- config.provider = resolveProvider(value);
3427
+ config.provider = resolveProvider2(value);
2197
3428
  }
2198
3429
  }
2199
3430
  if (!config.agentId || !config.workspaceId || !config.apiBase || !config.apiKey || !config.projectPath) {
2200
3431
  console.error("Missing required arguments");
2201
3432
  process.exit(1);
2202
3433
  }
2203
- const worker = new AgentWorker(config);
3434
+ const worker = new ReviewerWorker(config);
2204
3435
  worker.run().catch((err) => {
2205
- console.error("Fatal worker error:", err);
3436
+ console.error("Fatal reviewer error:", err);
2206
3437
  process.exit(1);
2207
3438
  });
2208
3439
  }
2209
-
2210
- // src/index-node.ts
2211
- var exports_index_node = {};
2212
- __export(exports_index_node, {
2213
- getLocusPath: () => getLocusPath,
2214
- getAgentArtifactsPath: () => getAgentArtifactsPath,
2215
- createAiRunner: () => createAiRunner,
2216
- c: () => c,
2217
- WorkspacesModule: () => WorkspacesModule,
2218
- TasksModule: () => TasksModule,
2219
- TaskExecutor: () => TaskExecutor,
2220
- SprintsModule: () => SprintsModule,
2221
- ReviewService: () => ReviewService,
2222
- PromptBuilder: () => PromptBuilder,
2223
- PROVIDER: () => PROVIDER,
2224
- OrganizationsModule: () => OrganizationsModule,
2225
- LocusEvent: () => LocusEvent,
2226
- LocusEmitter: () => LocusEmitter,
2227
- LocusClient: () => LocusClient,
2228
- LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
2229
- LOCUS_CONFIG: () => LOCUS_CONFIG,
2230
- InvitationsModule: () => InvitationsModule,
2231
- HistoryManager: () => HistoryManager,
2232
- ExecSession: () => ExecSession,
2233
- ExecEventType: () => ExecEventType,
2234
- ExecEventEmitter: () => ExecEventEmitter,
2235
- DocumentFetcher: () => DocumentFetcher,
2236
- DocsModule: () => DocsModule,
2237
- DEFAULT_MODEL: () => DEFAULT_MODEL,
2238
- ContextTracker: () => ContextTracker,
2239
- CodexRunner: () => CodexRunner,
2240
- CodebaseIndexerService: () => CodebaseIndexerService,
2241
- CodebaseIndexer: () => CodebaseIndexer,
2242
- ClaudeRunner: () => ClaudeRunner,
2243
- CiModule: () => CiModule,
2244
- AuthModule: () => AuthModule,
2245
- AgentWorker: () => AgentWorker,
2246
- AgentOrchestrator: () => AgentOrchestrator,
2247
- AIModule: () => AIModule
2248
- });
2249
- module.exports = __toCommonJS(exports_index_node);
2250
3440
  // src/exec/context-tracker.ts
2251
3441
  var REFERENCE_ALIASES = {
2252
3442
  plan: ["the plan", "sprint plan", "project plan", "implementation plan"],
@@ -2660,8 +3850,8 @@ class ExecEventEmitter {
2660
3850
  }
2661
3851
  }
2662
3852
  // src/exec/history-manager.ts
2663
- var import_node_fs6 = require("node:fs");
2664
- var import_node_path8 = require("node:path");
3853
+ var import_node_fs7 = require("node:fs");
3854
+ var import_node_path9 = require("node:path");
2665
3855
  var DEFAULT_MAX_SESSIONS = 30;
2666
3856
  function generateSessionId2() {
2667
3857
  const timestamp = Date.now().toString(36);
@@ -2673,30 +3863,30 @@ class HistoryManager {
2673
3863
  historyDir;
2674
3864
  maxSessions;
2675
3865
  constructor(projectPath, options) {
2676
- this.historyDir = options?.historyDir ?? import_node_path8.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
3866
+ this.historyDir = options?.historyDir ?? import_node_path9.join(projectPath, LOCUS_CONFIG.dir, LOCUS_CONFIG.sessionsDir);
2677
3867
  this.maxSessions = options?.maxSessions ?? DEFAULT_MAX_SESSIONS;
2678
3868
  this.ensureHistoryDir();
2679
3869
  }
2680
3870
  ensureHistoryDir() {
2681
- if (!import_node_fs6.existsSync(this.historyDir)) {
2682
- import_node_fs6.mkdirSync(this.historyDir, { recursive: true });
3871
+ if (!import_node_fs7.existsSync(this.historyDir)) {
3872
+ import_node_fs7.mkdirSync(this.historyDir, { recursive: true });
2683
3873
  }
2684
3874
  }
2685
3875
  getSessionPath(sessionId) {
2686
- return import_node_path8.join(this.historyDir, `${sessionId}.json`);
3876
+ return import_node_path9.join(this.historyDir, `${sessionId}.json`);
2687
3877
  }
2688
3878
  saveSession(session) {
2689
3879
  const filePath = this.getSessionPath(session.id);
2690
3880
  session.updatedAt = Date.now();
2691
- import_node_fs6.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
3881
+ import_node_fs7.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
2692
3882
  }
2693
3883
  loadSession(sessionId) {
2694
3884
  const filePath = this.getSessionPath(sessionId);
2695
- if (!import_node_fs6.existsSync(filePath)) {
3885
+ if (!import_node_fs7.existsSync(filePath)) {
2696
3886
  return null;
2697
3887
  }
2698
3888
  try {
2699
- const content = import_node_fs6.readFileSync(filePath, "utf-8");
3889
+ const content = import_node_fs7.readFileSync(filePath, "utf-8");
2700
3890
  return JSON.parse(content);
2701
3891
  } catch {
2702
3892
  return null;
@@ -2704,18 +3894,18 @@ class HistoryManager {
2704
3894
  }
2705
3895
  deleteSession(sessionId) {
2706
3896
  const filePath = this.getSessionPath(sessionId);
2707
- if (!import_node_fs6.existsSync(filePath)) {
3897
+ if (!import_node_fs7.existsSync(filePath)) {
2708
3898
  return false;
2709
3899
  }
2710
3900
  try {
2711
- import_node_fs6.rmSync(filePath);
3901
+ import_node_fs7.rmSync(filePath);
2712
3902
  return true;
2713
3903
  } catch {
2714
3904
  return false;
2715
3905
  }
2716
3906
  }
2717
3907
  listSessions(options) {
2718
- const files = import_node_fs6.readdirSync(this.historyDir);
3908
+ const files = import_node_fs7.readdirSync(this.historyDir);
2719
3909
  let sessions = [];
2720
3910
  for (const file of files) {
2721
3911
  if (file.endsWith(".json")) {
@@ -2788,11 +3978,11 @@ class HistoryManager {
2788
3978
  return deleted;
2789
3979
  }
2790
3980
  getSessionCount() {
2791
- const files = import_node_fs6.readdirSync(this.historyDir);
3981
+ const files = import_node_fs7.readdirSync(this.historyDir);
2792
3982
  return files.filter((f) => f.endsWith(".json")).length;
2793
3983
  }
2794
3984
  sessionExists(sessionId) {
2795
- return import_node_fs6.existsSync(this.getSessionPath(sessionId));
3985
+ return import_node_fs7.existsSync(this.getSessionPath(sessionId));
2796
3986
  }
2797
3987
  findSessionByPartialId(partialId) {
2798
3988
  const sessions = this.listSessions();
@@ -2806,12 +3996,12 @@ class HistoryManager {
2806
3996
  return this.historyDir;
2807
3997
  }
2808
3998
  clearAllSessions() {
2809
- const files = import_node_fs6.readdirSync(this.historyDir);
3999
+ const files = import_node_fs7.readdirSync(this.historyDir);
2810
4000
  let deleted = 0;
2811
4001
  for (const file of files) {
2812
4002
  if (file.endsWith(".json")) {
2813
4003
  try {
2814
- import_node_fs6.rmSync(import_node_path8.join(this.historyDir, file));
4004
+ import_node_fs7.rmSync(import_node_path9.join(this.historyDir, file));
2815
4005
  deleted++;
2816
4006
  } catch {}
2817
4007
  }
@@ -3076,12 +4266,14 @@ ${currentPrompt}`);
3076
4266
  }
3077
4267
  }
3078
4268
  // src/orchestrator.ts
3079
- var import_node_child_process4 = require("node:child_process");
3080
- var import_node_fs7 = require("node:fs");
3081
- var import_node_path9 = 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");
3082
4272
  var import_node_url = require("node:url");
3083
- var import_shared3 = require("@locusai/shared");
4273
+ var import_shared4 = require("@locusai/shared");
3084
4274
  var import_events4 = require("events");
4275
+ var MAX_AGENTS = 5;
4276
+
3085
4277
  class AgentOrchestrator extends import_events4.EventEmitter {
3086
4278
  client;
3087
4279
  config;
@@ -3089,6 +4281,8 @@ class AgentOrchestrator extends import_events4.EventEmitter {
3089
4281
  isRunning = false;
3090
4282
  processedTasks = new Set;
3091
4283
  resolvedSprintId = null;
4284
+ worktreeManager = null;
4285
+ heartbeatInterval = null;
3092
4286
  constructor(config) {
3093
4287
  super();
3094
4288
  this.config = config;
@@ -3097,6 +4291,15 @@ class AgentOrchestrator extends import_events4.EventEmitter {
3097
4291
  token: config.apiKey
3098
4292
  });
3099
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
+ }
3100
4303
  async resolveSprintId() {
3101
4304
  if (this.config.sprintId) {
3102
4305
  return this.config.sprintId;
@@ -3140,6 +4343,12 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
3140
4343
  if (this.resolvedSprintId) {
3141
4344
  console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
3142
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
+ }
3143
4352
  console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
3144
4353
  console.log(c.dim(`----------------------------------------------
3145
4354
  `));
@@ -3148,9 +4357,30 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
3148
4357
  console.log(c.dim("ℹ No available tasks found in the backlog."));
3149
4358
  return;
3150
4359
  }
3151
- 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);
3152
4383
  while (this.agents.size > 0 && this.isRunning) {
3153
- await this.reapAgents();
3154
4384
  if (this.agents.size === 0) {
3155
4385
  break;
3156
4386
  }
@@ -3159,8 +4389,8 @@ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
3159
4389
  console.log(`
3160
4390
  ${c.success("✅ Orchestrator finished")}`);
3161
4391
  }
3162
- async spawnAgent() {
3163
- 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)}`;
3164
4394
  const agentState = {
3165
4395
  id: agentId,
3166
4396
  status: "IDLE",
@@ -3172,13 +4402,9 @@ ${c.success("✅ Orchestrator finished")}`);
3172
4402
  this.agents.set(agentId, agentState);
3173
4403
  console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
3174
4404
  `);
3175
- const potentialPaths = [];
3176
- const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
3177
- const currentModuleDir = import_node_path9.dirname(currentModulePath);
3178
- potentialPaths.push(import_node_path9.join(currentModuleDir, "agent", "worker.js"), import_node_path9.join(currentModuleDir, "worker.js"), import_node_path9.join(currentModuleDir, "agent", "worker.ts"));
3179
- const workerPath = potentialPaths.find((p) => import_node_fs7.existsSync(p));
4405
+ const workerPath = this.resolveWorkerPath();
3180
4406
  if (!workerPath) {
3181
- 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.");
3182
4408
  }
3183
4409
  const workerArgs = [
3184
4410
  "--agent-id",
@@ -3201,8 +4427,15 @@ ${c.success("✅ Orchestrator finished")}`);
3201
4427
  if (this.resolvedSprintId) {
3202
4428
  workerArgs.push("--sprint-id", this.resolvedSprintId);
3203
4429
  }
3204
- const agentProcess = import_node_child_process4.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], {
3205
4437
  stdio: ["pipe", "pipe", "pipe"],
4438
+ detached: true,
3206
4439
  env: {
3207
4440
  ...process.env,
3208
4441
  FORCE_COLOR: "1",
@@ -3217,6 +4450,9 @@ ${c.success("✅ Orchestrator finished")}`);
3217
4450
  agentState.tasksCompleted = msg.tasksCompleted || 0;
3218
4451
  agentState.tasksFailed = msg.tasksFailed || 0;
3219
4452
  }
4453
+ if (msg.type === "heartbeat") {
4454
+ agentState.lastHeartbeat = new Date;
4455
+ }
3220
4456
  });
3221
4457
  agentProcess.stdout?.on("data", (data) => {
3222
4458
  process.stdout.write(data.toString());
@@ -3241,7 +4477,30 @@ ${agentId} finished (exit code: ${code})`);
3241
4477
  });
3242
4478
  this.emit("agent:spawned", { agentId });
3243
4479
  }
3244
- 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
+ }
3245
4504
  async getAvailableTasks() {
3246
4505
  try {
3247
4506
  const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
@@ -3258,10 +4517,10 @@ ${agentId} finished (exit code: ${code})`);
3258
4517
  try {
3259
4518
  const tasks2 = await this.getAvailableTasks();
3260
4519
  const priorityOrder = [
3261
- import_shared3.TaskPriority.CRITICAL,
3262
- import_shared3.TaskPriority.HIGH,
3263
- import_shared3.TaskPriority.MEDIUM,
3264
- import_shared3.TaskPriority.LOW
4520
+ import_shared4.TaskPriority.CRITICAL,
4521
+ import_shared4.TaskPriority.HIGH,
4522
+ import_shared4.TaskPriority.MEDIUM,
4523
+ import_shared4.TaskPriority.LOW
3265
4524
  ];
3266
4525
  let task = tasks2.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority))[0];
3267
4526
  if (!task && tasks2.length > 0) {
@@ -3285,7 +4544,7 @@ ${agentId} finished (exit code: ${code})`);
3285
4544
  async completeTask(taskId, agentId, summary) {
3286
4545
  try {
3287
4546
  await this.client.tasks.update(taskId, this.config.workspaceId, {
3288
- status: import_shared3.TaskStatus.VERIFICATION
4547
+ status: import_shared4.TaskStatus.IN_REVIEW
3289
4548
  });
3290
4549
  if (summary) {
3291
4550
  await this.client.tasks.addComment(taskId, this.config.workspaceId, {
@@ -3310,7 +4569,7 @@ ${summary}`
3310
4569
  async failTask(taskId, agentId, error) {
3311
4570
  try {
3312
4571
  await this.client.tasks.update(taskId, this.config.workspaceId, {
3313
- status: import_shared3.TaskStatus.BACKLOG,
4572
+ status: import_shared4.TaskStatus.BACKLOG,
3314
4573
  assignedTo: null
3315
4574
  });
3316
4575
  await this.client.tasks.addComment(taskId, this.config.workspaceId, {
@@ -3333,11 +4592,52 @@ ${summary}`
3333
4592
  await this.cleanup();
3334
4593
  this.emit("stopped", { timestamp: new Date });
3335
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
+ }
3336
4615
  async cleanup() {
4616
+ if (this.heartbeatInterval) {
4617
+ clearInterval(this.heartbeatInterval);
4618
+ this.heartbeatInterval = null;
4619
+ }
3337
4620
  for (const [agentId, agent] of this.agents.entries()) {
3338
4621
  if (agent.process && !agent.process.killed) {
3339
4622
  console.log(`Killing agent: ${agentId}`);
3340
- 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"));
3341
4641
  }
3342
4642
  }
3343
4643
  this.agents.clear();
@@ -3345,12 +4645,543 @@ ${summary}`
3345
4645
  getStats() {
3346
4646
  return {
3347
4647
  activeAgents: this.agents.size,
4648
+ agentCount: this.agentCount,
4649
+ useWorktrees: this.useWorktrees,
3348
4650
  processedTasks: this.processedTasks.size,
3349
4651
  totalTasksCompleted: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksCompleted, 0),
3350
4652
  totalTasksFailed: Array.from(this.agents.values()).reduce((sum, agent) => sum + agent.tasksFailed, 0)
3351
4653
  };
3352
4654
  }
4655
+ getAgentStates() {
4656
+ return Array.from(this.agents.values());
4657
+ }
3353
4658
  sleep(ms) {
3354
- 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
+ }
3355
5186
  }
3356
5187
  }