@locusai/sdk 0.15.0 → 0.15.2

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 (47) hide show
  1. package/dist/agent/worker.js +0 -28
  2. package/dist/events.d.ts.map +1 -1
  3. package/dist/index-node.d.ts +1 -1
  4. package/dist/index-node.d.ts.map +1 -1
  5. package/dist/index-node.js +428 -2081
  6. package/dist/index.d.ts +0 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +0 -28
  9. package/dist/{jobs/proposals → proposals}/context-gatherer.d.ts +2 -5
  10. package/dist/{jobs/proposals → proposals}/context-gatherer.d.ts.map +1 -1
  11. package/dist/proposals/index.d.ts.map +1 -0
  12. package/dist/{jobs/proposals → proposals}/proposal-engine.d.ts +1 -1
  13. package/dist/proposals/proposal-engine.d.ts.map +1 -0
  14. package/package.json +2 -2
  15. package/dist/jobs/__tests__/job-runner.test.d.ts +0 -2
  16. package/dist/jobs/__tests__/job-runner.test.d.ts.map +0 -1
  17. package/dist/jobs/__tests__/lint-scan.test.d.ts +0 -2
  18. package/dist/jobs/__tests__/lint-scan.test.d.ts.map +0 -1
  19. package/dist/jobs/__tests__/scheduler.test.d.ts +0 -2
  20. package/dist/jobs/__tests__/scheduler.test.d.ts.map +0 -1
  21. package/dist/jobs/base-job.d.ts +0 -29
  22. package/dist/jobs/base-job.d.ts.map +0 -1
  23. package/dist/jobs/default-registry.d.ts +0 -3
  24. package/dist/jobs/default-registry.d.ts.map +0 -1
  25. package/dist/jobs/index.d.ts +0 -12
  26. package/dist/jobs/index.d.ts.map +0 -1
  27. package/dist/jobs/job-registry.d.ts +0 -10
  28. package/dist/jobs/job-registry.d.ts.map +0 -1
  29. package/dist/jobs/job-runner.d.ts +0 -33
  30. package/dist/jobs/job-runner.d.ts.map +0 -1
  31. package/dist/jobs/proposals/index.d.ts.map +0 -1
  32. package/dist/jobs/proposals/proposal-engine.d.ts.map +0 -1
  33. package/dist/jobs/scans/dependency-scan.d.ts +0 -28
  34. package/dist/jobs/scans/dependency-scan.d.ts.map +0 -1
  35. package/dist/jobs/scans/index.d.ts +0 -5
  36. package/dist/jobs/scans/index.d.ts.map +0 -1
  37. package/dist/jobs/scans/lint-scan.d.ts +0 -20
  38. package/dist/jobs/scans/lint-scan.d.ts.map +0 -1
  39. package/dist/jobs/scans/test-scan.d.ts +0 -20
  40. package/dist/jobs/scans/test-scan.d.ts.map +0 -1
  41. package/dist/jobs/scans/todo-scan.d.ts +0 -15
  42. package/dist/jobs/scans/todo-scan.d.ts.map +0 -1
  43. package/dist/jobs/scheduler.d.ts +0 -80
  44. package/dist/jobs/scheduler.d.ts.map +0 -1
  45. package/dist/modules/jobs.d.ts +0 -14
  46. package/dist/modules/jobs.d.ts.map +0 -1
  47. /package/dist/{jobs/proposals → proposals}/index.d.ts +0 -0
@@ -225,29 +225,6 @@ var init_invitations = __esm(() => {
225
225
  };
226
226
  });
227
227
 
228
- // src/modules/jobs.ts
229
- var JobsModule;
230
- var init_jobs = __esm(() => {
231
- JobsModule = class JobsModule extends BaseModule {
232
- async create(workspaceId, data) {
233
- const { data: res } = await this.api.post(`/workspaces/${workspaceId}/job-runs`, data);
234
- return res.jobRun;
235
- }
236
- async list(workspaceId, params) {
237
- const { data } = await this.api.get(`/workspaces/${workspaceId}/job-runs`, { params });
238
- return data.jobRuns;
239
- }
240
- async get(workspaceId, id) {
241
- const { data } = await this.api.get(`/workspaces/${workspaceId}/job-runs/${id}`);
242
- return data.jobRun;
243
- }
244
- async update(workspaceId, id, data) {
245
- const { data: res } = await this.api.patch(`/workspaces/${workspaceId}/job-runs/${id}`, data);
246
- return res.jobRun;
247
- }
248
- };
249
- });
250
-
251
228
  // src/modules/organizations.ts
252
229
  var OrganizationsModule;
