@locusai/sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,7 +52,8 @@ __export(exports_src, {
52
52
  InvitationsModule: () => InvitationsModule,
53
53
  DocsModule: () => DocsModule,
54
54
  CiModule: () => CiModule,
55
- AuthModule: () => AuthModule
55
+ AuthModule: () => AuthModule,
56
+ AIModule: () => AIModule
56
57
  });
57
58
  module.exports = __toCommonJS(exports_src);
58
59
  var import_axios = __toESM(require("axios"));
@@ -85,6 +86,28 @@ class BaseModule {
85
86
  }
86
87
  }
87
88
 
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);
93
+ return data;
94
+ }
95
+ async listSessions(workspaceId) {
96
+ const { data } = await this.api.get(`/ai/${workspaceId}/sessions`);
97
+ return data;
98
+ }
99
+ async getSession(workspaceId, sessionId) {
100
+ const { data } = await this.api.get(`/ai/${workspaceId}/session/${sessionId}`);
101
+ return data;
102
+ }
103
+ getChatStreamUrl(workspaceId, sessionId) {
104
+ return `${this.api.defaults.baseURL}/ai/${workspaceId}/chat/stream?sessionId=${sessionId}`;
105
+ }
106
+ async deleteSession(workspaceId, sessionId) {
107
+ await this.api.delete(`/ai/${workspaceId}/session/${sessionId}`);
108
+ }
109
+ }
110
+
88
111
  // src/modules/auth.ts
89
112
  class AuthModule extends BaseModule {
90
113
  async getMe() {
@@ -248,6 +271,10 @@ class SprintsModule extends BaseModule {
248
271
  const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints/${id}/complete`);
249
272
  return data.sprint;
250
273
  }
274
+ async triggerAIPlanning(id, workspaceId) {
275
+ const { data } = await this.api.post(`/workspaces/${workspaceId}/sprints/${id}/trigger-ai-planning`);
276
+ return data.sprint;
277
+ }
251
278
  }
252
279
 
253
280
  // src/modules/tasks.ts
@@ -294,6 +321,16 @@ class TasksModule extends BaseModule {
294
321
  const { data } = await this.api.post(`/workspaces/${workspaceId}/tasks/${id}/comment`, body);
295
322
  return data.comment;
296
323
  }
324
+ async batchUpdate(ids, workspaceId, updates) {
325
+ await this.api.patch(`/workspaces/${workspaceId}/tasks/batch`, {
326
+ ids,
327
+ updates
328
+ });
329
+ }
330
+ async getContext(id, workspaceId) {
331
+ const { data } = await this.api.get(`/workspaces/${workspaceId}/tasks/${id}/context`);
332
+ return data;
333
+ }
297
334
  }
298
335
 
299
336
  // src/modules/workspaces.ts
@@ -347,6 +384,7 @@ class LocusClient {
347
384
  api;
348
385
  emitter;
349
386
  auth;
387
+ ai;
350
388
  tasks;
351
389
  sprints;
352
390
  workspaces;
@@ -358,7 +396,7 @@ class LocusClient {
358
396
  this.emitter = new LocusEmitter;
359
397
  this.api = import_axios.default.create({
360
398
  baseURL: config.baseUrl,
361
- timeout: config.timeout || 1e4,
399
+ timeout: config.timeout || 60000,
362
400
  headers: {
363
401
  "Content-Type": "application/json",
364
402
  ...config.token ? { Authorization: `Bearer ${config.token}` } : {}
@@ -366,6 +404,7 @@ class LocusClient {
366
404
  });
367
405
  this.setupInterceptors();
368
406
  this.auth = new AuthModule(this.api, this.emitter);
407
+ this.ai = new AIModule(this.api, this.emitter);
369
408
  this.tasks = new TasksModule(this.api, this.emitter);
370
409
  this.sprints = new SprintsModule(this.api, this.emitter);
371
410
  this.workspaces = new WorkspacesModule(this.api, this.emitter);
@@ -439,7 +478,6 @@ __export(exports_worker, {
439
478
  AgentWorker: () => AgentWorker
440
479
  });
441
480
  module.exports = __toCommonJS(exports_worker);
442
- var import_shared3 = require("@locusai/shared");
443
481
 
444
482
  // src/core/config.ts
445
483
  var import_node_path = require("node:path");
@@ -456,7 +494,9 @@ var LOCUS_CONFIG = {
456
494
  configFile: "config.json",
457
495
  indexFile: "codebase-index.json",
458
496
  contextFile: "CLAUDE.md",
459
- artifactsDir: "artifacts"
497
+ artifactsDir: "artifacts",
498
+ documentsDir: "documents",
499
+ agentSkillsDir: ".agent/skills"
460
500
  };
461
501
  function getLocusPath(projectPath, fileName) {
462
502
  if (fileName === "contextFile") {
@@ -623,12 +663,8 @@ class ClaudeRunner {
623
663
  return null;
624
664
  }
625
665
  handleEvent(event) {
626
- const { type, delta, content_block } = event;
627
- if (type === "content_block_delta" && delta) {
628
- if (delta.type === "text_delta" && delta.text) {
629
- this.log?.(delta.text, "info");
630
- }
631
- } else if (type === "content_block_start" && content_block) {
666
+ const { type, content_block } = event;
667
+ if (type === "content_block_start" && content_block) {
632
668
  if (content_block.type === "tool_use" && content_block.name) {
633
669
  this.log?.(`
634
670
 
@@ -715,7 +751,13 @@ class CodexRunner {
715
751
  });
716
752
  }
717
753
  buildArgs(outputPath) {
718
- const args = ["exec", "--full-auto", "--output-last-message", outputPath];
754
+ const args = [
755
+ "exec",
756
+ "--full-auto",
757
+ "--skip-git-repo-check",
758
+ "--output-last-message",
759
+ outputPath
760
+ ];
719
761
  if (this.model) {
720
762
  args.push("--model", this.model);
721
763
  }
@@ -781,6 +823,7 @@ function createAiRunner(provider, config) {
781
823
  // src/agent/artifact-syncer.ts
782
824
  var import_node_fs2 = require("node:fs");
783
825
  var import_node_path4 = require("node:path");
826
+ var import_shared2 = require("@locusai/shared");
784
827
  class ArtifactSyncer {
785
828
  deps;
786
829
  constructor(deps) {
@@ -812,10 +855,13 @@ class ArtifactSyncer {
812
855
  }
813
856
  try {
814
857
  const files = import_node_fs2.readdirSync(artifactsDir);
815
- if (files.length === 0)
816
- return;
817
858
  this.deps.log(`Syncing ${files.length} artifacts to server...`, "info");
818
- const artifactsGroupId = await this.getOrCreateArtifactsGroup();
859
+ const groups = await this.deps.client.docs.listGroups(this.deps.workspaceId);
860
+ const groupMap = new Map(groups.map((g) => [g.id, g.name]));
861
+ let artifactsGroupId = groups.find((g) => g.name === "Artifacts")?.id;
862
+ if (!artifactsGroupId) {
863
+ artifactsGroupId = await this.getOrCreateArtifactsGroup();
864
+ }
819
865
  const existingDocs = await this.deps.client.docs.list(this.deps.workspaceId);
820
866
  for (const file of files) {
821
867
  const filePath = import_node_path4.join(artifactsDir, file);
@@ -834,12 +880,35 @@ class ArtifactSyncer {
834
880
  await this.deps.client.docs.create(this.deps.workspaceId, {
835
881
  title,
836
882
  content,
837
- groupId: artifactsGroupId
883
+ groupId: artifactsGroupId,
884
+ type: import_shared2.DocType.GENERAL
838
885
  });
839
886
  this.deps.log(`Created artifact: ${file}`, "success");
840
887
  }
841
888
  }
842
889
  }
890
+ for (const doc of existingDocs) {
891
+ if (doc.groupId === artifactsGroupId) {
892
+ const fileName = `${doc.title}.md`;
893
+ const filePath = import_node_path4.join(artifactsDir, fileName);
894
+ if (!import_node_fs2.existsSync(filePath)) {
895
+ import_node_fs2.writeFileSync(filePath, doc.content || "");
896
+ this.deps.log(`Fetched artifact: ${fileName}`, "success");
897
+ }
898
+ } else {
899
+ const documentsDir = getLocusPath(this.deps.projectPath, "documentsDir");
900
+ const groupName = groupMap.get(doc.groupId || "") || "General";
901
+ const groupDir = import_node_path4.join(documentsDir, groupName);
902
+ if (!import_node_fs2.existsSync(groupDir)) {
903
+ import_node_fs2.mkdirSync(groupDir, { recursive: true });
904
+ }
905
+ const fileName = `${doc.title}.md`;
906
+ const filePath = import_node_path4.join(groupDir, fileName);
907
+ if (!import_node_fs2.existsSync(filePath) || import_node_fs2.readFileSync(filePath, "utf-8") !== doc.content) {
908
+ import_node_fs2.writeFileSync(filePath, doc.content || "");
909
+ }
910
+ }
911
+ }
843
912
  } catch (error) {
844
913
  this.deps.log(`Failed to sync artifacts: ${error}`, "error");
845
914
  }
@@ -847,6 +916,7 @@ class ArtifactSyncer {
847
916
  }
848
917
 
849
918
  // src/core/indexer.ts
919
+ var import_node_crypto2 = require("node:crypto");
850
920
  var import_node_fs3 = require("node:fs");
851
921
  var import_node_path5 = require("node:path");
852
922
  var import_globby = require("globby");
@@ -854,16 +924,64 @@ var import_globby = require("globby");
854
924
  class CodebaseIndexer {
855
925
  projectPath;
856
926
  indexPath;
927
+ fullReindexRatioThreshold = 0.2;
857
928
  constructor(projectPath) {
858
929
  this.projectPath = projectPath;
859
930
  this.indexPath = import_node_path5.join(projectPath, ".locus", "codebase-index.json");
860
931
  }
861
- async index(onProgress, treeSummarizer) {
932
+ async index(onProgress, treeSummarizer, force = false) {
862
933
  if (!treeSummarizer) {
863
934
  throw new Error("A treeSummarizer is required for this indexing method.");
864
935
  }
865
- if (onProgress)
866
- onProgress("Generating file tree...");
936
+ onProgress?.("Generating file tree...");
937
+ const currentFiles = await this.getFileTree();
938
+ const treeString = currentFiles.join(`
939
+ `);
940
+ const newTreeHash = this.hashTree(treeString);
941
+ const existingIndex = this.loadIndex();
942
+ if (!force && existingIndex?.treeHash === newTreeHash) {
943
+ onProgress?.("No file changes detected, skipping reindex");
944
+ return null;
945
+ }
946
+ const currentHashes = this.computeFileHashes(currentFiles);
947
+ const existingHashes = existingIndex?.fileHashes;
948
+ const canIncremental = !force && existingIndex && existingHashes;
949
+ if (canIncremental) {
950
+ onProgress?.("Performing incremental update");
951
+ const { added, deleted, modified } = this.diffFiles(currentHashes, existingHashes);
952
+ const changedFiles = [...added, ...modified];
953
+ const totalChanges = changedFiles.length + deleted.length;
954
+ const existingFileCount = Object.keys(existingHashes).length;
955
+ onProgress?.(`File changes detected: ${changedFiles.length} changed, ${added.length} added, ${deleted.length} deleted`);
956
+ if (existingFileCount > 0) {
957
+ const changeRatio = totalChanges / existingFileCount;
958
+ if (changeRatio <= this.fullReindexRatioThreshold && changedFiles.length > 0) {
959
+ onProgress?.(`Reindexing ${changedFiles.length} changed files and merging with existing index`);
960
+ const incrementalIndex = await treeSummarizer(changedFiles.join(`
961
+ `));
962
+ const updatedIndex = this.cloneIndex(existingIndex);
963
+ this.removeFilesFromIndex(updatedIndex, [...deleted, ...modified]);
964
+ return this.mergeIndex(updatedIndex, incrementalIndex, currentHashes, newTreeHash);
965
+ }
966
+ if (changedFiles.length === 0 && deleted.length > 0) {
967
+ onProgress?.(`Removing ${deleted.length} deleted files from index`);
968
+ const updatedIndex = this.cloneIndex(existingIndex);
969
+ this.removeFilesFromIndex(updatedIndex, deleted);
970
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
971
+ }
972
+ if (changedFiles.length === 0 && deleted.length === 0) {
973
+ onProgress?.("No actual file changes, updating hashes only");
974
+ const updatedIndex = this.cloneIndex(existingIndex);
975
+ return this.applyIndexMetadata(updatedIndex, currentHashes, newTreeHash);
976
+ }
977
+ onProgress?.(`Too many changes (${(changeRatio * 100).toFixed(1)}%), performing full reindex`);
978
+ }
979
+ }
980
+ onProgress?.("AI is analyzing codebase structure...");
981
+ const index = await treeSummarizer(treeString);
982
+ return this.applyIndexMetadata(index, currentHashes, newTreeHash);
983
+ }
984
+ async getFileTree() {
867
985
  const gitmodulesPath = import_node_path5.join(this.projectPath, ".gitmodules");
868
986
  const submoduleIgnores = [];
869
987
  if (import_node_fs3.existsSync(gitmodulesPath)) {
@@ -881,7 +999,7 @@ class CodebaseIndexer {
881
999
  }
882
1000
  } catch {}
883
1001
  }
884
- const files = await import_globby.globby(["**/*"], {
1002
+ return import_globby.globby(["**/*"], {
885
1003
  cwd: this.projectPath,
886
1004
  gitignore: true,
887
1005
  ignore: [
@@ -922,13 +1040,6 @@ class CodebaseIndexer {
922
1040
  "**/*.{png,jpg,jpeg,gif,svg,ico,mp4,webm,wav,mp3,woff,woff2,eot,ttf,otf,pdf,zip,tar.gz,rar}"
923
1041
  ]
924
1042
  });
925
- const treeString = files.join(`
926
- `);
927
- if (onProgress)
928
- onProgress("AI is analyzing codebase structure...");
929
- const index = await treeSummarizer(treeString);
930
- index.lastIndexed = new Date().toISOString();
931
- return index;
932
1043
  }
933
1044
  loadIndex() {
934
1045
  if (import_node_fs3.existsSync(this.indexPath)) {
@@ -947,6 +1058,79 @@ class CodebaseIndexer {
947
1058
  }
948
1059
  import_node_fs3.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
949
1060
  }
1061
+ cloneIndex(index) {
1062
+ return JSON.parse(JSON.stringify(index));
1063
+ }
1064
+ applyIndexMetadata(index, fileHashes, treeHash) {
1065
+ index.lastIndexed = new Date().toISOString();
1066
+ index.treeHash = treeHash;
1067
+ index.fileHashes = fileHashes;
1068
+ return index;
1069
+ }
1070
+ hashTree(tree) {
1071
+ return import_node_crypto2.createHash("sha256").update(tree).digest("hex");
1072
+ }
1073
+ hashFile(filePath) {
1074
+ try {
1075
+ const content = import_node_fs3.readFileSync(import_node_path5.join(this.projectPath, filePath), "utf-8");
1076
+ return import_node_crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
1077
+ } catch {
1078
+ return null;
1079
+ }
1080
+ }
1081
+ computeFileHashes(files) {
1082
+ const hashes = {};
1083
+ for (const file of files) {
1084
+ const hash = this.hashFile(file);
1085
+ if (hash !== null) {
1086
+ hashes[file] = hash;
1087
+ }
1088
+ }
1089
+ return hashes;
1090
+ }
1091
+ diffFiles(currentHashes, existingHashes) {
1092
+ const currentFiles = Object.keys(currentHashes);
1093
+ const existingFiles = Object.keys(existingHashes);
1094
+ const existingSet = new Set(existingFiles);
1095
+ const currentSet = new Set(currentFiles);
1096
+ const added = currentFiles.filter((f) => !existingSet.has(f));
1097
+ const deleted = existingFiles.filter((f) => !currentSet.has(f));
1098
+ const modified = currentFiles.filter((f) => existingSet.has(f) && currentHashes[f] !== existingHashes[f]);
1099
+ return { added, deleted, modified };
1100
+ }
1101
+ removeFilesFromIndex(index, files) {
1102
+ const fileSet = new Set(files);
1103
+ for (const file of files) {
1104
+ delete index.responsibilities[file];
1105
+ }
1106
+ for (const [symbol, paths] of Object.entries(index.symbols)) {
1107
+ index.symbols[symbol] = paths.filter((p) => !fileSet.has(p));
1108
+ if (index.symbols[symbol].length === 0) {
1109
+ delete index.symbols[symbol];
1110
+ }
1111
+ }
1112
+ }
1113
+ mergeIndex(existing, incremental, newHashes, newTreeHash) {
1114
+ const mergedSymbols = { ...existing.symbols };
1115
+ for (const [symbol, paths] of Object.entries(incremental.symbols)) {
1116
+ if (mergedSymbols[symbol]) {
1117
+ mergedSymbols[symbol] = [
1118
+ ...new Set([...mergedSymbols[symbol], ...paths])
1119
+ ];
1120
+ } else {
1121
+ mergedSymbols[symbol] = paths;
1122
+ }
1123
+ }
1124
+ const merged = {
1125
+ symbols: mergedSymbols,
1126
+ responsibilities: {
1127
+ ...existing.responsibilities,
1128
+ ...incremental.responsibilities
1129
+ },
1130
+ lastIndexed: ""
1131
+ };
1132
+ return this.applyIndexMetadata(merged, newHashes, newTreeHash);
1133
+ }
950
1134
  }
951
1135
 
952
1136
  // src/agent/codebase-indexer-service.ts
@@ -957,9 +1141,8 @@ class CodebaseIndexerService {
957
1141
  this.deps = deps;
958
1142
  this.indexer = new CodebaseIndexer(deps.projectPath);
959
1143
  }
960
- async reindex() {
1144
+ async reindex(force = false) {
961
1145
  try {
962
- this.deps.log("Reindexing codebase...", "info");
963
1146
  const index = await this.indexer.index((msg) => this.deps.log(msg, "info"), async (tree) => {
964
1147
  const prompt = `You are a codebase analysis expert. Analyze the file tree and extract:
965
1148
  1. Key symbols (classes, functions, types) and their locations
@@ -980,7 +1163,11 @@ Return ONLY valid JSON, no markdown formatting.`;
980
1163
  return JSON.parse(jsonMatch[0]);
981
1164
  }
982
1165
  return { symbols: {}, responsibilities: {}, lastIndexed: "" };
983
- });
1166
+ }, force);
1167
+ if (index === null) {
1168
+ this.deps.log("No changes detected, skipping reindex", "info");
1169
+ return;
1170
+ }
984
1171
  this.indexer.saveIndex(index);
985
1172
  this.deps.log("Codebase reindexed successfully", "success");
986
1173
  } catch (error) {
@@ -989,53 +1176,17 @@ Return ONLY valid JSON, no markdown formatting.`;
989
1176
  }
990
1177
  }
991
1178
 
992
- // src/agent/sprint-planner.ts
993
- class SprintPlanner {
994
- deps;
995
- constructor(deps) {
996
- this.deps = deps;
997
- }
998
- async planSprint(sprint, tasks2) {
999
- this.deps.log(`Planning sprint: ${sprint.name}`, "info");
1000
- try {
1001
- const taskList = tasks2.map((t) => `- [${t.id}] ${t.title}: ${t.description || "No description"}`).join(`
1002
- `);
1003
- const planningPrompt = `# Sprint Planning: ${sprint.name}
1004
-
1005
- You are an expert project manager and lead engineer. You need to create a mindmap and execution plan for the following tasks in this sprint.
1006
-
1007
- ## Tasks
1008
- ${taskList}
1009
-
1010
- ## Instructions
1011
- 1. Analyze dependencies between these tasks.
1012
- 2. Prioritize them for the most efficient execution.
1013
- 3. Create a mindmap (in Markdown or Mermaid format) that visualizes the sprint structure.
1014
- 4. Output your final plan. The plan should clearly state the order of execution.
1015
-
1016
- **IMPORTANT**:
1017
- - Do NOT create any files on the filesystem during this planning phase.
1018
- - Avoid using absolute local paths (e.g., /Users/...) in your output. Use relative paths starting from the project root if necessary.
1019
- - Your output will be saved as the official sprint mindmap on the server.`;
1020
- const plan = await this.deps.aiRunner.run(planningPrompt, true);
1021
- this.deps.log("Sprint mindmap generated and posted to server.", "success");
1022
- return plan;
1023
- } catch (error) {
1024
- this.deps.log(`Sprint planning failed: ${error}`, "error");
1025
- return sprint.mindmap || "";
1026
- }
1027
- }
1028
- }
1029
-
1030
1179
  // src/core/prompt-builder.ts
1031
1180
  var import_node_fs4 = require("node:fs");
1032
- var import_shared2 = require("@locusai/shared");
1181
+ var import_node_os2 = require("node:os");
1182
+ var import_node_path6 = require("node:path");
1183
+ var import_shared3 = require("@locusai/shared");
1033
1184
  class PromptBuilder {
1034
1185
  projectPath;
1035
1186
  constructor(projectPath) {
1036
1187
  this.projectPath = projectPath;
1037
1188
  }
1038
- async build(task) {
1189
+ async build(task, options = {}) {
1039
1190
  let prompt = `# Task: ${task.title}
1040
1191
 
1041
1192
  `;
@@ -1050,18 +1201,81 @@ You are acting as a ${roleText}.
1050
1201
  ${task.description || "No description provided."}
1051
1202
 
1052
1203
  `;
1204
+ const projectConfig = this.getProjectConfig();
1205
+ if (projectConfig) {
1206
+ prompt += `## Project Metadata
1207
+ `;
1208
+ prompt += `- Version: ${projectConfig.version || "Unknown"}
1209
+ `;
1210
+ prompt += `- Created At: ${projectConfig.createdAt || "Unknown"}
1211
+
1212
+ `;
1213
+ }
1214
+ let serverContext = null;
1215
+ if (options.taskContext) {
1216
+ try {
1217
+ serverContext = JSON.parse(options.taskContext);
1218
+ } catch {
1219
+ serverContext = { context: options.taskContext };
1220
+ }
1221
+ }
1053
1222
  const contextPath = getLocusPath(this.projectPath, "contextFile");
1223
+ let hasLocalContext = false;
1054
1224
  if (import_node_fs4.existsSync(contextPath)) {
1055
1225
  try {
1056
1226
  const context = import_node_fs4.readFileSync(contextPath, "utf-8");
1057
- prompt += `## Project Context (from CLAUDE.md)
1227
+ if (context.trim().length > 20) {
1228
+ prompt += `## Project Context (Local)
1058
1229
  ${context}
1059
1230
 
1060
1231
  `;
1232
+ hasLocalContext = true;
1233
+ }
1061
1234
  } catch (err) {
1062
1235
  console.warn(`Warning: Could not read context file: ${err}`);
1063
1236
  }
1064
1237
  }
1238
+ if (!hasLocalContext) {
1239
+ const fallback = this.getFallbackContext();
1240
+ if (fallback) {
1241
+ prompt += `## Project Context (README Fallback)
1242
+ ${fallback}
1243
+
1244
+ `;
1245
+ }
1246
+ }
1247
+ if (serverContext) {
1248
+ prompt += `## Project Context (Server)
1249
+ `;
1250
+ if (serverContext.project) {
1251
+ prompt += `- Project: ${serverContext.project.name || "Unknown"}
1252
+ `;
1253
+ if (!hasLocalContext && serverContext.project.techStack?.length) {
1254
+ prompt += `- Tech Stack: ${serverContext.project.techStack.join(", ")}
1255
+ `;
1256
+ }
1257
+ }
1258
+ if (serverContext.context) {
1259
+ prompt += `
1260
+ ${serverContext.context}
1261
+ `;
1262
+ }
1263
+ prompt += `
1264
+ `;
1265
+ }
1266
+ prompt += this.getProjectStructure();
1267
+ prompt += this.getSkillsInfo();
1268
+ prompt += `## Project Knowledge Base
1269
+ `;
1270
+ prompt += `You have access to the following documentation directories for context:
1271
+ `;
1272
+ prompt += `- Artifacts: \`.locus/artifacts\`
1273
+ `;
1274
+ prompt += `- Documents: \`.locus/documents\`
1275
+ `;
1276
+ prompt += `If you need more information about the project strategies, plans, or architecture, please read files in these directories.
1277
+
1278
+ `;
1065
1279
  const indexPath = getLocusPath(this.projectPath, "indexFile");
1066
1280
  if (import_node_fs4.existsSync(indexPath)) {
1067
1281
  prompt += `## Codebase Overview
@@ -1070,11 +1284,19 @@ There is an index file in the .locus/codebase-index.json and if you need you can
1070
1284
  `;
1071
1285
  }
1072
1286
  if (task.docs && task.docs.length > 0) {
1073
- prompt += `## Attached Documents
1287
+ prompt += `## Attached Documents (Summarized)
1288
+ `;
1289
+ prompt += `> Full content available on server. Rely on Task Description for specific requirements.
1290
+
1074
1291
  `;
1075
1292
  for (const doc of task.docs) {
1076
- prompt += `### ${doc.title}
1077
- ${doc.content || "(No content)"}
1293
+ const content = doc.content || "";
1294
+ const limit = 800;
1295
+ const preview = content.slice(0, limit);
1296
+ const isTruncated = content.length > limit;
1297
+ prompt += `### Doc: ${doc.title}
1298
+ ${preview}${isTruncated ? `
1299
+ ...(truncated)...` : ""}
1078
1300
 
1079
1301
  `;
1080
1302
  }
@@ -1090,7 +1312,7 @@ ${doc.content || "(No content)"}
1090
1312
  `;
1091
1313
  }
1092
1314
  if (task.comments && task.comments.length > 0) {
1093
- const comments = task.comments.slice(0, 5);
1315
+ const comments = task.comments.slice(0, 3);
1094
1316
  prompt += `## Task History & Feedback
1095
1317
  `;
1096
1318
  prompt += `Review the following comments for context or rejection feedback:
@@ -1112,20 +1334,122 @@ ${comment.text}
1112
1334
  `;
1113
1335
  return prompt;
1114
1336
  }
1337
+ getProjectConfig() {
1338
+ const configPath = getLocusPath(this.projectPath, "configFile");
1339
+ if (import_node_fs4.existsSync(configPath)) {
1340
+ try {
1341
+ return JSON.parse(import_node_fs4.readFileSync(configPath, "utf-8"));
1342
+ } catch {
1343
+ return null;
1344
+ }
1345
+ }
1346
+ return null;
1347
+ }
1348
+ getFallbackContext() {
1349
+ const readmePath = import_node_path6.join(this.projectPath, "README.md");
1350
+ if (import_node_fs4.existsSync(readmePath)) {
1351
+ try {
1352
+ const content = import_node_fs4.readFileSync(readmePath, "utf-8");
1353
+ const limit = 1000;
1354
+ return content.slice(0, limit) + (content.length > limit ? `
1355
+ ...(truncated)...` : "");
1356
+ } catch {
1357
+ return "";
1358
+ }
1359
+ }
1360
+ return "";
1361
+ }
1362
+ getProjectStructure() {
1363
+ try {
1364
+ const entries = import_node_fs4.readdirSync(this.projectPath);
1365
+ const folders = entries.filter((e) => {
1366
+ if (e.startsWith(".") || e === "node_modules")
1367
+ return false;
1368
+ try {
1369
+ return import_node_fs4.statSync(import_node_path6.join(this.projectPath, e)).isDirectory();
1370
+ } catch {
1371
+ return false;
1372
+ }
1373
+ });
1374
+ if (folders.length === 0)
1375
+ return "";
1376
+ let structure = `## Project Structure
1377
+ `;
1378
+ structure += `Key directories in this project:
1379
+ `;
1380
+ for (const folder of folders) {
1381
+ structure += `- \`${folder}/\`
1382
+ `;
1383
+ }
1384
+ return `${structure}
1385
+ `;
1386
+ } catch {
1387
+ return "";
1388
+ }
1389
+ }
1390
+ getSkillsInfo() {
1391
+ const projectSkillsDirs = [
1392
+ LOCUS_CONFIG.agentSkillsDir,
1393
+ ".cursor/skills",
1394
+ ".claude/skills",
1395
+ ".codex/skills",
1396
+ ".gemini/skills"
1397
+ ];
1398
+ const globalHome = import_node_os2.homedir();
1399
+ const globalSkillsDirs = [
1400
+ import_node_path6.join(globalHome, ".cursor/skills"),
1401
+ import_node_path6.join(globalHome, ".claude/skills"),
1402
+ import_node_path6.join(globalHome, ".codex/skills"),
1403
+ import_node_path6.join(globalHome, ".gemini/skills")
1404
+ ];
1405
+ const allSkillNames = new Set;
1406
+ for (const relativePath of projectSkillsDirs) {
1407
+ const fullPath = import_node_path6.join(this.projectPath, relativePath);
1408
+ this.scanSkillsInDirectory(fullPath, allSkillNames);
1409
+ }
1410
+ for (const fullPath of globalSkillsDirs) {
1411
+ this.scanSkillsInDirectory(fullPath, allSkillNames);
1412
+ }
1413
+ const uniqueSkills = Array.from(allSkillNames).sort();
1414
+ if (uniqueSkills.length === 0)
1415
+ return "";
1416
+ return `## Available Agent Skills
1417
+ ` + `The project has the following specialized skills available (from project or global locations):
1418
+ ` + uniqueSkills.map((s) => `- ${s}`).join(`
1419
+ `) + `
1420
+
1421
+ `;
1422
+ }
1423
+ scanSkillsInDirectory(dirPath, skillSet) {
1424
+ if (!import_node_fs4.existsSync(dirPath))
1425
+ return;
1426
+ try {
1427
+ const entries = import_node_fs4.readdirSync(dirPath).filter((name) => {
1428
+ try {
1429
+ return import_node_fs4.statSync(import_node_path6.join(dirPath, name)).isDirectory();
1430
+ } catch {
1431
+ return false;
1432
+ }
1433
+ });
1434
+ for (const entry of entries) {
1435
+ skillSet.add(entry);
1436
+ }
1437
+ } catch {}
1438
+ }
1115
1439
  roleToText(role) {
1116
1440
  if (!role) {
1117
1441
  return null;
1118
1442
  }
1119
1443
  switch (role) {
1120
- case import_shared2.AssigneeRole.BACKEND:
1444
+ case import_shared3.AssigneeRole.BACKEND:
1121
1445
  return "Backend Engineer";
1122
- case import_shared2.AssigneeRole.FRONTEND:
1446
+ case import_shared3.AssigneeRole.FRONTEND:
1123
1447
  return "Frontend Engineer";
1124
- case import_shared2.AssigneeRole.PM:
1448
+ case import_shared3.AssigneeRole.PM:
1125
1449
  return "Product Manager";
1126
- case import_shared2.AssigneeRole.QA:
1450
+ case import_shared3.AssigneeRole.QA:
1127
1451
  return "QA Engineer";
1128
- case import_shared2.AssigneeRole.DESIGN:
1452
+ case import_shared3.AssigneeRole.DESIGN:
1129
1453
  return "Product Designer";
1130
1454
  default:
1131
1455
  return "engineer";
@@ -1141,18 +1465,11 @@ class TaskExecutor {
1141
1465
  this.deps = deps;
1142
1466
  this.promptBuilder = new PromptBuilder(deps.projectPath);
1143
1467
  }
1144
- updateSprintPlan(sprintPlan) {
1145
- this.deps.sprintPlan = sprintPlan;
1146
- }
1147
- async execute(task) {
1468
+ async execute(task, context) {
1148
1469
  this.deps.log(`Executing: ${task.title}`, "info");
1149
- let basePrompt = await this.promptBuilder.build(task);
1150
- if (this.deps.sprintPlan) {
1151
- basePrompt = `## Sprint Context
1152
- ${this.deps.sprintPlan}
1153
-
1154
- ${basePrompt}`;
1155
- }
1470
+ const basePrompt = await this.promptBuilder.build(task, {
1471
+ taskContext: context
1472
+ });
1156
1473
  try {
1157
1474
  let plan = null;
1158
1475
  if (this.deps.skipPlanning) {
@@ -1211,16 +1528,14 @@ class AgentWorker {
1211
1528
  config;
1212
1529
  client;
1213
1530
  aiRunner;
1214
- sprintPlanner;
1215
1531
  indexerService;
1216
1532
  artifactSyncer;
1217
1533
  taskExecutor;
1218
1534
  consecutiveEmpty = 0;
1219
- maxEmpty = 5;
1535
+ maxEmpty = 60;
1220
1536
  maxTasks = 50;
1221
1537
  tasksCompleted = 0;
1222
1538
  pollInterval = 1e4;
1223
- sprintPlan = null;
1224
1539
  constructor(config) {
1225
1540
  this.config = config;
1226
1541
  const projectPath = config.projectPath || process.cwd();
@@ -1241,10 +1556,6 @@ class AgentWorker {
1241
1556
  model: config.model,
1242
1557
  log
1243
1558
  });
1244
- this.sprintPlanner = new SprintPlanner({
1245
- aiRunner: this.aiRunner,
1246
- log
1247
- });
1248
1559
  this.indexerService = new CodebaseIndexerService({
1249
1560
  aiRunner: this.aiRunner,
1250
1561
  projectPath,
@@ -1259,7 +1570,6 @@ class AgentWorker {
1259
1570
  this.taskExecutor = new TaskExecutor({
1260
1571
  aiRunner: this.aiRunner,
1261
1572
  projectPath,
1262
- sprintPlan: null,
1263
1573
  skipPlanning: config.skipPlanning,
1264
1574
  log
1265
1575
  });
@@ -1298,8 +1608,13 @@ class AgentWorker {
1298
1608
  }
1299
1609
  async executeTask(task) {
1300
1610
  const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
1301
- this.taskExecutor.updateSprintPlan(this.sprintPlan);
1302
- const result = await this.taskExecutor.execute(fullTask);
1611
+ let context = "";
1612
+ try {
1613
+ context = await this.client.tasks.getContext(task.id, this.config.workspaceId);
1614
+ } catch (err) {
1615
+ this.log(`Failed to fetch task context: ${err}`, "warn");
1616
+ }
1617
+ const result = await this.taskExecutor.execute(fullTask, context);
1303
1618
  await this.indexerService.reindex();
1304
1619
  return result;
1305
1620
  }
@@ -1307,35 +1622,12 @@ class AgentWorker {
1307
1622
  this.log(`Agent started in ${this.config.projectPath || process.cwd()}`, "success");
1308
1623
  const sprint = await this.getActiveSprint();
1309
1624
  if (sprint) {
1310
- this.log(`Found active sprint: ${sprint.name} (${sprint.id})`, "info");
1311
- const tasks2 = await this.client.tasks.list(this.config.workspaceId, {
1312
- sprintId: sprint.id
1313
- });
1314
- const activeTasks = tasks2.filter((t) => t.status === import_shared3.TaskStatus.BACKLOG || t.status === import_shared3.TaskStatus.IN_PROGRESS);
1315
- this.log(`Sprint tasks found: ${activeTasks.length}`, "info");
1316
- if (activeTasks.length <= 1) {
1317
- this.log("Skipping mindmap generation (only one task in sprint).", "info");
1318
- this.sprintPlan = null;
1319
- } else {
1320
- const latestTaskCreation = activeTasks.reduce((latest, task) => {
1321
- const taskDate = new Date(task.createdAt);
1322
- return taskDate > latest ? taskDate : latest;
1323
- }, new Date(0));
1324
- const mindmapDate = sprint.mindmapUpdatedAt ? new Date(sprint.mindmapUpdatedAt) : new Date(0);
1325
- const needsPlanning = !sprint.mindmap || sprint.mindmap.trim() === "" || latestTaskCreation > mindmapDate;
1326
- if (needsPlanning) {
1327
- if (sprint.mindmap && latestTaskCreation > mindmapDate) {
1328
- this.log("New tasks have been added to the sprint since last mindmap. Regenerating...", "warn");
1329
- }
1330
- this.sprintPlan = await this.sprintPlanner.planSprint(sprint, tasks2);
1331
- await this.client.sprints.update(sprint.id, this.config.workspaceId, {
1332
- mindmap: this.sprintPlan,
1333
- mindmapUpdatedAt: new Date
1334
- });
1335
- } else {
1336
- this.log("Using existing sprint mindmap.", "info");
1337
- this.sprintPlan = sprint.mindmap ?? null;
1338
- }
1625
+ this.log(`Active sprint found: ${sprint.name}. Ensuring plan is up to date...`, "info");
1626
+ try {
1627
+ await this.client.sprints.triggerAIPlanning(sprint.id, this.config.workspaceId);
1628
+ this.log(`Sprint plan sync checked on server.`, "success");
1629
+ } catch (err) {
1630
+ this.log(`Sprint planning sync failed (non-critical): ${err instanceof Error ? err.message : String(err)}`, "warn");
1339
1631
  }
1340
1632
  } else {
1341
1633
  this.log("No active sprint found for planning.", "warn");
@@ -1355,7 +1647,11 @@ class AgentWorker {
1355
1647
  this.consecutiveEmpty = 0;
1356
1648
  this.log(`Claimed: ${task.title}`, "success");
1357
1649
  const result = await this.executeTask(task);
1358
- await this.artifactSyncer.sync();
1650
+ try {
1651
+ await this.artifactSyncer.sync();
1652
+ } catch (err) {
1653
+ this.log(`Artifact sync failed: ${err}`, "error");
1654
+ }
1359
1655
  if (result.success) {
1360
1656
  this.log(`Completed: ${task.title}`, "success");
1361
1657
  await this.client.tasks.update(task.id, this.config.workspaceId, {
@@ -1392,7 +1688,7 @@ if (process.argv[1]?.includes("agent-worker") || process.argv[1]?.includes("work
1392
1688
  config.workspaceId = args[++i];
1393
1689
  else if (arg === "--sprint-id")
1394
1690
  config.sprintId = args[++i];
1395
- else if (arg === "--api-base")
1691
+ else if (arg === "--api-url")
1396
1692
  config.apiBase = args[++i];
1397
1693
  else if (arg === "--api-key")
1398
1694
  config.apiKey = args[++i];
@@ -1429,7 +1725,6 @@ __export(exports_index_node, {
1429
1725
  TasksModule: () => TasksModule,
1430
1726
  TaskExecutor: () => TaskExecutor,
1431
1727
  SprintsModule: () => SprintsModule,
1432
- SprintPlanner: () => SprintPlanner,
1433
1728
  PromptBuilder: () => PromptBuilder,
1434
1729
  PROVIDER: () => PROVIDER,
1435
1730
  OrganizationsModule: () => OrganizationsModule,
@@ -1448,13 +1743,14 @@ __export(exports_index_node, {
1448
1743
  AuthModule: () => AuthModule,
1449
1744
  ArtifactSyncer: () => ArtifactSyncer,
1450
1745
  AgentWorker: () => AgentWorker,
1451
- AgentOrchestrator: () => AgentOrchestrator
1746
+ AgentOrchestrator: () => AgentOrchestrator,
1747
+ AIModule: () => AIModule
1452
1748
  });
1453
1749
  module.exports = __toCommonJS(exports_index_node);
1454
1750
  // src/orchestrator.ts
1455
1751
  var import_node_child_process3 = require("node:child_process");
1456
1752
  var import_node_fs5 = require("node:fs");
1457
- var import_node_path6 = require("node:path");
1753
+ var import_node_path7 = require("node:path");
1458
1754
  var import_node_url = require("node:url");
1459
1755
  var import_shared4 = require("@locusai/shared");
1460
1756
  var import_events3 = require("events");
@@ -1550,8 +1846,8 @@ ${c.success("✅ Orchestrator finished")}`);
1550
1846
  `);
1551
1847
  const potentialPaths = [];
1552
1848
  const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator.ts");
1553
- const currentModuleDir = import_node_path6.dirname(currentModulePath);
1554
- potentialPaths.push(import_node_path6.join(currentModuleDir, "agent", "worker.js"), import_node_path6.join(currentModuleDir, "worker.js"), import_node_path6.join(currentModuleDir, "agent", "worker.ts"));
1849
+ const currentModuleDir = import_node_path7.dirname(currentModulePath);
1850
+ potentialPaths.push(import_node_path7.join(currentModuleDir, "agent", "worker.js"), import_node_path7.join(currentModuleDir, "worker.js"), import_node_path7.join(currentModuleDir, "agent", "worker.ts"));
1555
1851
  const workerPath = potentialPaths.find((p) => import_node_fs5.existsSync(p));
1556
1852
  if (!workerPath) {
1557
1853
  throw new Error(`Worker file not found. Checked: ${potentialPaths.join(", ")}. ` + `Make sure the SDK is properly built and installed.`);
@@ -1561,7 +1857,7 @@ ${c.success("✅ Orchestrator finished")}`);
1561
1857
  agentId,
1562
1858
  "--workspace-id",
1563
1859
  this.config.workspaceId,
1564
- "--api-base",
1860
+ "--api-url",
1565
1861
  this.config.apiBase,
1566
1862
  "--api-key",
1567
1863
  this.config.apiKey,