253
230
  var init_organizations = __esm(() => {
@@ -535,7 +512,6 @@ __export(exports_src, {
535
512
  LocusEvent: () => LocusEvent,
536
513
  LocusEmitter: () => LocusEmitter,
537
514
  LocusClient: () => LocusClient,
538
- JobsModule: () => JobsModule,
539
515
  InvitationsModule: () => InvitationsModule,
540
516
  InstancesModule: () => InstancesModule,
541
517
  DocsModule: () => DocsModule,
@@ -559,7 +535,6 @@ class LocusClient {
559
535
  docs;
560
536
  ci;
561
537
  instances;
562
- jobs;
563
538
  suggestions;
564
539
  constructor(config) {
565
540
  this.emitter = new LocusEmitter;
@@ -581,7 +556,6 @@ class LocusClient {
581
556
  this.docs = new DocsModule(this.api, this.emitter);
582
557
  this.ci = new CiModule(this.api, this.emitter);
583
558
  this.instances = new InstancesModule(this.api, this.emitter);
584
- this.jobs = new JobsModule(this.api, this.emitter);
585
559
  this.suggestions = new SuggestionsModule(this.api, this.emitter);
586
560
  if (config.retryOptions) {
587
561
  this.setupRetryInterceptor(config.retryOptions);
@@ -650,7 +624,6 @@ var init_src = __esm(() => {
650
624
  init_docs();
651
625
  init_instances();
652
626
  init_invitations();
653
- init_jobs();
654
627
  init_organizations();
655
628
  init_sprints();
656
629
  init_suggestions();
@@ -664,7 +637,6 @@ var init_src = __esm(() => {
664
637
  init_docs();
665
638
  init_instances();
666
639
  init_invitations();
667
- init_jobs();
668
640
  init_organizations();
669
641
  init_sprints();
670
642
  init_suggestions();
@@ -2907,19 +2879,15 @@ __export(exports_index_node, {
2907
2879
  getAgentArtifactsPath: () => getAgentArtifactsPath,
2908
2880
  extractJsonFromLLMOutput: () => extractJsonFromLLMOutput,
2909
2881
  detectRemoteProvider: () => detectRemoteProvider,
2910
- createDefaultRegistry: () => createDefaultRegistry,
2911
2882
  createAiRunner: () => createAiRunner,
2912
2883
  c: () => c,
2913
2884
  buildSummaryPrompt: () => buildSummaryPrompt,
2914
2885
  buildFacilitatorPrompt: () => buildFacilitatorPrompt,
2915
2886
  WorkspacesModule: () => WorkspacesModule,
2916
- TodoScanJob: () => TodoScanJob,
2917
- TestScanJob: () => TestScanJob,
2918
2887
  TasksModule: () => TasksModule,
2919
2888
  TaskExecutor: () => TaskExecutor,
2920
2889
  SuggestionsModule: () => SuggestionsModule,
2921
2890
  SprintsModule: () => SprintsModule,
2922
- SchedulerEvent: () => SchedulerEvent,
2923
2891
  ReviewerWorker: () => ReviewerWorker,
2924
2892
  ReviewService: () => ReviewService,
2925
2893
  ProposalEngine: () => ProposalEngine,
@@ -2933,16 +2901,10 @@ __export(exports_index_node, {
2933
2901
  LocusEvent: () => LocusEvent,
2934
2902
  LocusEmitter: () => LocusEmitter,
2935
2903
  LocusClient: () => LocusClient,
2936
- LintScanJob: () => LintScanJob,
2937
2904
  LOCUS_SCHEMA_BASE_URL: () => LOCUS_SCHEMA_BASE_URL,
2938
2905
  LOCUS_SCHEMAS: () => LOCUS_SCHEMAS,
2939
2906
  LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
2940
2907
  LOCUS_CONFIG: () => LOCUS_CONFIG,
2941
- JobsModule: () => JobsModule,
2942
- JobScheduler: () => JobScheduler,
2943
- JobRunner: () => JobRunner,
2944
- JobRegistry: () => JobRegistry,
2945
- JobEvent: () => JobEvent,
2946
2908
  InvitationsModule: () => InvitationsModule,
2947
2909
  InstancesModule: () => InstancesModule,
2948
2910
  HistoryManager: () => HistoryManager,
@@ -2957,7 +2919,6 @@ __export(exports_index_node, {
2957
2919
  DiscussionManager: () => DiscussionManager,
2958
2920
  DiscussionInsightSchema: () => DiscussionInsightSchema,
2959
2921
  DiscussionFacilitator: () => DiscussionFacilitator,
2960
- DependencyScanJob: () => DependencyScanJob,
2961
2922
  DEFAULT_MODEL: () => DEFAULT_MODEL,
2962
2923
  ContextTracker: () => ContextTracker,
2963
2924
  ContextGatherer: () => ContextGatherer,
@@ -2968,7 +2929,6 @@ __export(exports_index_node, {
2968
2929
  CiModule: () => CiModule,
2969
2930
  CODEX_MODELS: () => CODEX_MODELS,
2970
2931
  CLAUDE_MODELS: () => CLAUDE_MODELS,
2971
- BaseJob: () => BaseJob,
2972
2932
  AuthModule: () => AuthModule,
2973
2933
  AgentWorker: () => AgentWorker,
2974
2934
  AgentOrchestrator: () => AgentOrchestrator
@@ -5023,2046 +4983,198 @@ init_git_utils();
5023
4983
  // src/index-node.ts
5024
4984
  init_src();
5025
4985
 
5026
- // src/jobs/base-job.ts
5027
- class BaseJob {
5028
- shouldAutoExecute(category, rules) {
5029
- const rule = rules.find((r) => r.category === category);
5030
- return rule ? rule.autoExecute : false;
5031
- }
5032
- }
5033
- // src/jobs/job-registry.ts
5034
- class JobRegistry {
5035
- jobs = new Map;
5036
- register(job) {
5037
- this.jobs.set(job.type, job);
5038
- }
5039
- get(type) {
5040
- return this.jobs.get(type);
5041
- }
5042
- getAll() {
5043
- return Array.from(this.jobs.values());
5044
- }
5045
- has(type) {
5046
- return this.jobs.has(type);
5047
- }
5048
- }
5049
-
5050
- // src/jobs/scans/dependency-scan.ts
4986
+ // src/orchestrator/index.ts
5051
4987
  init_git_utils();
4988
+ init_src();
4989
+ init_colors();
4990
+ init_resolve_bin();
5052
4991
  var import_node_child_process7 = require("node:child_process");
5053
4992
  var import_node_fs9 = require("node:fs");
5054
4993
  var import_node_path10 = require("node:path");
4994
+ var import_node_url = require("node:url");
5055
4995
  var import_shared4 = require("@locusai/shared");
5056
- class DependencyScanJob extends BaseJob {
5057
- type = import_shared4.JobType.DEPENDENCY_CHECK;
5058
- name = "Dependency Check";
5059
- async run(context) {
5060
- const { projectPath, autonomyRules } = context;
5061
- const pm = this.detectPackageManager(projectPath);
5062
- if (!pm) {
5063
- return {
5064
- summary: "No package manager lock file detected",
5065
- suggestions: [],
5066
- filesChanged: 0
5067
- };
4996
+ var import_events4 = require("events");
4997
+
4998
+ class AgentOrchestrator extends import_events4.EventEmitter {
4999
+ client;
5000
+ config;
5001
+ isRunning = false;
5002
+ processedTasks = new Set;
5003
+ resolvedSprintId = null;
5004
+ agentState = null;
5005
+ heartbeatInterval = null;
5006
+ constructor(config) {
5007
+ super();
5008
+ this.config = config;
5009
+ this.client = new LocusClient({
5010
+ baseUrl: config.apiBase,
5011
+ token: config.apiKey
5012
+ });
5013
+ }
5014
+ async resolveSprintId() {
5015
+ if (this.config.sprintId) {
5016
+ return this.config.sprintId;
5068
5017
  }
5069
- let outdated;
5070
5018
  try {
5071
- outdated = this.getOutdatedPackages(pm, projectPath);
5072
- } catch (err) {
5073
- const message = err instanceof Error ? err.message : String(err);
5074
- return {
5075
- summary: `Dependency check failed (outdated): ${message}`,
5076
- suggestions: [],
5077
- filesChanged: 0,
5078
- errors: [message]
5079
- };
5019
+ const sprint = await this.client.sprints.getActive(this.config.workspaceId);
5020
+ if (sprint?.id) {
5021
+ console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
5022
+ return sprint.id;
5023
+ }
5024
+ } catch {}
5025
+ console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
5026
+ return "";
5027
+ }
5028
+ async start() {
5029
+ if (this.isRunning) {
5030
+ throw new Error("Orchestrator is already running");
5080
5031
  }
5081
- let vulnerabilities;
5032
+ this.isRunning = true;
5033
+ this.processedTasks.clear();
5082
5034
  try {
5083
- vulnerabilities = this.runSecurityAudit(pm, projectPath);
5084
- } catch {
5085
- vulnerabilities = [];
5086
- }
5087
- const patch = outdated.filter((p) => p.risk === "patch");
5088
- const minor = outdated.filter((p) => p.risk === "minor");
5089
- const major = outdated.filter((p) => p.risk === "major");
5090
- const canAutoExecute = this.shouldAutoExecute(import_shared4.ChangeCategory.DEPENDENCY, autonomyRules);
5091
- let filesChanged = 0;
5092
- let prUrl;
5093
- if (canAutoExecute && (patch.length > 0 || minor.length > 0) && major.length === 0) {
5094
- const autoResult = this.autoUpdate(pm, patch, minor, projectPath);
5095
- filesChanged = autoResult.filesChanged;
5096
- prUrl = autoResult.prUrl ?? undefined;
5097
- }
5098
- const suggestions2 = this.buildSuggestions(patch, minor, major, pm);
5099
- const summaryParts = [];
5100
- if (outdated.length > 0) {
5101
- summaryParts.push(`${outdated.length} outdated (${patch.length} patch, ${minor.length} minor, ${major.length} major)`);
5102
- } else {
5103
- summaryParts.push("all dependencies up to date");
5104
- }
5105
- if (vulnerabilities.length > 0) {
5106
- summaryParts.push(`${vulnerabilities.length} vulnerability(ies)`);
5107
- }
5108
- if (filesChanged > 0) {
5109
- summaryParts.push(`auto-updated ${patch.length + minor.length} safe package(s)`);
5035
+ await this.orchestrationLoop();
5036
+ } catch (error) {
5037
+ this.emit("error", error);
5038
+ throw error;
5039
+ } finally {
5040
+ await this.cleanup();
5110
5041
  }
5111
- const summary = `Dependency check: ${summaryParts.join(", ")} (${pm})`;
5112
- return {
5113
- summary,
5114
- suggestions: suggestions2,
5115
- filesChanged,
5116
- prUrl
5117
- };
5118
- }
5119
- detectPackageManager(projectPath) {
5120
- if (import_node_fs9.existsSync(import_node_path10.join(projectPath, "bun.lock")))
5121
- return "bun";
5122
- if (import_node_fs9.existsSync(import_node_path10.join(projectPath, "pnpm-lock.yaml")))
5123
- return "pnpm";
5124
- if (import_node_fs9.existsSync(import_node_path10.join(projectPath, "yarn.lock")))
5125
- return "yarn";
5126
- if (import_node_fs9.existsSync(import_node_path10.join(projectPath, "package-lock.json")))
5127
- return "npm";
5128
- return null;
5129
5042
  }
5130
- getOutdatedPackages(pm, projectPath) {
5131
- const output = this.runOutdatedCommand(pm, projectPath);
5132
- if (pm === "bun") {
5133
- return this.parseBunOutdated(output.stdout);
5043
+ async orchestrationLoop() {
5044
+ this.resolvedSprintId = await this.resolveSprintId();
5045
+ this.emit("started", {
5046
+ timestamp: new Date,
5047
+ config: this.config,
5048
+ sprintId: this.resolvedSprintId
5049
+ });
5050
+ this.printBanner();
5051
+ const tasks2 = await this.getAvailableTasks();
5052
+ if (tasks2.length === 0) {
5053
+ console.log(c.dim("ℹ No available tasks found in the backlog."));
5054
+ return;
5134
5055
  }
5135
- return this.parseJsonOutdated(pm, output.stdout);
5136
- }
5137
- runOutdatedCommand(pm, projectPath) {
5138
- const commands = {
5139
- bun: ["bun", "outdated"],
5140
- npm: ["npm", "outdated", "--json"],
5141
- pnpm: ["pnpm", "outdated", "--format", "json"],
5142
- yarn: ["yarn", "outdated", "--json"]
5143
- };
5144
- const [bin, ...args] = commands[pm];
5145
- return this.exec(bin, args, projectPath);
5056
+ if (!this.preflightChecks())
5057
+ return;
5058
+ this.startHeartbeatMonitor();
5059
+ await this.spawnAgent();
5060
+ await this.waitForAgent();
5061
+ console.log(`
5062
+ ${c.success(" Orchestrator finished")}`);
5146
5063
  }
5147
- parseBunOutdated(stdout) {
5148
- const packages = [];
5149
- const lines = stdout.split(`
5150
- `);
5151
- for (const line of lines) {
5152
- const match = line.match(/^\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|?\s*$/);
5153
- if (!match)
5154
- continue;
5155
- const [, name, current, , latest] = match;
5156
- if (!name || !current || !latest || name === "Package")
5157
- continue;
5158
- packages.push({
5159
- name,
5160
- current,
5161
- wanted: latest,
5162
- latest,
5163
- risk: this.classifyRisk(current, latest)
5164
- });
5064
+ printBanner() {
5065
+ console.log(`
5066
+ ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
5067
+ console.log(c.dim("----------------------------------------------"));
5068
+ console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
5069
+ if (this.resolvedSprintId) {
5070
+ console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
5165
5071
  }
5166
- return packages;
5072
+ console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
5073
+ console.log(c.dim(`----------------------------------------------
5074
+ `));
5167
5075
  }
5168
- parseJsonOutdated(pm, stdout) {
5169
- const packages = [];
5170
- if (!stdout.trim())
5171
- return packages;
5172
- if (pm === "yarn") {
5173
- return this.parseYarnOutdated(stdout);
5174
- }
5175
- let data;
5176
- try {
5177
- data = JSON.parse(stdout);
5178
- } catch {
5179
- return packages;
5076
+ preflightChecks() {
5077
+ if (!isGitAvailable()) {
5078
+ console.log(c.error("git is not installed. Install from https://git-scm.com/"));
5079
+ return false;
5180
5080
  }
5181
- for (const [name, info] of Object.entries(data)) {
5182
- const current = info.current ?? "0.0.0";
5183
- const wanted = info.wanted ?? current;
5184
- const latest = info.latest ?? wanted;
5185
- packages.push({
5186
- name,
5187
- current,
5188
- wanted,
5189
- latest,
5190
- risk: this.classifyRisk(current, latest)
5191
- });
5081
+ if (!isGhAvailable(this.config.projectPath)) {
5082
+ 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/"));
5192
5083
  }
5193
- return packages;
5084
+ return true;
5194
5085
  }
5195
- parseYarnOutdated(stdout) {
5196
- const packages = [];
5197
- for (const line of stdout.split(`
5198
- `)) {
5199
- if (!line.trim())
5200
- continue;
5201
- try {
5202
- const obj = JSON.parse(line);
5203
- if (obj.type === "table" && Array.isArray(obj.data?.body)) {
5204
- for (const row of obj.data.body) {
5205
- if (!Array.isArray(row) || row.length < 4)
5206
- continue;
5207
- const [name, current, wanted, latest] = row;
5208
- packages.push({
5209
- name,
5210
- current,
5211
- wanted,
5212
- latest,
5213
- risk: this.classifyRisk(current, latest)
5214
- });
5215
- }
5216
- }
5217
- } catch {}
5086
+ async getAvailableTasks() {
5087
+ try {
5088
+ const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
5089
+ return tasks2.filter((task) => !this.processedTasks.has(task.id));
5090
+ } catch (error) {
5091
+ this.emit("error", error);
5092
+ return [];
5218
5093
  }
5219
- return packages;
5220
5094
  }
5221
- runSecurityAudit(pm, projectPath) {
5222
- const commands = {
5223
- bun: ["bun", "audit"],
5224
- npm: ["npm", "audit", "--json"],
5225
- pnpm: ["pnpm", "audit", "--json"],
5226
- yarn: ["yarn", "audit", "--json"]
5095
+ async spawnAgent() {
5096
+ const agentId = `agent-0-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
5097
+ this.agentState = {
5098
+ id: agentId,
5099
+ status: "IDLE",
5100
+ currentTaskId: null,
5101
+ tasksCompleted: 0,
5102
+ tasksFailed: 0,
5103
+ lastHeartbeat: new Date
5227
5104
  };
5228
- const [bin, ...args] = commands[pm];
5229
- const output = this.exec(bin, args, projectPath);
5230
- if (pm === "npm" || pm === "pnpm") {
5231
- return this.parseNpmAudit(output.stdout);
5232
- }
5233
- return this.parseGenericAudit(output.stdout);
5234
- }
5235
- parseNpmAudit(stdout) {
5236
- const vulnerabilities = [];
5237
- if (!stdout.trim())
5238
- return vulnerabilities;
5239
- try {
5240
- const data = JSON.parse(stdout);
5241
- const vulns = data.vulnerabilities ?? data.advisories ?? {};
5242
- for (const [name, info] of Object.entries(vulns)) {
5243
- const v = info;
5244
- vulnerabilities.push({
5245
- name,
5246
- severity: v.severity ?? "unknown",
5247
- title: v.title ?? v.overview ?? name,
5248
- url: v.url ?? undefined
5249
- });
5250
- }
5251
- } catch {}
5252
- return vulnerabilities;
5253
- }
5254
- parseGenericAudit(stdout) {
5255
- const vulnerabilities = [];
5256
- for (const line of stdout.split(`
5257
- `)) {
5258
- if (!line.trim())
5259
- continue;
5260
- try {
5261
- const obj = JSON.parse(line);
5262
- if (obj.type === "auditAdvisory" && obj.data?.advisory) {
5263
- const adv = obj.data.advisory;
5264
- vulnerabilities.push({
5265
- name: adv.module_name ?? "unknown",
5266
- severity: adv.severity ?? "unknown",
5267
- title: adv.title ?? "Unknown vulnerability",
5268
- url: adv.url
5269
- });
5270
- }
5271
- } catch {}
5105
+ console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
5106
+ `);
5107
+ const workerPath = this.resolveWorkerPath();
5108
+ if (!workerPath) {
5109
+ throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
5272
5110
  }
5273
- return vulnerabilities;
5111
+ const workerArgs = this.buildWorkerArgs(agentId);
5112
+ const agentProcess = import_node_child_process7.spawn(process.execPath, [workerPath, ...workerArgs], {
5113
+ stdio: ["pipe", "pipe", "pipe"],
5114
+ detached: true,
5115
+ env: getAugmentedEnv({
5116
+ FORCE_COLOR: "1",
5117
+ TERM: "xterm-256color",
5118
+ LOCUS_WORKER: agentId,
5119
+ LOCUS_WORKSPACE: this.config.workspaceId
5120
+ })
5121
+ });
5122
+ this.agentState.process = agentProcess;
5123
+ this.attachProcessHandlers(agentId, this.agentState, agentProcess);
5124
+ this.emit("agent:spawned", { agentId });
5274
5125
  }
5275
- autoUpdate(pm, patch, minor, projectPath) {
5276
- const safePackages = [...patch, ...minor];
5277
- try {
5278
- this.runUpdateCommand(pm, safePackages, projectPath);
5279
- this.runInstallCommand(pm, projectPath);
5280
- } catch {
5281
- return { filesChanged: 0, prUrl: null };
5282
- }
5283
- let changedFiles;
5284
- try {
5285
- const diffOutput = import_node_child_process7.execFileSync("git", ["diff", "--name-only"], {
5286
- cwd: projectPath,
5287
- encoding: "utf-8",
5288
- stdio: ["pipe", "pipe", "pipe"]
5289
- }).trim();
5290
- changedFiles = diffOutput ? diffOutput.split(`
5291
- `) : [];
5292
- } catch {
5293
- changedFiles = [];
5294
- }
5295
- if (changedFiles.length === 0) {
5296
- return { filesChanged: 0, prUrl: null };
5126
+ async waitForAgent() {
5127
+ while (this.agentState && this.isRunning) {
5128
+ await sleep(2000);
5297
5129
  }
5298
- const prUrl = this.commitAndPush(projectPath, changedFiles, safePackages, pm);
5299
- return { filesChanged: changedFiles.length, prUrl };
5300
5130
  }
5301
- runUpdateCommand(pm, packages, projectPath) {
5302
- const pkgSpecs = packages.map((p) => `${p.name}@${p.latest}`);
5303
- switch (pm) {
5304
- case "bun":
5305
- this.exec("bun", ["add", ...pkgSpecs], projectPath);
5306
- break;
5307
- case "npm":
5308
- this.exec("npm", ["install", ...pkgSpecs], projectPath);
5309
- break;
5310
- case "pnpm":
5311
- this.exec("pnpm", ["add", ...pkgSpecs], projectPath);
5312
- break;
5313
- case "yarn":
5314
- this.exec("yarn", ["add", ...pkgSpecs], projectPath);
5315
- break;
5316
- }
5131
+ startHeartbeatMonitor() {
5132
+ this.heartbeatInterval = setInterval(() => {
5133
+ if (!this.agentState)
5134
+ return;
5135
+ const now = Date.now();
5136
+ if (this.agentState.status === "WORKING" && now - this.agentState.lastHeartbeat.getTime() > import_shared4.STALE_AGENT_TIMEOUT_MS) {
5137
+ console.log(c.error(`Agent ${this.agentState.id} is stale (no heartbeat for 10 minutes). Killing.`));
5138
+ if (this.agentState.process && !this.agentState.process.killed) {
5139
+ killProcessTree(this.agentState.process);
5140
+ }
5141
+ this.emit("agent:stale", { agentId: this.agentState.id });
5142
+ }
5143
+ }, 60000);
5317
5144
  }
5318
- runInstallCommand(pm, projectPath) {
5319
- const commands = {
5320
- bun: ["bun", "install"],
5321
- npm: ["npm", "install"],
5322
- pnpm: ["pnpm", "install"],
5323
- yarn: ["yarn", "install"]
5324
- };
5325
- const [bin, ...args] = commands[pm];
5326
- this.exec(bin, args, projectPath);
5145
+ async stop() {
5146
+ this.isRunning = false;
5147
+ await this.cleanup();
5148
+ this.emit("stopped", { timestamp: new Date });
5327
5149
  }
5328
- commitAndPush(projectPath, changedFiles, packages, pm) {
5329
- try {
5330
- const defaultBranch = getDefaultBranch(projectPath);
5331
- const branchName = `locus/dep-update-${Date.now().toString(36)}`;
5332
- this.gitExec(["checkout", "-b", branchName], projectPath);
5333
- this.gitExec(["add", ...changedFiles], projectPath);
5334
- const packageList = packages.map((p) => `${p.name}@${p.latest}`).join(", ");
5335
- const commitMessage = `fix(deps): update ${packages.length} safe dependencies
5336
-
5337
- Updated: ${packageList}
5338
- Package manager: ${pm}
5339
- Agent: locus-dependency-check
5340
- Co-authored-by: LocusAI <agent@locusai.team>`;
5341
- this.gitExec(["commit", "-m", commitMessage], projectPath);
5342
- this.gitExec(["push", "-u", "origin", branchName], projectPath);
5343
- let prUrl = null;
5344
- if (detectRemoteProvider(projectPath) === "github" && isGhAvailable(projectPath)) {
5345
- try {
5346
- const title = `[Locus] Update ${packages.length} safe dependencies`;
5347
- const body = [
5348
- "## Summary",
5349
- "",
5350
- `Automated dependency updates applied by Locus using \`${pm}\`.`,
5351
- "",
5352
- "### Updated packages",
5353
- "",
5354
- ...packages.map((p) => `- \`${p.name}\`: ${p.current} → ${p.latest} (${p.risk})`),
5355
- "",
5356
- `- **Files changed**: ${changedFiles.length}`,
5357
- "",
5358
- "---",
5359
- "*Created by Locus Agent (dependency-check)*"
5360
- ].join(`
5361
- `);
5362
- prUrl = import_node_child_process7.execFileSync("gh", [
5363
- "pr",
5364
- "create",
5365
- "--title",
5366
- title,
5367
- "--body",
5368
- body,
5369
- "--base",
5370
- defaultBranch,
5371
- "--head",
5372
- branchName
5373
- ], {
5374
- cwd: projectPath,
5375
- encoding: "utf-8",
5376
- stdio: ["pipe", "pipe", "pipe"]
5377
- }).trim();
5378
- } catch {}
5379
- }
5380
- try {
5381
- this.gitExec(["checkout", defaultBranch], projectPath);
5382
- } catch {}
5383
- return prUrl;
5384
- } catch {
5385
- return null;
5150
+ stopAgent(agentId) {
5151
+ if (!this.agentState || this.agentState.id !== agentId)
5152
+ return false;
5153
+ if (this.agentState.process && !this.agentState.process.killed) {
5154
+ killProcessTree(this.agentState.process);
5386
5155
  }
5156
+ return true;
5387
5157
  }
5388
- buildSuggestions(patch, minor, major, pm) {
5389
- const suggestions2 = [];
5390
- if (patch.length > 0) {
5391
- suggestions2.push({
5392
- type: import_shared4.SuggestionType.DEPENDENCY_UPDATE,
5393
- title: `${patch.length} patch update(s) available`,
5394
- description: `Safe patch updates: ${patch.map((p) => `${p.name} ${p.current} → ${p.latest}`).join(", ")}. Run \`${this.getUpdateHint(pm, patch)}\` to apply.`,
5395
- metadata: {
5396
- risk: "patch",
5397
- packages: patch.map((p) => ({
5398
- name: p.name,
5399
- current: p.current,
5400
- latest: p.latest
5401
- }))
5402
- }
5403
- });
5404
- }
5405
- if (minor.length > 0) {
5406
- suggestions2.push({
5407
- type: import_shared4.SuggestionType.DEPENDENCY_UPDATE,
5408
- title: `${minor.length} minor update(s) available`,
5409
- description: `Minor updates: ${minor.map((p) => `${p.name} ${p.current} → ${p.latest}`).join(", ")}. Generally safe but review changelogs.`,
5410
- metadata: {
5411
- risk: "minor",
5412
- packages: minor.map((p) => ({
5413
- name: p.name,
5414
- current: p.current,
5415
- latest: p.latest
5416
- }))
5417
- }
5418
- });
5158
+ async cleanup() {
5159
+ if (this.heartbeatInterval) {
5160
+ clearInterval(this.heartbeatInterval);
5161
+ this.heartbeatInterval = null;
5419
5162
  }
5420
- if (major.length > 0) {
5421
- suggestions2.push({
5422
- type: import_shared4.SuggestionType.DEPENDENCY_UPDATE,
5423
- title: `${major.length} major update(s) require review`,
5424
- description: `Breaking changes possible: ${major.map((p) => `${p.name} ${p.current} → ${p.latest}`).join(", ")}. Review migration guides before upgrading.`,
5425
- metadata: {
5426
- risk: "major",
5427
- packages: major.map((p) => ({
5428
- name: p.name,
5429
- current: p.current,
5430
- latest: p.latest
5431
- }))
5432
- }
5433
- });
5163
+ if (this.agentState?.process && !this.agentState.process.killed) {
5164
+ console.log(`Killing agent: ${this.agentState.id}`);
5165
+ killProcessTree(this.agentState.process);
5434
5166
  }
5435
- return suggestions2;
5436
- }
5437
- classifyRisk(current, latest) {
5438
- const currentParts = this.parseSemver(current);
5439
- const latestParts = this.parseSemver(latest);
5440
- if (!currentParts || !latestParts)
5441
- return "major";
5442
- if (latestParts.major !== currentParts.major)
5443
- return "major";
5444
- if (latestParts.minor !== currentParts.minor)
5445
- return "minor";
5446
- return "patch";
5447
- }
5448
- parseSemver(version) {
5449
- const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
5450
- if (!match)
5451
- return null;
5167
+ }
5168
+ getStats() {
5452
5169
  return {
5453
- major: parseInt(match[1], 10),
5454
- minor: parseInt(match[2], 10),
5455
- patch: parseInt(match[3], 10)
5170
+ activeAgents: this.agentState ? 1 : 0,
5171
+ totalTasksCompleted: this.agentState?.tasksCompleted ?? 0,
5172
+ totalTasksFailed: this.agentState?.tasksFailed ?? 0,
5173
+ processedTasks: this.processedTasks.size
5456
5174
  };
5457
5175
  }
5458
- getUpdateHint(pm, packages) {
5459
- const specs = packages.map((p) => `${p.name}@${p.latest}`).join(" ");
5460
- switch (pm) {
5461
- case "bun":
5462
- return `bun add ${specs}`;
5463
- case "npm":
5464
- return `npm install ${specs}`;
5465
- case "pnpm":
5466
- return `pnpm add ${specs}`;
5467
- case "yarn":
5468
- return `yarn add ${specs}`;
5469
- }
5470
- }
5471
- exec(bin, args, cwd) {
5472
- try {
5473
- const stdout = import_node_child_process7.execFileSync(bin, args, {
5474
- cwd,
5475
- encoding: "utf-8",
5476
- stdio: ["pipe", "pipe", "pipe"],
5477
- timeout: 120000
5478
- });
5479
- return { stdout, stderr: "", exitCode: 0 };
5480
- } catch (err) {
5481
- if (isExecError(err)) {
5482
- return {
5483
- stdout: err.stdout ?? "",
5484
- stderr: err.stderr ?? "",
5485
- exitCode: err.status ?? 1
5486
- };
5487
- }
5488
- throw err;
5489
- }
5490
- }
5491
- gitExec(args, cwd) {
5492
- return import_node_child_process7.execFileSync("git", args, {
5493
- cwd,
5494
- encoding: "utf-8",
5495
- stdio: ["pipe", "pipe", "pipe"]
5496
- });
5497
- }
5498
- }
5499
- function isExecError(err) {
5500
- return typeof err === "object" && err !== null && "status" in err && "stdout" in err;
5501
- }
5502
- // src/jobs/scans/lint-scan.ts
5503
- init_git_utils();
5504
- var import_node_child_process8 = require("node:child_process");
5505
- var import_node_fs10 = require("node:fs");
5506
- var import_node_path11 = require("node:path");
5507
- var import_shared5 = require("@locusai/shared");
5508
- class LintScanJob extends BaseJob {
5509
- type = import_shared5.JobType.LINT_SCAN;
5510
- name = "Linting Scan";
5511
- async run(context) {
5512
- const { projectPath, autonomyRules } = context;
5513
- const linter = this.detectLinter(projectPath);
5514
- if (!linter) {
5515
- return {
5516
- summary: "No linter configuration detected",
5517
- suggestions: [],
5518
- filesChanged: 0
5519
- };
5520
- }
5521
- let lintOutput;
5522
- try {
5523
- lintOutput = this.runLinter(linter.checkCommand, projectPath);
5524
- } catch (err) {
5525
- const message = err instanceof Error ? err.message : String(err);
5526
- return {
5527
- summary: `Linting scan failed: ${message}`,
5528
- suggestions: [],
5529
- filesChanged: 0,
5530
- errors: [message]
5531
- };
5532
- }
5533
- const parsed = this.parseLintOutput(linter.kind, lintOutput);
5534
- if (parsed.errors === 0 && parsed.warnings === 0) {
5535
- return {
5536
- summary: `Linting scan passed — no issues found (${linter.kind})`,
5537
- suggestions: [],
5538
- filesChanged: 0
5539
- };
5540
- }
5541
- const canAutoFix = this.shouldAutoExecute(import_shared5.ChangeCategory.STYLE, autonomyRules);
5542
- if (canAutoFix) {
5543
- return this.autoFix(linter, parsed, context);
5544
- }
5545
- return this.buildSuggestionResult(linter, parsed);
5546
- }
5547
- detectLinter(projectPath) {
5548
- if (import_node_fs10.existsSync(import_node_path11.join(projectPath, "biome.json")) || import_node_fs10.existsSync(import_node_path11.join(projectPath, "biome.jsonc"))) {
5549
- return {
5550
- kind: "biome",
5551
- checkCommand: ["bunx", "biome", "check", "."],
5552
- fixCommand: ["bunx", "biome", "check", "--fix", "."]
5553
- };
5554
- }
5555
- const eslintConfigPatterns = [
5556
- ".eslintrc",
5557
- ".eslintrc.js",
5558
- ".eslintrc.cjs",
5559
- ".eslintrc.json",
5560
- ".eslintrc.yml",
5561
- ".eslintrc.yaml"
5562
- ];
5563
- for (const config of eslintConfigPatterns) {
5564
- if (import_node_fs10.existsSync(import_node_path11.join(projectPath, config))) {
5565
- return {
5566
- kind: "eslint",
5567
- checkCommand: ["npx", "eslint", "."],
5568
- fixCommand: ["npx", "eslint", "--fix", "."]
5569
- };
5570
- }
5571
- }
5572
- try {
5573
- const files = import_node_fs10.readdirSync(projectPath);
5574
- const hasFlatConfig = files.some((f) => f.startsWith("eslint.config.") && /\.(js|cjs|mjs|ts|cts|mts)$/.test(f));
5575
- if (hasFlatConfig) {
5576
- return {
5577
- kind: "eslint",
5578
- checkCommand: ["npx", "eslint", "."],
5579
- fixCommand: ["npx", "eslint", "--fix", "."]
5580
- };
5581
- }
5582
- } catch {}
5583
- return null;
5584
- }
5585
- runLinter(command, projectPath) {
5586
- const [bin, ...args] = command;
5587
- try {
5588
- const stdout = import_node_child_process8.execFileSync(bin, args, {
5589
- cwd: projectPath,
5590
- encoding: "utf-8",
5591
- stdio: ["pipe", "pipe", "pipe"],
5592
- timeout: 120000
5593
- });
5594
- return { stdout, stderr: "", exitCode: 0 };
5595
- } catch (err) {
5596
- if (isExecError2(err)) {
5597
- return {
5598
- stdout: err.stdout ?? "",
5599
- stderr: err.stderr ?? "",
5600
- exitCode: err.status ?? 1
5601
- };
5602
- }
5603
- throw err;
5604
- }
5605
- }
5606
- parseLintOutput(kind, output) {
5607
- const combined = `${output.stdout}
5608
- ${output.stderr}`;
5609
- if (kind === "biome") {
5610
- return this.parseBiomeOutput(combined);
5611
- }
5612
- return this.parseEslintOutput(combined);
5613
- }
5614
- parseBiomeOutput(raw) {
5615
- let errors = 0;
5616
- let warnings = 0;
5617
- const errorMatch = raw.match(/Found (\d+) error/i);
5618
- if (errorMatch) {
5619
- errors = parseInt(errorMatch[1], 10);
5620
- }
5621
- const warningMatch = raw.match(/Found (\d+) warning/i);
5622
- if (warningMatch) {
5623
- warnings = parseInt(warningMatch[1], 10);
5624
- }
5625
- if (errors === 0 && warnings === 0) {
5626
- const diagnosticLines = raw.split(`
5627
- `).filter((line) => /\s+(error|warning)\[/.test(line));
5628
- errors = diagnosticLines.filter((l) => /\serror\[/.test(l)).length;
5629
- warnings = diagnosticLines.filter((l) => /\swarning\[/.test(l)).length;
5630
- }
5631
- return { errors, warnings, raw };
5632
- }
5633
- parseEslintOutput(raw) {
5634
- let errors = 0;
5635
- let warnings = 0;
5636
- const summaryMatch = raw.match(/(\d+) problems?\s*\((\d+) errors?,\s*(\d+) warnings?\)/);
5637
- if (summaryMatch) {
5638
- errors = parseInt(summaryMatch[2], 10);
5639
- warnings = parseInt(summaryMatch[3], 10);
5640
- } else {
5641
- const issueLines = raw.split(`
5642
- `).filter((line) => /^\s+\d+:\d+\s+(error|warning)\s/.test(line));
5643
- errors = issueLines.filter((l) => /\serror\s/.test(l)).length;
5644
- warnings = issueLines.filter((l) => /\swarning\s/.test(l)).length;
5645
- }
5646
- return { errors, warnings, raw };
5647
- }
5648
- autoFix(linter, parsed, context) {
5649
- const { projectPath } = context;
5650
- try {
5651
- this.runLinter(linter.fixCommand, projectPath);
5652
- } catch (err) {
5653
- const message = err instanceof Error ? err.message : String(err);
5654
- return {
5655
- summary: `Lint auto-fix failed: ${message}`,
5656
- suggestions: [],
5657
- filesChanged: 0,
5658
- errors: [message]
5659
- };
5660
- }
5661
- let changedFiles;
5662
- try {
5663
- const diffOutput = import_node_child_process8.execFileSync("git", ["diff", "--name-only"], {
5664
- cwd: projectPath,
5665
- encoding: "utf-8",
5666
- stdio: ["pipe", "pipe", "pipe"]
5667
- }).trim();
5668
- changedFiles = diffOutput ? diffOutput.split(`
5669
- `) : [];
5670
- } catch {
5671
- changedFiles = [];
5672
- }
5673
- if (changedFiles.length === 0) {
5674
- return {
5675
- summary: `Linting scan found ${this.issueCountSummary(parsed)} but auto-fix made no changes (${linter.kind})`,
5676
- suggestions: this.buildIssueSuggestions(linter, parsed),
5677
- filesChanged: 0
5678
- };
5679
- }
5680
- const prUrl = this.commitAndPush(projectPath, changedFiles, linter, parsed);
5681
- const summary = prUrl ? `Auto-fixed ${this.issueCountSummary(parsed)} across ${changedFiles.length} file(s) — PR created (${linter.kind})` : `Auto-fixed ${this.issueCountSummary(parsed)} across ${changedFiles.length} file(s) (${linter.kind})`;
5682
- return {
5683
- summary,
5684
- suggestions: [],
5685
- filesChanged: changedFiles.length,
5686
- prUrl: prUrl ?? undefined
5687
- };
5688
- }
5689
- commitAndPush(projectPath, changedFiles, linter, parsed) {
5690
- try {
5691
- const defaultBranch = getDefaultBranch(projectPath);
5692
- const branchName = `locus/lint-fix-${Date.now().toString(36)}`;
5693
- this.gitExec(["checkout", "-b", branchName], projectPath);
5694
- this.gitExec(["add", ...changedFiles], projectPath);
5695
- const commitMessage = `fix(lint): auto-fix ${this.issueCountSummary(parsed)} via ${linter.kind}
5696
-
5697
- Agent: locus-lint-scan
5698
- Co-authored-by: LocusAI <agent@locusai.team>`;
5699
- this.gitExec(["commit", "-m", commitMessage], projectPath);
5700
- this.gitExec(["push", "-u", "origin", branchName], projectPath);
5701
- let prUrl = null;
5702
- if (detectRemoteProvider(projectPath) === "github" && isGhAvailable(projectPath)) {
5703
- try {
5704
- const title = `[Locus] Auto-fix lint issues (${this.issueCountSummary(parsed)})`;
5705
- const body = [
5706
- "## Summary",
5707
- "",
5708
- `Automated lint fixes applied by Locus using \`${linter.kind}\`.`,
5709
- "",
5710
- `- **Issues found**: ${parsed.errors} error(s), ${parsed.warnings} warning(s)`,
5711
- `- **Files changed**: ${changedFiles.length}`,
5712
- "",
5713
- "### Changed files",
5714
- "",
5715
- ...changedFiles.map((f) => `- \`${f}\``),
5716
- "",
5717
- "---",
5718
- "*Created by Locus Agent (lint-scan)*"
5719
- ].join(`
5720
- `);
5721
- prUrl = import_node_child_process8.execFileSync("gh", [
5722
- "pr",
5723
- "create",
5724
- "--title",
5725
- title,
5726
- "--body",
5727
- body,
5728
- "--base",
5729
- defaultBranch,
5730
- "--head",
5731
- branchName
5732
- ], {
5733
- cwd: projectPath,
5734
- encoding: "utf-8",
5735
- stdio: ["pipe", "pipe", "pipe"]
5736
- }).trim();
5737
- } catch {}
5738
- }
5739
- try {
5740
- this.gitExec(["checkout", defaultBranch], projectPath);
5741
- } catch {}
5742
- return prUrl;
5743
- } catch {
5744
- return null;
5745
- }
5746
- }
5747
- buildSuggestionResult(linter, parsed) {
5748
- return {
5749
- summary: `Linting scan found ${this.issueCountSummary(parsed)} (${linter.kind})`,
5750
- suggestions: this.buildIssueSuggestions(linter, parsed),
5751
- filesChanged: 0
5752
- };
5753
- }
5754
- buildIssueSuggestions(linter, parsed) {
5755
- const suggestions2 = [];
5756
- if (parsed.errors > 0) {
5757
- suggestions2.push({
5758
- type: import_shared5.SuggestionType.CODE_FIX,
5759
- title: `Fix ${parsed.errors} lint error(s)`,
5760
- description: `The ${linter.kind} linter found ${parsed.errors} error(s). Run \`${linter.fixCommand.join(" ")}\` to auto-fix, or review the issues manually.`,
5761
- metadata: { linter: linter.kind, errors: parsed.errors }
5762
- });
5763
- }
5764
- if (parsed.warnings > 0) {
5765
- suggestions2.push({
5766
- type: import_shared5.SuggestionType.CODE_FIX,
5767
- title: `Resolve ${parsed.warnings} lint warning(s)`,
5768
- description: `The ${linter.kind} linter found ${parsed.warnings} warning(s). Run \`${linter.fixCommand.join(" ")}\` to auto-fix, or review the issues manually.`,
5769
- metadata: { linter: linter.kind, warnings: parsed.warnings }
5770
- });
5771
- }
5772
- return suggestions2;
5773
- }
5774
- issueCountSummary(parsed) {
5775
- const parts = [];
5776
- if (parsed.errors > 0)
5777
- parts.push(`${parsed.errors} error(s)`);
5778
- if (parsed.warnings > 0)
5779
- parts.push(`${parsed.warnings} warning(s)`);
5780
- return parts.join(", ") || "0 issues";
5781
- }
5782
- gitExec(args, cwd) {
5783
- return import_node_child_process8.execFileSync("git", args, {
5784
- cwd,
5785
- encoding: "utf-8",
5786
- stdio: ["pipe", "pipe", "pipe"]
5787
- });
5788
- }
5789
- }
5790
- function isExecError2(err) {
5791
- return typeof err === "object" && err !== null && "status" in err && "stdout" in err;
5792
- }
5793
- // src/jobs/scans/test-scan.ts
5794
- var import_node_child_process9 = require("node:child_process");
5795
- var import_node_fs11 = require("node:fs");
5796
- var import_node_path12 = require("node:path");
5797
- var import_shared6 = require("@locusai/shared");
5798
- class TestScanJob extends BaseJob {
5799
- type = import_shared6.JobType.FLAKY_TEST_DETECTION;
5800
- name = "Flaky Test Detection";
5801
- async run(context) {
5802
- const { projectPath } = context;
5803
- const retryCount = context.config.options?.retryCount ?? 2;
5804
- const runner = this.detectTestRunner(projectPath);
5805
- if (!runner) {
5806
- return {
5807
- summary: "No test runner detected — skipping flaky test detection",
5808
- suggestions: [],
5809
- filesChanged: 0
5810
- };
5811
- }
5812
- let firstRun;
5813
- try {
5814
- firstRun = this.runTests(runner, projectPath);
5815
- } catch (err) {
5816
- const message = err instanceof Error ? err.message : String(err);
5817
- return {
5818
- summary: `Test scan failed: ${message}`,
5819
- suggestions: [],
5820
- filesChanged: 0,
5821
- errors: [message]
5822
- };
5823
- }
5824
- if (firstRun.total === 0) {
5825
- return {
5826
- summary: "No tests found in project",
5827
- suggestions: [],
5828
- filesChanged: 0
5829
- };
5830
- }
5831
- if (firstRun.failed === 0) {
5832
- return {
5833
- summary: `All ${firstRun.total} test(s) passed (${runner.kind}) — no flaky tests detected`,
5834
- suggestions: [],
5835
- filesChanged: 0
5836
- };
5837
- }
5838
- const allRuns = [
5839
- { runIndex: 0, results: this.indexByKey(firstRun.tests) }
5840
- ];
5841
- for (let i = 1;i <= retryCount; i++) {
5842
- try {
5843
- const rerun = this.runTests(runner, projectPath);
5844
- allRuns.push({ runIndex: i, results: this.indexByKey(rerun.tests) });
5845
- } catch {}
5846
- }
5847
- const { flaky, broken } = this.classifyTests(allRuns, firstRun.tests);
5848
- const suggestions2 = [];
5849
- for (const test of flaky) {
5850
- suggestions2.push({
5851
- type: import_shared6.SuggestionType.TEST_FIX,
5852
- title: `Flaky test: ${test.testName}`,
5853
- description: [
5854
- `**File:** \`${test.testFile}\``,
5855
- `**Test:** ${test.testName}`,
5856
- `**Pass rate:** ${test.passRate}/${allRuns.length} runs`,
5857
- `**Failure messages:**`,
5858
- ...test.failureMessages.map((m) => `> ${m}`)
5859
- ].join(`
5860
- `),
5861
- metadata: {
5862
- testFile: test.testFile,
5863
- testName: test.testName,
5864
- passRate: `${test.passRate}/${allRuns.length}`,
5865
- failureMessages: test.failureMessages,
5866
- category: "flaky"
5867
- }
5868
- });
5869
- }
5870
- for (const test of broken) {
5871
- suggestions2.push({
5872
- type: import_shared6.SuggestionType.TEST_FIX,
5873
- title: `Broken test: ${test.testName}`,
5874
- description: [
5875
- `**File:** \`${test.testFile}\``,
5876
- `**Test:** ${test.testName}`,
5877
- `**Status:** Failed in all ${allRuns.length} run(s)`,
5878
- `**Failure messages:**`,
5879
- ...test.failureMessages.map((m) => `> ${m}`)
5880
- ].join(`
5881
- `),
5882
- metadata: {
5883
- testFile: test.testFile,
5884
- testName: test.testName,
5885
- passRate: `0/${allRuns.length}`,
5886
- failureMessages: test.failureMessages,
5887
- category: "broken"
5888
- }
5889
- });
5890
- }
5891
- const summary = [
5892
- `Test health (${runner.kind}):`,
5893
- `${firstRun.total} total,`,
5894
- `${firstRun.passed} passing,`,
5895
- `${firstRun.failed} failing,`,
5896
- `${flaky.length} flaky,`,
5897
- `${broken.length} broken`
5898
- ].join(" ");
5899
- return {
5900
- summary,
5901
- suggestions: suggestions2,
5902
- filesChanged: 0
5903
- };
5904
- }
5905
- detectTestRunner(projectPath) {
5906
- try {
5907
- const files = import_node_fs11.readdirSync(projectPath);
5908
- if (files.some((f) => f.startsWith("jest.config."))) {
5909
- return {
5910
- kind: "jest",
5911
- command: ["npx", "jest", "--json", "--no-coverage"]
5912
- };
5913
- }
5914
- } catch {}
5915
- try {
5916
- const files = import_node_fs11.readdirSync(projectPath);
5917
- if (files.some((f) => f.startsWith("vitest.config."))) {
5918
- return {
5919
- kind: "vitest",
5920
- command: ["npx", "vitest", "run", "--reporter=json"]
5921
- };
5922
- }
5923
- } catch {}
5924
- const pkgJsonPath = import_node_path12.join(projectPath, "package.json");
5925
- if (import_node_fs11.existsSync(pkgJsonPath)) {
5926
- try {
5927
- const pkgJson = JSON.parse(import_node_fs11.readFileSync(pkgJsonPath, "utf-8"));
5928
- if (pkgJson.jest || pkgJson.devDependencies?.jest || pkgJson.dependencies?.jest) {
5929
- return {
5930
- kind: "jest",
5931
- command: ["npx", "jest", "--json", "--no-coverage"]
5932
- };
5933
- }
5934
- if (pkgJson.devDependencies?.vitest || pkgJson.dependencies?.vitest) {
5935
- return {
5936
- kind: "vitest",
5937
- command: ["npx", "vitest", "run", "--reporter=json"]
5938
- };
5939
- }
5940
- if (pkgJson.devDependencies?.mocha || pkgJson.dependencies?.mocha) {
5941
- return {
5942
- kind: "mocha",
5943
- command: ["npx", "mocha", "--reporter=json"]
5944
- };
5945
- }
5946
- if (pkgJson.scripts?.test && pkgJson.scripts.test !== 'echo "Error: no test specified" && exit 1') {
5947
- return {
5948
- kind: "fallback",
5949
- command: ["npm", "test", "--", "--no-coverage"]
5950
- };
5951
- }
5952
- } catch {}
5953
- }
5954
- if (import_node_fs11.existsSync(import_node_path12.join(projectPath, "bun.lock")) || import_node_fs11.existsSync(import_node_path12.join(projectPath, "bunfig.toml"))) {
5955
- return { kind: "fallback", command: ["bun", "test"] };
5956
- }
5957
- return null;
5958
- }
5959
- runTests(runner, projectPath) {
5960
- const output = this.exec(runner.command, projectPath);
5961
- switch (runner.kind) {
5962
- case "jest":
5963
- return this.parseJestOutput(output);
5964
- case "vitest":
5965
- return this.parseVitestOutput(output);
5966
- case "mocha":
5967
- return this.parseMochaOutput(output);
5968
- default:
5969
- return this.parseFallbackOutput(output);
5970
- }
5971
- }
5972
- parseJestOutput(output) {
5973
- const combined = `${output.stdout}
5974
- ${output.stderr}`;
5975
- const tests = [];
5976
- let total = 0;
5977
- let passed = 0;
5978
- let failed = 0;
5979
- let skipped = 0;
5980
- const jsonData = this.extractJson(combined);
5981
- if (jsonData) {
5982
- total = jsonData.numTotalTests ?? 0;
5983
- passed = jsonData.numPassedTests ?? 0;
5984
- failed = jsonData.numFailedTests ?? 0;
5985
- skipped = jsonData.numPendingTests ?? 0;
5986
- const testResults = jsonData.testResults ?? [];
5987
- for (const suite of testResults) {
5988
- const filePath = suite.testFilePath ?? suite.name ?? "unknown";
5989
- const assertionResults = suite.assertionResults ?? suite.testResults ?? [];
5990
- for (const test of assertionResults) {
5991
- const ancestors = Array.isArray(test.ancestorTitles) ? test.ancestorTitles.join(" > ") : "";
5992
- const testName = ancestors ? `${ancestors} > ${test.title ?? test.fullName ?? "unknown"}` : test.title ?? test.fullName ?? "unknown";
5993
- tests.push({
5994
- testFile: filePath,
5995
- testName,
5996
- status: test.status === "passed" ? "passed" : test.status === "pending" ? "skipped" : "failed",
5997
- failureMessages: Array.isArray(test.failureMessages) ? test.failureMessages.map(String) : []
5998
- });
5999
- }
6000
- }
6001
- }
6002
- return { total, passed, failed, skipped, tests };
6003
- }
6004
- parseVitestOutput(output) {
6005
- return this.parseJestOutput(output);
6006
- }
6007
- parseMochaOutput(output) {
6008
- const combined = `${output.stdout}
6009
- ${output.stderr}`;
6010
- const tests = [];
6011
- let total = 0;
6012
- let passed = 0;
6013
- let failed = 0;
6014
- let skipped = 0;
6015
- const jsonData = this.extractJson(combined);
6016
- if (jsonData?.stats) {
6017
- total = jsonData.stats.tests ?? 0;
6018
- passed = jsonData.stats.passes ?? 0;
6019
- failed = jsonData.stats.failures ?? 0;
6020
- skipped = jsonData.stats.pending ?? 0;
6021
- if (Array.isArray(jsonData.passes)) {
6022
- for (const t of jsonData.passes) {
6023
- tests.push({
6024
- testFile: t.file ?? "unknown",
6025
- testName: t.fullTitle ?? t.title ?? "unknown",
6026
- status: "passed",
6027
- failureMessages: []
6028
- });
6029
- }
6030
- }
6031
- if (Array.isArray(jsonData.failures)) {
6032
- for (const t of jsonData.failures) {
6033
- tests.push({
6034
- testFile: t.file ?? "unknown",
6035
- testName: t.fullTitle ?? t.title ?? "unknown",
6036
- status: "failed",
6037
- failureMessages: t.err?.message ? [t.err.message] : []
6038
- });
6039
- }
6040
- }
6041
- if (Array.isArray(jsonData.pending)) {
6042
- for (const t of jsonData.pending) {
6043
- tests.push({
6044
- testFile: t.file ?? "unknown",
6045
- testName: t.fullTitle ?? t.title ?? "unknown",
6046
- status: "skipped",
6047
- failureMessages: []
6048
- });
6049
- }
6050
- }
6051
- }
6052
- return { total, passed, failed, skipped, tests };
6053
- }
6054
- parseFallbackOutput(output) {
6055
- const combined = `${output.stdout}
6056
- ${output.stderr}`;
6057
- let total = 0;
6058
- let passed = 0;
6059
- let failed = 0;
6060
- let skipped = 0;
6061
- const jestSummary = combined.match(/Tests:\s*(?:(\d+)\s+failed,\s*)?(?:(\d+)\s+skipped,\s*)?(?:(\d+)\s+passed,\s*)?(\d+)\s+total/i);
6062
- if (jestSummary) {
6063
- failed = parseInt(jestSummary[1] ?? "0", 10);
6064
- skipped = parseInt(jestSummary[2] ?? "0", 10);
6065
- passed = parseInt(jestSummary[3] ?? "0", 10);
6066
- total = parseInt(jestSummary[4], 10);
6067
- }
6068
- if (total === 0) {
6069
- const vitestSummary = combined.match(/(\d+)\s+passed.*?(\d+)\s+failed.*?(\d+)\s+total/i);
6070
- if (vitestSummary) {
6071
- passed = parseInt(vitestSummary[1], 10);
6072
- failed = parseInt(vitestSummary[2], 10);
6073
- total = parseInt(vitestSummary[3], 10);
6074
- }
6075
- }
6076
- if (total === 0) {
6077
- const lines = combined.split(`
6078
- `);
6079
- for (const line of lines) {
6080
- if (/\bpass(ed|ing)?\b/i.test(line))
6081
- passed++;
6082
- if (/\bfail(ed|ing|ure)?\b/i.test(line))
6083
- failed++;
6084
- if (/\bskip(ped)?\b/i.test(line))
6085
- skipped++;
6086
- }
6087
- total = passed + failed + skipped;
6088
- }
6089
- return { total, passed, failed, skipped, tests: [] };
6090
- }
6091
- classifyTests(allRuns, _firstRunTests) {
6092
- const flaky = [];
6093
- const broken = [];
6094
- const failedKeys = new Set;
6095
- for (const run of allRuns) {
6096
- for (const [key, result] of run.results) {
6097
- if (result.status === "failed") {
6098
- failedKeys.add(key);
6099
- }
6100
- }
6101
- }
6102
- for (const key of failedKeys) {
6103
- let passCount = 0;
6104
- let failCount = 0;
6105
- const failureMessages = [];
6106
- let testFile = "unknown";
6107
- let testName = key;
6108
- for (const run of allRuns) {
6109
- const result = run.results.get(key);
6110
- if (!result)
6111
- continue;
6112
- testFile = result.testFile;
6113
- testName = result.testName;
6114
- if (result.status === "passed") {
6115
- passCount++;
6116
- } else if (result.status === "failed") {
6117
- failCount++;
6118
- for (const msg of result.failureMessages) {
6119
- if (msg && !failureMessages.includes(msg)) {
6120
- failureMessages.push(msg);
6121
- }
6122
- }
6123
- }
6124
- }
6125
- const truncatedMessages = failureMessages.map((m) => m.length > 500 ? `${m.slice(0, 500)}...` : m);
6126
- if (passCount > 0 && failCount > 0) {
6127
- flaky.push({
6128
- testFile,
6129
- testName,
6130
- passRate: passCount,
6131
- failureMessages: truncatedMessages
6132
- });
6133
- } else if (failCount > 0 && passCount === 0) {
6134
- broken.push({
6135
- testFile,
6136
- testName,
6137
- passRate: 0,
6138
- failureMessages: truncatedMessages
6139
- });
6140
- }
6141
- }
6142
- return { flaky, broken };
6143
- }
6144
- testKey(test) {
6145
- return `${test.testFile}::${test.testName}`;
6146
- }
6147
- indexByKey(tests) {
6148
- const map = new Map;
6149
- for (const test of tests) {
6150
- map.set(this.testKey(test), test);
6151
- }
6152
- return map;
6153
- }
6154
- extractJson(text) {
6155
- const firstBrace = text.indexOf("{");
6156
- if (firstBrace === -1)
6157
- return null;
6158
- let depth = 0;
6159
- let lastBrace = -1;
6160
- for (let i = firstBrace;i < text.length; i++) {
6161
- if (text[i] === "{")
6162
- depth++;
6163
- else if (text[i] === "}") {
6164
- depth--;
6165
- if (depth === 0) {
6166
- lastBrace = i;
6167
- break;
6168
- }
6169
- }
6170
- }
6171
- if (lastBrace === -1)
6172
- return null;
6173
- try {
6174
- return JSON.parse(text.slice(firstBrace, lastBrace + 1));
6175
- } catch {
6176
- return null;
6177
- }
6178
- }
6179
- exec(command, projectPath) {
6180
- const [bin, ...args] = command;
6181
- try {
6182
- const stdout = import_node_child_process9.execFileSync(bin, args, {
6183
- cwd: projectPath,
6184
- encoding: "utf-8",
6185
- stdio: ["pipe", "pipe", "pipe"],
6186
- timeout: 300000
6187
- });
6188
- return { stdout, stderr: "", exitCode: 0 };
6189
- } catch (err) {
6190
- if (isExecError3(err)) {
6191
- return {
6192
- stdout: err.stdout ?? "",
6193
- stderr: err.stderr ?? "",
6194
- exitCode: err.status ?? 1
6195
- };
6196
- }
6197
- throw err;
6198
- }
6199
- }
6200
- }
6201
- function isExecError3(err) {
6202
- return typeof err === "object" && err !== null && "status" in err && "stdout" in err;
6203
- }
6204
- // src/jobs/scans/todo-scan.ts
6205
- var import_node_child_process10 = require("node:child_process");
6206
- var import_node_path13 = require("node:path");
6207
- var import_shared7 = require("@locusai/shared");
6208
- class TodoScanJob extends BaseJob {
6209
- type = import_shared7.JobType.TODO_CLEANUP;
6210
- name = "TODO Cleanup";
6211
- async run(context) {
6212
- const { projectPath } = context;
6213
- let output;
6214
- try {
6215
- output = this.scanForTodos(projectPath);
6216
- } catch (err) {
6217
- const message = err instanceof Error ? err.message : String(err);
6218
- return {
6219
- summary: `TODO scan failed: ${message}`,
6220
- suggestions: [],
6221
- filesChanged: 0,
6222
- errors: [message]
6223
- };
6224
- }
6225
- const items = this.parseGrepOutput(output.stdout, projectPath);
6226
- if (items.length === 0) {
6227
- return {
6228
- summary: "TODO scan passed — no TODO/FIXME/HACK/XXX comments found",
6229
- suggestions: [],
6230
- filesChanged: 0
6231
- };
6232
- }
6233
- const grouped = this.groupByType(items);
6234
- const suggestions2 = this.buildSuggestions(items);
6235
- const summary = this.buildSummary(grouped, items, context);
6236
- return {
6237
- summary,
6238
- suggestions: suggestions2,
6239
- filesChanged: 0
6240
- };
6241
- }
6242
- scanForTodos(projectPath) {
6243
- const args = [
6244
- "-rn",
6245
- "--include=*.ts",
6246
- "--include=*.tsx",
6247
- "--include=*.js",
6248
- "--include=*.jsx",
6249
- "--exclude-dir=node_modules",
6250
- "--exclude-dir=.git",
6251
- "--exclude-dir=dist",
6252
- "--exclude-dir=build",
6253
- "--exclude-dir=.locus",
6254
- "-E",
6255
- "(TODO|FIXME|HACK|XXX):?",
6256
- projectPath
6257
- ];
6258
- return this.exec("grep", args, projectPath);
6259
- }
6260
- parseGrepOutput(stdout, projectPath) {
6261
- const items = [];
6262
- const lines = stdout.split(`
6263
- `);
6264
- for (const line of lines) {
6265
- if (!line.trim())
6266
- continue;
6267
- const match = line.match(/^(.+?):(\d+):(.+)$/);
6268
- if (!match)
6269
- continue;
6270
- const [, filePath, lineNum, content] = match;
6271
- const typeMatch = content.match(/\b(TODO|FIXME|HACK|XXX):?\s*(.*)/);
6272
- if (!typeMatch)
6273
- continue;
6274
- const todoType = typeMatch[1];
6275
- const text = typeMatch[2].trim() || content.trim();
6276
- const relFile = import_node_path13.relative(projectPath, filePath);
6277
- items.push({
6278
- file: relFile,
6279
- line: parseInt(lineNum, 10),
6280
- type: todoType,
6281
- text
6282
- });
6283
- }
6284
- return items;
6285
- }
6286
- groupByType(items) {
6287
- const grouped = {
6288
- TODO: [],
6289
- FIXME: [],
6290
- HACK: [],
6291
- XXX: []
6292
- };
6293
- for (const item of items) {
6294
- grouped[item.type].push(item);
6295
- }
6296
- return grouped;
6297
- }
6298
- buildSuggestions(items) {
6299
- return items.map((item) => ({
6300
- type: import_shared7.SuggestionType.CODE_FIX,
6301
- title: `${item.type} in ${item.file}:${item.line}`,
6302
- description: `${item.type} comment found: ${item.text}`,
6303
- metadata: {
6304
- file: item.file,
6305
- line: item.line,
6306
- todoType: item.type,
6307
- text: item.text
6308
- }
6309
- }));
6310
- }
6311
- buildSummary(grouped, items, context) {
6312
- const uniqueFiles = new Set(items.map((i) => i.file)).size;
6313
- const counts = [];
6314
- if (grouped.TODO.length > 0)
6315
- counts.push(`${grouped.TODO.length} TODOs`);
6316
- if (grouped.FIXME.length > 0)
6317
- counts.push(`${grouped.FIXME.length} FIXMEs`);
6318
- if (grouped.HACK.length > 0)
6319
- counts.push(`${grouped.HACK.length} HACKs`);
6320
- if (grouped.XXX.length > 0)
6321
- counts.push(`${grouped.XXX.length} XXXs`);
6322
- let summary = `Found ${counts.join(", ")} across ${uniqueFiles} file(s)`;
6323
- const previousCount = context.config.options?.previousTodoCount;
6324
- if (typeof previousCount === "number") {
6325
- const diff = previousCount - items.length;
6326
- if (diff > 0) {
6327
- summary += ` (${diff} resolved since last run)`;
6328
- } else if (diff < 0) {
6329
- summary += ` (${Math.abs(diff)} new since last run)`;
6330
- }
6331
- }
6332
- return summary;
6333
- }
6334
- exec(bin, args, cwd) {
6335
- try {
6336
- const stdout = import_node_child_process10.execFileSync(bin, args, {
6337
- cwd,
6338
- encoding: "utf-8",
6339
- stdio: ["pipe", "pipe", "pipe"],
6340
- timeout: 120000
6341
- });
6342
- return { stdout, stderr: "", exitCode: 0 };
6343
- } catch (err) {
6344
- if (isExecError4(err)) {
6345
- return {
6346
- stdout: err.stdout ?? "",
6347
- stderr: err.stderr ?? "",
6348
- exitCode: err.status ?? 1
6349
- };
6350
- }
6351
- throw err;
6352
- }
6353
- }
6354
- }
6355
- function isExecError4(err) {
6356
- return typeof err === "object" && err !== null && "status" in err && "stdout" in err;
6357
- }
6358
- // src/jobs/default-registry.ts
6359
- function createDefaultRegistry() {
6360
- const registry = new JobRegistry;
6361
- registry.register(new LintScanJob);
6362
- registry.register(new DependencyScanJob);
6363
- registry.register(new TodoScanJob);
6364
- registry.register(new TestScanJob);
6365
- return registry;
6366
- }
6367
- // src/jobs/job-runner.ts
6368
- var import_shared8 = require("@locusai/shared");
6369
- var JobEvent;
6370
- ((JobEvent2) => {
6371
- JobEvent2["JOB_STARTED"] = "JOB_STARTED";
6372
- JobEvent2["JOB_COMPLETED"] = "JOB_COMPLETED";
6373
- JobEvent2["JOB_FAILED"] = "JOB_FAILED";
6374
- })(JobEvent ||= {});
6375
-
6376
- class JobRunner {
6377
- registry;
6378
- client;
6379
- projectPath;
6380
- workspaceId;
6381
- constructor(registry, client, projectPath, workspaceId) {
6382
- this.registry = registry;
6383
- this.client = client;
6384
- this.projectPath = projectPath;
6385
- this.workspaceId = workspaceId;
6386
- }
6387
- async runJob(jobType, config, autonomyRules) {
6388
- const job = this.registry.get(jobType);
6389
- if (!job) {
6390
- throw new Error(`No job handler registered for type: ${jobType}`);
6391
- }
6392
- const jobRun = await this.client.jobs.create(this.workspaceId, {
6393
- jobType,
6394
- status: import_shared8.JobStatus.RUNNING,
6395
- startedAt: new Date().toISOString()
6396
- });
6397
- this.client.emitter.emit("JOB_STARTED" /* JOB_STARTED */, {
6398
- jobType,
6399
- jobRunId: jobRun.id
6400
- });
6401
- const context = {
6402
- workspaceId: this.workspaceId,
6403
- projectPath: this.projectPath,
6404
- config,
6405
- autonomyRules,
6406
- client: this.client
6407
- };
6408
- try {
6409
- const result = await job.run(context);
6410
- await this.client.jobs.update(this.workspaceId, jobRun.id, {
6411
- status: import_shared8.JobStatus.COMPLETED,
6412
- completedAt: new Date().toISOString(),
6413
- result: {
6414
- summary: result.summary,
6415
- filesChanged: result.filesChanged,
6416
- prUrl: result.prUrl,
6417
- errors: result.errors
6418
- }
6419
- });
6420
- for (const suggestion of result.suggestions) {
6421
- await this.client.suggestions.create(this.workspaceId, {
6422
- type: suggestion.type,
6423
- title: suggestion.title,
6424
- description: suggestion.description,
6425
- jobRunId: jobRun.id,
6426
- metadata: suggestion.metadata
6427
- });
6428
- }
6429
- this.client.emitter.emit("JOB_COMPLETED" /* JOB_COMPLETED */, {
6430
- jobType,
6431
- jobRunId: jobRun.id,
6432
- result
6433
- });
6434
- return result;
6435
- } catch (err) {
6436
- const errorMessage = err instanceof Error ? err.message : String(err);
6437
- await this.client.jobs.update(this.workspaceId, jobRun.id, {
6438
- status: import_shared8.JobStatus.FAILED,
6439
- completedAt: new Date().toISOString(),
6440
- error: errorMessage
6441
- }).catch(() => {});
6442
- this.client.emitter.emit("JOB_FAILED" /* JOB_FAILED */, {
6443
- jobType,
6444
- jobRunId: jobRun.id,
6445
- error: errorMessage
6446
- });
6447
- throw err;
6448
- }
6449
- }
6450
- async runAllEnabled(configs, autonomyRules) {
6451
- const results = new Map;
6452
- const enabledConfigs = configs.filter((c2) => c2.enabled && this.registry.has(c2.type));
6453
- for (const config of enabledConfigs) {
6454
- try {
6455
- const result = await this.runJob(config.type, config, autonomyRules);
6456
- results.set(config.type, result);
6457
- } catch {}
6458
- }
6459
- return results;
6460
- }
6461
- }
6462
- // src/jobs/proposals/context-gatherer.ts
6463
- var import_node_child_process11 = require("node:child_process");
6464
- var import_node_fs12 = require("node:fs");
6465
- var import_node_path14 = require("node:path");
6466
-
6467
- class ContextGatherer {
6468
- async gather(projectPath, client, workspaceId) {
6469
- const [jobRuns, activeSprint, allTasks, skippedSuggestions] = await Promise.all([
6470
- this.fetchJobRuns(client, workspaceId),
6471
- this.fetchActiveSprint(client, workspaceId),
6472
- this.fetchTasks(client, workspaceId),
6473
- this.fetchSkippedSuggestions(client, workspaceId)
6474
- ]);
6475
- const sprintTasks = activeSprint ? allTasks.filter((t) => t.sprintId === activeSprint.id) : [];
6476
- const backlogTasks = allTasks.filter((t) => !t.sprintId);
6477
- const gitLog = this.readGitLog(projectPath);
6478
- const artifactContents = this.readArtifacts(projectPath);
6479
- const locusInstructions = this.readLocusInstructions(projectPath);
6480
- return {
6481
- jobRuns,
6482
- activeSprint,
6483
- sprintTasks,
6484
- backlogTasks,
6485
- gitLog,
6486
- artifactContents,
6487
- locusInstructions,
6488
- skippedSuggestions
6489
- };
6490
- }
6491
- async fetchJobRuns(client, workspaceId) {
6492
- try {
6493
- return await client.jobs.list(workspaceId, { limit: 10 });
6494
- } catch {
6495
- return [];
6496
- }
6497
- }
6498
- async fetchActiveSprint(client, workspaceId) {
6499
- try {
6500
- return await client.sprints.getActive(workspaceId);
6501
- } catch {
6502
- return null;
6503
- }
6504
- }
6505
- async fetchTasks(client, workspaceId) {
6506
- try {
6507
- return await client.tasks.list(workspaceId);
6508
- } catch {
6509
- return [];
6510
- }
6511
- }
6512
- async fetchSkippedSuggestions(client, workspaceId) {
6513
- try {
6514
- return await client.suggestions.list(workspaceId, { status: "SKIPPED" });
6515
- } catch {
6516
- return [];
6517
- }
6518
- }
6519
- readGitLog(projectPath) {
6520
- try {
6521
- return import_node_child_process11.execFileSync("git", ["log", "--oneline", "--no-decorate", "-n", "20"], {
6522
- cwd: projectPath,
6523
- encoding: "utf-8",
6524
- timeout: 1e4,
6525
- stdio: ["pipe", "pipe", "pipe"]
6526
- }).trim();
6527
- } catch {
6528
- return "";
6529
- }
6530
- }
6531
- readArtifacts(projectPath) {
6532
- const artifactsDir = import_node_path14.join(projectPath, ".locus", "artifacts");
6533
- if (!import_node_fs12.existsSync(artifactsDir))
6534
- return [];
6535
- try {
6536
- const files = import_node_fs12.readdirSync(artifactsDir).filter((f) => f.endsWith(".md"));
6537
- return files.slice(0, 10).map((name) => ({
6538
- name,
6539
- content: import_node_fs12.readFileSync(import_node_path14.join(artifactsDir, name), "utf-8").slice(0, 2000)
6540
- }));
6541
- } catch {
6542
- return [];
6543
- }
6544
- }
6545
- readLocusInstructions(projectPath) {
6546
- const locusPath = import_node_path14.join(projectPath, ".locus", "LOCUS.md");
6547
- if (!import_node_fs12.existsSync(locusPath))
6548
- return null;
6549
- try {
6550
- return import_node_fs12.readFileSync(locusPath, "utf-8").slice(0, 3000);
6551
- } catch {
6552
- return null;
6553
- }
6554
- }
6555
- }
6556
- // src/jobs/proposals/proposal-engine.ts
6557
- init_factory();
6558
- var import_shared9 = require("@locusai/shared");
6559
- class ProposalEngine {
6560
- contextGatherer;
6561
- constructor(contextGatherer) {
6562
- this.contextGatherer = contextGatherer ?? new ContextGatherer;
6563
- }
6564
- async runProposalCycle(projectPath, client, workspaceId) {
6565
- const context = await this.contextGatherer.gather(projectPath, client, workspaceId);
6566
- return this.generateProposals(context, projectPath, client, workspaceId);
6567
- }
6568
- async generateProposals(context, projectPath, client, workspaceId) {
6569
- const prompt = this.buildPrompt(context);
6570
- const runner = createAiRunner(undefined, {
6571
- projectPath,
6572
- timeoutMs: 5 * 60 * 1000,
6573
- maxTurns: 1
6574
- });
6575
- let aiResponse;
6576
- try {
6577
- aiResponse = await runner.run(prompt);
6578
- } catch {
6579
- return [];
6580
- }
6581
- const proposals = this.parseResponse(aiResponse);
6582
- const created = [];
6583
- for (const proposal of proposals) {
6584
- if (this.isDuplicate(proposal.title, context.skippedSuggestions)) {
6585
- continue;
6586
- }
6587
- try {
6588
- const suggestion = await client.suggestions.create(workspaceId, {
6589
- type: import_shared9.SuggestionType.NEXT_STEP,
6590
- title: proposal.title,
6591
- description: proposal.description,
6592
- metadata: {
6593
- complexity: proposal.complexity,
6594
- relatedBacklogItem: proposal.relatedBacklogItem,
6595
- source: "proposal-engine"
6596
- }
6597
- });
6598
- created.push(suggestion);
6599
- } catch {}
6600
- }
6601
- return created;
6602
- }
6603
- buildPrompt(context) {
6604
- const sections = [];
6605
- sections.push("You are a proactive software engineering advisor. Based on the project context below, propose 1-3 high-value next steps the team should take. Focus on actionable, impactful work.");
6606
- if (context.jobRuns.length > 0) {
6607
- const jobSummaries = context.jobRuns.filter((j) => j.result).map((j) => `- [${j.jobType}] ${j.status}: ${j.result?.summary ?? "No summary"} (${j.result?.filesChanged ?? 0} files changed)`).join(`
6608
- `);
6609
- if (jobSummaries) {
6610
- sections.push(`## Recent Job Results
6611
- ${jobSummaries}`);
6612
- }
6613
- }
6614
- if (context.activeSprint) {
6615
- const sprintInfo = `Sprint: ${context.activeSprint.name} (${context.activeSprint.status})`;
6616
- const tasksByStatus = this.groupTasksByStatus(context.sprintTasks);
6617
- sections.push(`## Current Sprint
6618
- ${sprintInfo}
6619
- ${tasksByStatus}`);
6620
- }
6621
- if (context.backlogTasks.length > 0) {
6622
- const backlogList = context.backlogTasks.slice(0, 15).map((t) => `- [${t.priority}] ${t.title}${t.description ? `: ${t.description.slice(0, 100)}` : ""}`).join(`
6623
- `);
6624
- sections.push(`## Backlog Items
6625
- ${backlogList}`);
6626
- }
6627
- if (context.gitLog) {
6628
- sections.push(`## Recent Commits (last 20)
6629
- ${context.gitLog}`);
6630
- }
6631
- if (context.artifactContents.length > 0) {
6632
- const artifacts = context.artifactContents.map((a) => `### ${a.name}
6633
- ${a.content}`).join(`
6634
-
6635
- `);
6636
- sections.push(`## Product Context
6637
- ${artifacts}`);
6638
- }
6639
- if (context.locusInstructions) {
6640
- sections.push(`## Project Instructions
6641
- ${context.locusInstructions}`);
6642
- }
6643
- if (context.skippedSuggestions.length > 0) {
6644
- const skipped = context.skippedSuggestions.map((s) => `- ${s.title}`).join(`
6645
- `);
6646
- sections.push(`## Previously Skipped Proposals (do NOT re-propose these)
6647
- ${skipped}`);
6648
- }
6649
- sections.push(`## Instructions
6650
- Propose 1-3 high-value next steps. For each, respond with exactly this format:
6651
-
6652
- PROPOSAL_START
6653
- Title: <clear, concise title>
6654
- Description: <what to do and why, 2-4 sentences>
6655
- Complexity: <low|medium|high>
6656
- Related Backlog: <title of related backlog item, or "none">
6657
- PROPOSAL_END
6658
-
6659
- Rules:
6660
- - Focus on what would deliver the most value right now
6661
- - Consider what the recent job results revealed (bugs, tech debt, missing tests)
6662
- - Align with the current sprint goals when possible
6663
- - Don't propose things that are already in progress
6664
- - Don't re-propose previously skipped suggestions
6665
- - Keep proposals specific and actionable`);
6666
- return sections.join(`
6667
-
6668
- `);
6669
- }
6670
- parseResponse(response) {
6671
- const proposals = [];
6672
- const blocks = response.split("PROPOSAL_START");
6673
- for (const block of blocks) {
6674
- const endIdx = block.indexOf("PROPOSAL_END");
6675
- if (endIdx === -1)
6676
- continue;
6677
- const content = block.slice(0, endIdx).trim();
6678
- const title = this.extractField(content, "Title");
6679
- const description = this.extractField(content, "Description");
6680
- const complexity = this.extractField(content, "Complexity") ?? "medium";
6681
- const relatedRaw = this.extractField(content, "Related Backlog");
6682
- const relatedBacklogItem = relatedRaw && relatedRaw.toLowerCase() !== "none" ? relatedRaw : null;
6683
- if (title && description) {
6684
- proposals.push({
6685
- title: title.slice(0, 200),
6686
- description: description.slice(0, 2000),
6687
- complexity: complexity.toLowerCase(),
6688
- relatedBacklogItem
6689
- });
6690
- }
6691
- }
6692
- return proposals.slice(0, 3);
6693
- }
6694
- extractField(content, field) {
6695
- const regex = new RegExp(`^${field}:\\s*(.+)`, "im");
6696
- const match = content.match(regex);
6697
- return match ? match[1].trim() : null;
6698
- }
6699
- isDuplicate(title, skipped) {
6700
- const normalized = title.toLowerCase().trim();
6701
- return skipped.some((s) => {
6702
- const skippedNorm = s.title.toLowerCase().trim();
6703
- return skippedNorm === normalized || skippedNorm.includes(normalized) || normalized.includes(skippedNorm);
6704
- });
6705
- }
6706
- groupTasksByStatus(tasks2) {
6707
- const groups = {};
6708
- for (const t of tasks2) {
6709
- groups[t.status] = (groups[t.status] ?? 0) + 1;
6710
- }
6711
- return Object.entries(groups).map(([status, count]) => `- ${status}: ${count} task(s)`).join(`
6712
- `);
6713
- }
6714
- }
6715
- // src/jobs/scheduler.ts
6716
- var import_node_cron = __toESM(require("node-cron"));
6717
- var SchedulerEvent;
6718
- ((SchedulerEvent2) => {
6719
- SchedulerEvent2["SCHEDULER_STARTED"] = "SCHEDULER_STARTED";
6720
- SchedulerEvent2["SCHEDULER_STOPPED"] = "SCHEDULER_STOPPED";
6721
- SchedulerEvent2["JOB_SCHEDULED"] = "JOB_SCHEDULED";
6722
- SchedulerEvent2["JOB_TRIGGERED"] = "JOB_TRIGGERED";
6723
- SchedulerEvent2["JOB_SKIPPED"] = "JOB_SKIPPED";
6724
- SchedulerEvent2["CONFIG_RELOADED"] = "CONFIG_RELOADED";
6725
- SchedulerEvent2["PROPOSALS_GENERATED"] = "PROPOSALS_GENERATED";
6726
- })(SchedulerEvent ||= {});
6727
-
6728
- class JobScheduler {
6729
- runner;
6730
- configLoader;
6731
- emitter;
6732
- proposalConfig;
6733
- tasks = new Map;
6734
- runningJobs = new Set;
6735
- currentConfigs = [];
6736
- currentAutonomyRules = [];
6737
- proposalRunning = false;
6738
- proposalDebounceTimer = null;
6739
- constructor(runner, configLoader, emitter, proposalConfig) {
6740
- this.runner = runner;
6741
- this.configLoader = configLoader;
6742
- this.emitter = emitter;
6743
- this.proposalConfig = proposalConfig;
6744
- }
6745
- start() {
6746
- const { jobConfigs, autonomyRules } = this.configLoader();
6747
- this.currentConfigs = jobConfigs;
6748
- this.currentAutonomyRules = autonomyRules;
6749
- const enabledConfigs = jobConfigs.filter((c2) => c2.enabled && c2.schedule.enabled);
6750
- for (const config of enabledConfigs) {
6751
- this.scheduleJob(config);
6752
- }
6753
- this.emitter.emit("SCHEDULER_STARTED" /* SCHEDULER_STARTED */, {
6754
- jobCount: enabledConfigs.length,
6755
- jobs: enabledConfigs.map((c2) => ({
6756
- type: c2.type,
6757
- cronExpression: c2.schedule.cronExpression
6758
- }))
6759
- });
6760
- }
6761
- stop() {
6762
- if (this.proposalDebounceTimer) {
6763
- clearTimeout(this.proposalDebounceTimer);
6764
- this.proposalDebounceTimer = null;
6765
- }
6766
- for (const [, task] of this.tasks) {
6767
- task.stop();
6768
- }
6769
- this.tasks.clear();
6770
- this.runningJobs.clear();
6771
- this.emitter.emit("SCHEDULER_STOPPED" /* SCHEDULER_STOPPED */);
6772
- }
6773
- reload() {
6774
- const previousJobCount = this.tasks.size;
6775
- for (const [, task] of this.tasks) {
6776
- task.stop();
6777
- }
6778
- this.tasks.clear();
6779
- const { jobConfigs, autonomyRules } = this.configLoader();
6780
- this.currentConfigs = jobConfigs;
6781
- this.currentAutonomyRules = autonomyRules;
6782
- const enabledConfigs = jobConfigs.filter((c2) => c2.enabled && c2.schedule.enabled);
6783
- for (const config of enabledConfigs) {
6784
- this.scheduleJob(config);
6785
- }
6786
- this.emitter.emit("CONFIG_RELOADED" /* CONFIG_RELOADED */, {
6787
- previousJobCount,
6788
- newJobCount: enabledConfigs.length
6789
- });
6790
- }
6791
- getScheduledJobs() {
6792
- const result = [];
6793
- for (const config of this.currentConfigs) {
6794
- if (this.tasks.has(config.type)) {
6795
- result.push({
6796
- type: config.type,
6797
- cronExpression: config.schedule.cronExpression
6798
- });
6799
- }
6800
- }
6801
- return result;
6802
- }
6803
- isRunning(jobType) {
6804
- return this.runningJobs.has(jobType);
6805
- }
6806
- scheduleJob(config) {
6807
- const { type, schedule } = config;
6808
- const { cronExpression } = schedule;
6809
- if (!import_node_cron.default.validate(cronExpression)) {
6810
- console.error(`Invalid cron expression for job ${type}: "${cronExpression}" — skipping`);
6811
- return;
6812
- }
6813
- const task = import_node_cron.default.schedule(cronExpression, () => {
6814
- this.triggerJob(type);
6815
- });
6816
- this.tasks.set(type, task);
6817
- this.emitter.emit("JOB_SCHEDULED" /* JOB_SCHEDULED */, {
6818
- jobType: type,
6819
- cronExpression
6820
- });
6821
- }
6822
- triggerJob(jobType) {
6823
- if (this.runningJobs.has(jobType)) {
6824
- this.emitter.emit("JOB_SKIPPED" /* JOB_SKIPPED */, {
6825
- jobType,
6826
- reason: "Previous run still in progress"
6827
- });
6828
- return;
6829
- }
6830
- const config = this.currentConfigs.find((c2) => c2.type === jobType);
6831
- if (!config)
6832
- return;
6833
- this.emitter.emit("JOB_TRIGGERED" /* JOB_TRIGGERED */, {
6834
- jobType
6835
- });
6836
- this.runningJobs.add(jobType);
6837
- this.runner.runJob(jobType, config, this.currentAutonomyRules).catch(() => {}).finally(() => {
6838
- this.runningJobs.delete(jobType);
6839
- this.scheduleProposalCycle();
6840
- });
6841
- }
6842
- scheduleProposalCycle() {
6843
- if (!this.proposalConfig)
6844
- return;
6845
- if (this.proposalDebounceTimer) {
6846
- clearTimeout(this.proposalDebounceTimer);
6847
- }
6848
- this.proposalDebounceTimer = setTimeout(() => {
6849
- this.proposalDebounceTimer = null;
6850
- if (this.runningJobs.size === 0) {
6851
- this.runProposalCycle();
6852
- }
6853
- }, 30000);
6854
- }
6855
- async runProposalCycle() {
6856
- if (!this.proposalConfig || this.proposalRunning)
6857
- return;
6858
- this.proposalRunning = true;
6859
- const { engine, projectPath, client, workspaceId } = this.proposalConfig;
6860
- try {
6861
- const suggestions2 = await engine.runProposalCycle(projectPath, client, workspaceId);
6862
- if (suggestions2.length > 0) {
6863
- this.emitter.emit("PROPOSALS_GENERATED" /* PROPOSALS_GENERATED */, {
6864
- suggestions: suggestions2
6865
- });
6866
- }
6867
- } catch (err) {
6868
- console.error("[scheduler] Proposal cycle failed:", err);
6869
- } finally {
6870
- this.proposalRunning = false;
6871
- }
6872
- }
6873
- }
6874
- // src/orchestrator/index.ts
6875
- init_git_utils();
6876
- init_src();
6877
- init_colors();
6878
- init_resolve_bin();
6879
- var import_node_child_process12 = require("node:child_process");
6880
- var import_node_fs13 = require("node:fs");
6881
- var import_node_path15 = require("node:path");
6882
- var import_node_url = require("node:url");
6883
- var import_shared10 = require("@locusai/shared");
6884
- var import_events4 = require("events");
6885
-
6886
- class AgentOrchestrator extends import_events4.EventEmitter {
6887
- client;
6888
- config;
6889
- isRunning = false;
6890
- processedTasks = new Set;
6891
- resolvedSprintId = null;
6892
- agentState = null;
6893
- heartbeatInterval = null;
6894
- constructor(config) {
6895
- super();
6896
- this.config = config;
6897
- this.client = new LocusClient({
6898
- baseUrl: config.apiBase,
6899
- token: config.apiKey
6900
- });
6901
- }
6902
- async resolveSprintId() {
6903
- if (this.config.sprintId) {
6904
- return this.config.sprintId;
6905
- }
6906
- try {
6907
- const sprint = await this.client.sprints.getActive(this.config.workspaceId);
6908
- if (sprint?.id) {
6909
- console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
6910
- return sprint.id;
6911
- }
6912
- } catch {}
6913
- console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
6914
- return "";
6915
- }
6916
- async start() {
6917
- if (this.isRunning) {
6918
- throw new Error("Orchestrator is already running");
6919
- }
6920
- this.isRunning = true;
6921
- this.processedTasks.clear();
6922
- try {
6923
- await this.orchestrationLoop();
6924
- } catch (error) {
6925
- this.emit("error", error);
6926
- throw error;
6927
- } finally {
6928
- await this.cleanup();
6929
- }
6930
- }
6931
- async orchestrationLoop() {
6932
- this.resolvedSprintId = await this.resolveSprintId();
6933
- this.emit("started", {
6934
- timestamp: new Date,
6935
- config: this.config,
6936
- sprintId: this.resolvedSprintId
6937
- });
6938
- this.printBanner();
6939
- const tasks2 = await this.getAvailableTasks();
6940
- if (tasks2.length === 0) {
6941
- console.log(c.dim("ℹ No available tasks found in the backlog."));
6942
- return;
6943
- }
6944
- if (!this.preflightChecks())
6945
- return;
6946
- this.startHeartbeatMonitor();
6947
- await this.spawnAgent();
6948
- await this.waitForAgent();
6949
- console.log(`
6950
- ${c.success("✅ Orchestrator finished")}`);
6951
- }
6952
- printBanner() {
6953
- console.log(`
6954
- ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
6955
- console.log(c.dim("----------------------------------------------"));
6956
- console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
6957
- if (this.resolvedSprintId) {
6958
- console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
6959
- }
6960
- console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
6961
- console.log(c.dim(`----------------------------------------------
6962
- `));
6963
- }
6964
- preflightChecks() {
6965
- if (!isGitAvailable()) {
6966
- console.log(c.error("git is not installed. Install from https://git-scm.com/"));
6967
- return false;
6968
- }
6969
- if (!isGhAvailable(this.config.projectPath)) {
6970
- 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/"));
6971
- }
6972
- return true;
6973
- }
6974
- async getAvailableTasks() {
6975
- try {
6976
- const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
6977
- return tasks2.filter((task) => !this.processedTasks.has(task.id));
6978
- } catch (error) {
6979
- this.emit("error", error);
6980
- return [];
6981
- }
6982
- }
6983
- async spawnAgent() {
6984
- const agentId = `agent-0-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
6985
- this.agentState = {
6986
- id: agentId,
6987
- status: "IDLE",
6988
- currentTaskId: null,
6989
- tasksCompleted: 0,
6990
- tasksFailed: 0,
6991
- lastHeartbeat: new Date
6992
- };
6993
- console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
6994
- `);
6995
- const workerPath = this.resolveWorkerPath();
6996
- if (!workerPath) {
6997
- throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
6998
- }
6999
- const workerArgs = this.buildWorkerArgs(agentId);
7000
- const agentProcess = import_node_child_process12.spawn(process.execPath, [workerPath, ...workerArgs], {
7001
- stdio: ["pipe", "pipe", "pipe"],
7002
- detached: true,
7003
- env: getAugmentedEnv({
7004
- FORCE_COLOR: "1",
7005
- TERM: "xterm-256color",
7006
- LOCUS_WORKER: agentId,
7007
- LOCUS_WORKSPACE: this.config.workspaceId
7008
- })
7009
- });
7010
- this.agentState.process = agentProcess;
7011
- this.attachProcessHandlers(agentId, this.agentState, agentProcess);
7012
- this.emit("agent:spawned", { agentId });
7013
- }
7014
- async waitForAgent() {
7015
- while (this.agentState && this.isRunning) {
7016
- await sleep(2000);
7017
- }
7018
- }
7019
- startHeartbeatMonitor() {
7020
- this.heartbeatInterval = setInterval(() => {
7021
- if (!this.agentState)
7022
- return;
7023
- const now = Date.now();
7024
- if (this.agentState.status === "WORKING" && now - this.agentState.lastHeartbeat.getTime() > import_shared10.STALE_AGENT_TIMEOUT_MS) {
7025
- console.log(c.error(`Agent ${this.agentState.id} is stale (no heartbeat for 10 minutes). Killing.`));
7026
- if (this.agentState.process && !this.agentState.process.killed) {
7027
- killProcessTree(this.agentState.process);
7028
- }
7029
- this.emit("agent:stale", { agentId: this.agentState.id });
7030
- }
7031
- }, 60000);
7032
- }
7033
- async stop() {
7034
- this.isRunning = false;
7035
- await this.cleanup();
7036
- this.emit("stopped", { timestamp: new Date });
7037
- }
7038
- stopAgent(agentId) {
7039
- if (!this.agentState || this.agentState.id !== agentId)
7040
- return false;
7041
- if (this.agentState.process && !this.agentState.process.killed) {
7042
- killProcessTree(this.agentState.process);
7043
- }
7044
- return true;
7045
- }
7046
- async cleanup() {
7047
- if (this.heartbeatInterval) {
7048
- clearInterval(this.heartbeatInterval);
7049
- this.heartbeatInterval = null;
7050
- }
7051
- if (this.agentState?.process && !this.agentState.process.killed) {
7052
- console.log(`Killing agent: ${this.agentState.id}`);
7053
- killProcessTree(this.agentState.process);
7054
- }
7055
- }
7056
- getStats() {
7057
- return {
7058
- activeAgents: this.agentState ? 1 : 0,
7059
- totalTasksCompleted: this.agentState?.tasksCompleted ?? 0,
7060
- totalTasksFailed: this.agentState?.tasksFailed ?? 0,
7061
- processedTasks: this.processedTasks.size
7062
- };
7063
- }
7064
- getAgentStates() {
7065
- return this.agentState ? [this.agentState] : [];
5176
+ getAgentStates() {
5177
+ return this.agentState ? [this.agentState] : [];
7066
5178
  }
7067
5179
  buildWorkerArgs(agentId) {
7068
5180
  const args = [
@@ -7146,14 +5258,14 @@ ${agentId} finished (exit code: ${code})`);
7146
5258
  }
7147
5259
  resolveWorkerPath() {
7148
5260
  const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator/index.ts");
7149
- const currentModuleDir = import_node_path15.dirname(currentModulePath);
5261
+ const currentModuleDir = import_node_path10.dirname(currentModulePath);
7150
5262
  const potentialPaths = [
7151
- import_node_path15.join(currentModuleDir, "..", "agent", "worker.js"),
7152
- import_node_path15.join(currentModuleDir, "agent", "worker.js"),
7153
- import_node_path15.join(currentModuleDir, "worker.js"),
7154
- import_node_path15.join(currentModuleDir, "..", "agent", "worker.ts")
5263
+ import_node_path10.join(currentModuleDir, "..", "agent", "worker.js"),
5264
+ import_node_path10.join(currentModuleDir, "agent", "worker.js"),
5265
+ import_node_path10.join(currentModuleDir, "worker.js"),
5266
+ import_node_path10.join(currentModuleDir, "..", "agent", "worker.ts")
7155
5267
  ];
7156
- return potentialPaths.find((p) => import_node_fs13.existsSync(p));
5268
+ return potentialPaths.find((p) => import_node_fs9.existsSync(p));
7157
5269
  }
7158
5270
  }
7159
5271
  function killProcessTree(proc) {
@@ -7172,11 +5284,11 @@ function sleep(ms) {
7172
5284
  }
7173
5285
  // src/planning/plan-manager.ts
7174
5286
  init_config();
7175
- var import_node_fs14 = require("node:fs");
7176
- var import_node_path16 = require("node:path");
5287
+ var import_node_fs10 = require("node:fs");
5288
+ var import_node_path11 = require("node:path");
7177
5289
 
7178
5290
  // src/planning/sprint-plan.ts
7179
- var import_shared11 = require("@locusai/shared");
5291
+ var import_shared5 = require("@locusai/shared");
7180
5292
  var import_zod2 = require("zod");
7181
5293
 
7182
5294
  // src/utils/structured-output.ts
@@ -7284,7 +5396,7 @@ function plannedTasksToCreatePayloads(plan, sprintId) {
7284
5396
  return plan.tasks.map((task) => ({
7285
5397
  title: task.title,
7286
5398
  description: task.description,
7287
- status: import_shared11.TaskStatus.BACKLOG,
5399
+ status: import_shared5.TaskStatus.BACKLOG,
7288
5400
  assigneeRole: task.assigneeRole,
7289
5401
  priority: task.priority,
7290
5402
  labels: task.labels,
@@ -7338,19 +5450,19 @@ class PlanManager {
7338
5450
  save(plan) {
7339
5451
  this.ensurePlansDir();
7340
5452
  const slug = this.slugify(plan.name);
7341
- const jsonPath = import_node_path16.join(this.plansDir, `${slug}.json`);
7342
- const mdPath = import_node_path16.join(this.plansDir, `sprint-${slug}.md`);
7343
- import_node_fs14.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
7344
- import_node_fs14.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
5453
+ const jsonPath = import_node_path11.join(this.plansDir, `${slug}.json`);
5454
+ const mdPath = import_node_path11.join(this.plansDir, `sprint-${slug}.md`);
5455
+ import_node_fs10.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
5456
+ import_node_fs10.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
7345
5457
  return plan.id;
7346
5458
  }
7347
5459
  load(idOrSlug) {
7348
5460
  this.ensurePlansDir();
7349
- const files = import_node_fs14.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
5461
+ const files = import_node_fs10.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
7350
5462
  for (const file of files) {
7351
- const filePath = import_node_path16.join(this.plansDir, file);
5463
+ const filePath = import_node_path11.join(this.plansDir, file);
7352
5464
  try {
7353
- const plan = JSON.parse(import_node_fs14.readFileSync(filePath, "utf-8"));
5465
+ const plan = JSON.parse(import_node_fs10.readFileSync(filePath, "utf-8"));
7354
5466
  if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
7355
5467
  return plan;
7356
5468
  }
@@ -7360,11 +5472,11 @@ class PlanManager {
7360
5472
  }
7361
5473
  list(status) {
7362
5474
  this.ensurePlansDir();
7363
- const files = import_node_fs14.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
5475
+ const files = import_node_fs10.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
7364
5476
  const plans = [];
7365
5477
  for (const file of files) {
7366
5478
  try {
7367
- const plan = JSON.parse(import_node_fs14.readFileSync(import_node_path16.join(this.plansDir, file), "utf-8"));
5479
+ const plan = JSON.parse(import_node_fs10.readFileSync(import_node_path11.join(this.plansDir, file), "utf-8"));
7368
5480
  if (!status || plan.status === status) {
7369
5481
  plans.push(plan);
7370
5482
  }
@@ -7421,18 +5533,18 @@ class PlanManager {
7421
5533
  }
7422
5534
  delete(idOrSlug) {
7423
5535
  this.ensurePlansDir();
7424
- const files = import_node_fs14.readdirSync(this.plansDir);
5536
+ const files = import_node_fs10.readdirSync(this.plansDir);
7425
5537
  for (const file of files) {
7426
- const filePath = import_node_path16.join(this.plansDir, file);
5538
+ const filePath = import_node_path11.join(this.plansDir, file);
7427
5539
  if (!file.endsWith(".json"))
7428
5540
  continue;
7429
5541
  try {
7430
- const plan = JSON.parse(import_node_fs14.readFileSync(filePath, "utf-8"));
5542
+ const plan = JSON.parse(import_node_fs10.readFileSync(filePath, "utf-8"));
7431
5543
  if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
7432
- import_node_fs14.unlinkSync(filePath);
7433
- const mdPath = import_node_path16.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
7434
- if (import_node_fs14.existsSync(mdPath)) {
7435
- import_node_fs14.unlinkSync(mdPath);
5544
+ import_node_fs10.unlinkSync(filePath);
5545
+ const mdPath = import_node_path11.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
5546
+ if (import_node_fs10.existsSync(mdPath)) {
5547
+ import_node_fs10.unlinkSync(mdPath);
7436
5548
  }
7437
5549
  return;
7438
5550
  }
@@ -7446,8 +5558,8 @@ class PlanManager {
7446
5558
  return sprintPlanToMarkdown(plan);
7447
5559
  }
7448
5560
  ensurePlansDir() {
7449
- if (!import_node_fs14.existsSync(this.plansDir)) {
7450
- import_node_fs14.mkdirSync(this.plansDir, { recursive: true });
5561
+ if (!import_node_fs10.existsSync(this.plansDir)) {
5562
+ import_node_fs10.mkdirSync(this.plansDir, { recursive: true });
7451
5563
  }
7452
5564
  }
7453
5565
  slugify(name) {
@@ -7456,8 +5568,8 @@ class PlanManager {
7456
5568
  }
7457
5569
  // src/planning/planning-meeting.ts
7458
5570
  init_config();
7459
- var import_node_fs15 = require("node:fs");
7460
- var import_node_path17 = require("node:path");
5571
+ var import_node_fs11 = require("node:fs");
5572
+ var import_node_path12 = require("node:path");
7461
5573
 
7462
5574
  // src/planning/agents/planner.ts
7463
5575
  function buildPlannerPrompt(input) {
@@ -7543,8 +5655,8 @@ class PlanningMeeting {
7543
5655
  async run(directive, feedback) {
7544
5656
  this.log("Planning sprint...", "info");
7545
5657
  const plansDir = getLocusPath(this.projectPath, "plansDir");
7546
- if (!import_node_fs15.existsSync(plansDir)) {
7547
- import_node_fs15.mkdirSync(plansDir, { recursive: true });
5658
+ if (!import_node_fs11.existsSync(plansDir)) {
5659
+ import_node_fs11.mkdirSync(plansDir, { recursive: true });
7548
5660
  }
7549
5661
  const ts = Date.now();
7550
5662
  const planId = `plan-${ts}`;
@@ -7558,11 +5670,11 @@ class PlanningMeeting {
7558
5670
  });
7559
5671
  const response = await this.aiRunner.run(prompt);
7560
5672
  this.log("Planning meeting complete.", "success");
7561
- const expectedPath = import_node_path17.join(plansDir, `${fileName}.json`);
5673
+ const expectedPath = import_node_path12.join(plansDir, `${fileName}.json`);
7562
5674
  let plan = null;
7563
- if (import_node_fs15.existsSync(expectedPath)) {
5675
+ if (import_node_fs11.existsSync(expectedPath)) {
7564
5676
  try {
7565
- plan = JSON.parse(import_node_fs15.readFileSync(expectedPath, "utf-8"));
5677
+ plan = JSON.parse(import_node_fs11.readFileSync(expectedPath, "utf-8"));
7566
5678
  } catch {}
7567
5679
  }
7568
5680
  if (!plan) {
@@ -7577,5 +5689,240 @@ class PlanningMeeting {
7577
5689
  };
7578
5690
  }
7579
5691
  }
5692
+ // src/proposals/context-gatherer.ts
5693
+ var import_node_child_process8 = require("node:child_process");
5694
+ var import_node_fs12 = require("node:fs");
5695
+ var import_node_path13 = require("node:path");
5696
+
5697
+ class ContextGatherer {
5698
+ async gather(projectPath, client, workspaceId) {
5699
+ const [activeSprint, allTasks, skippedSuggestions] = await Promise.all([
5700
+ this.fetchActiveSprint(client, workspaceId),
5701
+ this.fetchTasks(client, workspaceId),
5702
+ this.fetchSkippedSuggestions(client, workspaceId)
5703
+ ]);
5704
+ const sprintTasks = activeSprint ? allTasks.filter((t) => t.sprintId === activeSprint.id) : [];
5705
+ const backlogTasks = allTasks.filter((t) => !t.sprintId);
5706
+ const gitLog = this.readGitLog(projectPath);
5707
+ const artifactContents = this.readArtifacts(projectPath);
5708
+ const locusInstructions = this.readLocusInstructions(projectPath);
5709
+ return {
5710
+ activeSprint,
5711
+ sprintTasks,
5712
+ backlogTasks,
5713
+ gitLog,
5714
+ artifactContents,
5715
+ locusInstructions,
5716
+ skippedSuggestions
5717
+ };
5718
+ }
5719
+ async fetchActiveSprint(client, workspaceId) {
5720
+ try {
5721
+ return await client.sprints.getActive(workspaceId);
5722
+ } catch {
5723
+ return null;
5724
+ }
5725
+ }
5726
+ async fetchTasks(client, workspaceId) {
5727
+ try {
5728
+ return await client.tasks.list(workspaceId);
5729
+ } catch {
5730
+ return [];
5731
+ }
5732
+ }
5733
+ async fetchSkippedSuggestions(client, workspaceId) {
5734
+ try {
5735
+ return await client.suggestions.list(workspaceId, { status: "SKIPPED" });
5736
+ } catch {
5737
+ return [];
5738
+ }
5739
+ }
5740
+ readGitLog(projectPath) {
5741
+ try {
5742
+ return import_node_child_process8.execFileSync("git", ["log", "--oneline", "--no-decorate", "-n", "20"], {
5743
+ cwd: projectPath,
5744
+ encoding: "utf-8",
5745
+ timeout: 1e4,
5746
+ stdio: ["pipe", "pipe", "pipe"]
5747
+ }).trim();
5748
+ } catch {
5749
+ return "";
5750
+ }
5751
+ }
5752
+ readArtifacts(projectPath) {
5753
+ const artifactsDir = import_node_path13.join(projectPath, ".locus", "artifacts");
5754
+ if (!import_node_fs12.existsSync(artifactsDir))
5755
+ return [];
5756
+ try {
5757
+ const files = import_node_fs12.readdirSync(artifactsDir).filter((f) => f.endsWith(".md"));
5758
+ return files.slice(0, 10).map((name) => ({
5759
+ name,
5760
+ content: import_node_fs12.readFileSync(import_node_path13.join(artifactsDir, name), "utf-8").slice(0, 2000)
5761
+ }));
5762
+ } catch {
5763
+ return [];
5764
+ }
5765
+ }
5766
+ readLocusInstructions(projectPath) {
5767
+ const locusPath = import_node_path13.join(projectPath, ".locus", "LOCUS.md");
5768
+ if (!import_node_fs12.existsSync(locusPath))
5769
+ return null;
5770
+ try {
5771
+ return import_node_fs12.readFileSync(locusPath, "utf-8").slice(0, 3000);
5772
+ } catch {
5773
+ return null;
5774
+ }
5775
+ }
5776
+ }
5777
+ // src/proposals/proposal-engine.ts
5778
+ init_factory();
5779
+ var import_shared6 = require("@locusai/shared");
5780
+ class ProposalEngine {
5781
+ contextGatherer;
5782
+ constructor(contextGatherer) {
5783
+ this.contextGatherer = contextGatherer ?? new ContextGatherer;
5784
+ }
5785
+ async runProposalCycle(projectPath, client, workspaceId) {
5786
+ const context = await this.contextGatherer.gather(projectPath, client, workspaceId);
5787
+ return this.generateProposals(context, projectPath, client, workspaceId);
5788
+ }
5789
+ async generateProposals(context, projectPath, client, workspaceId) {
5790
+ const prompt = this.buildPrompt(context);
5791
+ const runner = createAiRunner(undefined, {
5792
+ projectPath,
5793
+ timeoutMs: 5 * 60 * 1000,
5794
+ maxTurns: 1
5795
+ });
5796
+ let aiResponse;
5797
+ try {
5798
+ aiResponse = await runner.run(prompt);
5799
+ } catch {
5800
+ return [];
5801
+ }
5802
+ const proposals = this.parseResponse(aiResponse);
5803
+ const created = [];
5804
+ for (const proposal of proposals) {
5805
+ if (this.isDuplicate(proposal.title, context.skippedSuggestions)) {
5806
+ continue;
5807
+ }
5808
+ try {
5809
+ const suggestion = await client.suggestions.create(workspaceId, {
5810
+ type: import_shared6.SuggestionType.NEXT_STEP,
5811
+ title: proposal.title,
5812
+ description: proposal.description,
5813
+ metadata: {
5814
+ complexity: proposal.complexity,
5815
+ relatedBacklogItem: proposal.relatedBacklogItem,
5816
+ source: "proposal-engine"
5817
+ }
5818
+ });
5819
+ created.push(suggestion);
5820
+ } catch {}
5821
+ }
5822
+ return created;
5823
+ }
5824
+ buildPrompt(context) {
5825
+ const sections = [];
5826
+ sections.push("You are a proactive software engineering advisor. Based on the project context below, propose 1-3 high-value next steps the team should take. Focus on actionable, impactful work.");
5827
+ if (context.activeSprint) {
5828
+ const sprintInfo = `Sprint: ${context.activeSprint.name} (${context.activeSprint.status})`;
5829
+ const tasksByStatus = this.groupTasksByStatus(context.sprintTasks);
5830
+ sections.push(`## Current Sprint
5831
+ ${sprintInfo}
5832
+ ${tasksByStatus}`);
5833
+ }
5834
+ if (context.backlogTasks.length > 0) {
5835
+ const backlogList = context.backlogTasks.slice(0, 15).map((t) => `- [${t.priority}] ${t.title}${t.description ? `: ${t.description.slice(0, 100)}` : ""}`).join(`
5836
+ `);
5837
+ sections.push(`## Backlog Items
5838
+ ${backlogList}`);
5839
+ }
5840
+ if (context.gitLog) {
5841
+ sections.push(`## Recent Commits (last 20)
5842
+ ${context.gitLog}`);
5843
+ }
5844
+ if (context.artifactContents.length > 0) {
5845
+ const artifacts = context.artifactContents.map((a) => `### ${a.name}
5846
+ ${a.content}`).join(`
5847
+
5848
+ `);
5849
+ sections.push(`## Product Context
5850
+ ${artifacts}`);
5851
+ }
5852
+ if (context.locusInstructions) {
5853
+ sections.push(`## Project Instructions
5854
+ ${context.locusInstructions}`);
5855
+ }
5856
+ if (context.skippedSuggestions.length > 0) {
5857
+ const skipped = context.skippedSuggestions.map((s) => `- ${s.title}`).join(`
5858
+ `);
5859
+ sections.push(`## Previously Skipped Proposals (do NOT re-propose these)
5860
+ ${skipped}`);
5861
+ }
5862
+ sections.push(`## Instructions
5863
+ Propose 1-3 high-value next steps. For each, respond with exactly this format:
5864
+
5865
+ PROPOSAL_START
5866
+ Title: <clear, concise title>
5867
+ Description: <what to do and why, 2-4 sentences>
5868
+ Complexity: <low|medium|high>
5869
+ Related Backlog: <title of related backlog item, or "none">
5870
+ PROPOSAL_END
5871
+
5872
+ Rules:
5873
+ - Focus on what would deliver the most value right now
5874
+ - Align with the current sprint goals when possible
5875
+ - Don't propose things that are already in progress
5876
+ - Don't re-propose previously skipped suggestions
5877
+ - Keep proposals specific and actionable`);
5878
+ return sections.join(`
5879
+
5880
+ `);
5881
+ }
5882
+ parseResponse(response) {
5883
+ const proposals = [];
5884
+ const blocks = response.split("PROPOSAL_START");
5885
+ for (const block of blocks) {
5886
+ const endIdx = block.indexOf("PROPOSAL_END");
5887
+ if (endIdx === -1)
5888
+ continue;
5889
+ const content = block.slice(0, endIdx).trim();
5890
+ const title = this.extractField(content, "Title");
5891
+ const description = this.extractField(content, "Description");
5892
+ const complexity = this.extractField(content, "Complexity") ?? "medium";
5893
+ const relatedRaw = this.extractField(content, "Related Backlog");
5894
+ const relatedBacklogItem = relatedRaw && relatedRaw.toLowerCase() !== "none" ? relatedRaw : null;
5895
+ if (title && description) {
5896
+ proposals.push({
5897
+ title: title.slice(0, 200),
5898
+ description: description.slice(0, 2000),
5899
+ complexity: complexity.toLowerCase(),
5900
+ relatedBacklogItem
5901
+ });
5902
+ }
5903
+ }
5904
+ return proposals.slice(0, 3);
5905
+ }
5906
+ extractField(content, field) {
5907
+ const regex = new RegExp(`^${field}:\\s*(.+)`, "im");
5908
+ const match = content.match(regex);
5909
+ return match ? match[1].trim() : null;
5910
+ }
5911
+ isDuplicate(title, skipped) {
5912
+ const normalized = title.toLowerCase().trim();
5913
+ return skipped.some((s) => {
5914
+ const skippedNorm = s.title.toLowerCase().trim();
5915
+ return skippedNorm === normalized || skippedNorm.includes(normalized) || normalized.includes(skippedNorm);
5916
+ });
5917
+ }
5918
+ groupTasksByStatus(tasks2) {
5919
+ const groups = {};
5920
+ for (const t of tasks2) {
5921
+ groups[t.status] = (groups[t.status] ?? 0) + 1;
5922
+ }
5923
+ return Object.entries(groups).map(([status, count]) => `- ${status}: ${count} task(s)`).join(`
5924
+ `);
5925
+ }
5926
+ }
7580
5927
  // src/index-node.ts
7581
5928
  init_colors();