@locusai/sdk 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/agent/worker.js +67 -4
  2. package/dist/ai/claude-runner.d.ts +1 -0
  3. package/dist/ai/claude-runner.d.ts.map +1 -1
  4. package/dist/ai/codex-runner.d.ts +1 -0
  5. package/dist/ai/codex-runner.d.ts.map +1 -1
  6. package/dist/events.d.ts +2 -0
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/index-node.d.ts +1 -0
  9. package/dist/index-node.d.ts.map +1 -1
  10. package/dist/index-node.js +2130 -204
  11. package/dist/index.d.ts +6 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +56 -0
  14. package/dist/jobs/__tests__/job-runner.test.d.ts +2 -0
  15. package/dist/jobs/__tests__/job-runner.test.d.ts.map +1 -0
  16. package/dist/jobs/__tests__/lint-scan.test.d.ts +2 -0
  17. package/dist/jobs/__tests__/lint-scan.test.d.ts.map +1 -0
  18. package/dist/jobs/__tests__/scheduler.test.d.ts +2 -0
  19. package/dist/jobs/__tests__/scheduler.test.d.ts.map +1 -0
  20. package/dist/jobs/base-job.d.ts +29 -0
  21. package/dist/jobs/base-job.d.ts.map +1 -0
  22. package/dist/jobs/default-registry.d.ts +3 -0
  23. package/dist/jobs/default-registry.d.ts.map +1 -0
  24. package/dist/jobs/index.d.ts +12 -0
  25. package/dist/jobs/index.d.ts.map +1 -0
  26. package/dist/jobs/job-registry.d.ts +10 -0
  27. package/dist/jobs/job-registry.d.ts.map +1 -0
  28. package/dist/jobs/job-runner.d.ts +33 -0
  29. package/dist/jobs/job-runner.d.ts.map +1 -0
  30. package/dist/jobs/proposals/context-gatherer.d.ts +35 -0
  31. package/dist/jobs/proposals/context-gatherer.d.ts.map +1 -0
  32. package/dist/jobs/proposals/index.d.ts +4 -0
  33. package/dist/jobs/proposals/index.d.ts.map +1 -0
  34. package/dist/jobs/proposals/proposal-engine.d.ts +21 -0
  35. package/dist/jobs/proposals/proposal-engine.d.ts.map +1 -0
  36. package/dist/jobs/scans/dependency-scan.d.ts +28 -0
  37. package/dist/jobs/scans/dependency-scan.d.ts.map +1 -0
  38. package/dist/jobs/scans/index.d.ts +5 -0
  39. package/dist/jobs/scans/index.d.ts.map +1 -0
  40. package/dist/jobs/scans/lint-scan.d.ts +20 -0
  41. package/dist/jobs/scans/lint-scan.d.ts.map +1 -0
  42. package/dist/jobs/scans/test-scan.d.ts +20 -0
  43. package/dist/jobs/scans/test-scan.d.ts.map +1 -0
  44. package/dist/jobs/scans/todo-scan.d.ts +15 -0
  45. package/dist/jobs/scans/todo-scan.d.ts.map +1 -0
  46. package/dist/jobs/scheduler.d.ts +80 -0
  47. package/dist/jobs/scheduler.d.ts.map +1 -0
  48. package/dist/modules/jobs.d.ts +14 -0
  49. package/dist/modules/jobs.d.ts.map +1 -0
  50. package/dist/modules/suggestions.d.ts +12 -0
  51. package/dist/modules/suggestions.d.ts.map +1 -0
  52. package/package.json +7 -5
@@ -225,6 +225,29 @@ 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
+
228
251
  // src/modules/organizations.ts
229
252
  var OrganizationsModule;
230
253
  var init_organizations = __esm(() => {
@@ -307,6 +330,29 @@ var init_sprints = __esm(() => {
307
330
  };
308
331
  });
309
332
 
333
+ // src/modules/suggestions.ts
334
+ var SuggestionsModule;
335
+ var init_suggestions = __esm(() => {
336
+ SuggestionsModule = class SuggestionsModule extends BaseModule {
337
+ async create(workspaceId, data) {
338
+ const { data: res } = await this.api.post(`/workspaces/${workspaceId}/suggestions`, data);
339
+ return res.suggestion;
340
+ }
341
+ async list(workspaceId, params) {
342
+ const { data } = await this.api.get(`/workspaces/${workspaceId}/suggestions`, { params });
343
+ return data.suggestions;
344
+ }
345
+ async get(workspaceId, id) {
346
+ const { data } = await this.api.get(`/workspaces/${workspaceId}/suggestions/${id}`);
347
+ return data.suggestion;
348
+ }
349
+ async updateStatus(workspaceId, id, status) {
350
+ const { data } = await this.api.patch(`/workspaces/${workspaceId}/suggestions/${id}/status`, status);
351
+ return data.suggestion;
352
+ }
353
+ };
354
+ });
355
+
310
356
  // src/modules/tasks.ts
311
357
  var import_shared, TasksModule;
312
358
  var init_tasks = __esm(() => {
@@ -483,11 +529,13 @@ var exports_src = {};
483
529
  __export(exports_src, {
484
530
  WorkspacesModule: () => WorkspacesModule,
485
531
  TasksModule: () => TasksModule,
532
+ SuggestionsModule: () => SuggestionsModule,
486
533
  SprintsModule: () => SprintsModule,
487
534
  OrganizationsModule: () => OrganizationsModule,
488
535
  LocusEvent: () => LocusEvent,
489
536
  LocusEmitter: () => LocusEmitter,
490
537
  LocusClient: () => LocusClient,
538
+ JobsModule: () => JobsModule,
491
539
  InvitationsModule: () => InvitationsModule,
492
540
  InstancesModule: () => InstancesModule,
493
541
  DocsModule: () => DocsModule,
@@ -511,6 +559,8 @@ class LocusClient {
511
559
  docs;
512
560
  ci;
513
561
  instances;
562
+ jobs;
563
+ suggestions;
514
564
  constructor(config) {
515
565
  this.emitter = new LocusEmitter;
516
566
  this.api = import_axios.default.create({
@@ -531,6 +581,8 @@ class LocusClient {
531
581
  this.docs = new DocsModule(this.api, this.emitter);
532
582
  this.ci = new CiModule(this.api, this.emitter);
533
583
  this.instances = new InstancesModule(this.api, this.emitter);
584
+ this.jobs = new JobsModule(this.api, this.emitter);
585
+ this.suggestions = new SuggestionsModule(this.api, this.emitter);
534
586
  if (config.retryOptions) {
535
587
  this.setupRetryInterceptor(config.retryOptions);
536
588
  }
@@ -598,8 +650,10 @@ var init_src = __esm(() => {
598
650
  init_docs();
599
651
  init_instances();
600
652
  init_invitations();
653
+ init_jobs();
601
654
  init_organizations();
602
655
  init_sprints();
656
+ init_suggestions();
603
657
  init_tasks();
604
658
  init_workspaces();
605
659
  init_discussion_types();
@@ -610,8 +664,10 @@ var init_src = __esm(() => {
610
664
  init_docs();
611
665
  init_instances();
612
666
  init_invitations();
667
+ init_jobs();
613
668
  init_organizations();
614
669
  init_sprints();
670
+ init_suggestions();
615
671
  init_tasks();
616
672
  init_workspaces();
617
673
  });
@@ -883,6 +939,7 @@ class ClaudeRunner {
883
939
  currentToolName;
884
940
  activeTools = new Map;
885
941
  activeProcess = null;
942
+ aborted = false;
886
943
  timeoutMs;
887
944
  constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log, timeoutMs) {
888
945
  this.model = model;
@@ -895,6 +952,7 @@ class ClaudeRunner {
895
952
  }
896
953
  abort() {
897
954
  if (this.activeProcess && !this.activeProcess.killed) {
955
+ this.aborted = true;
898
956
  this.activeProcess.kill("SIGTERM");
899
957
  this.activeProcess = null;
900
958
  }
@@ -953,6 +1011,7 @@ class ClaudeRunner {
953
1011
  return args;
954
1012
  }
955
1013
  async* runStream(prompt) {
1014
+ this.aborted = false;
956
1015
  const args = this.buildCliArgs();
957
1016
  const env = getAugmentedEnv({
958
1017
  FORCE_COLOR: "1",
@@ -1047,7 +1106,7 @@ class ClaudeRunner {
1047
1106
  process.stderr.write(`${stderrBuffer}
1048
1107
  `);
1049
1108
  }
1050
- if (code !== 0 && !errorMessage) {
1109
+ if (code !== 0 && !errorMessage && !this.aborted) {
1051
1110
  const detail = stderrFull.trim() || lastResultContent.trim();
1052
1111
  errorMessage = this.createExecutionError(code, detail).message;
1053
1112
  this.eventEmitter?.emitErrorOccurred(errorMessage, `EXIT_${code}`);
@@ -1192,6 +1251,7 @@ class ClaudeRunner {
1192
1251
  return null;
1193
1252
  }
1194
1253
  executeRun(prompt) {
1254
+ this.aborted = false;
1195
1255
  return new Promise((resolve2, reject) => {
1196
1256
  const args = this.buildCliArgs();
1197
1257
  const env = getAugmentedEnv({
@@ -1244,7 +1304,7 @@ class ClaudeRunner {
1244
1304
  }
1245
1305
  process.stdout.write(`
1246
1306
  `);
1247
- if (code === 0) {
1307
+ if (code === 0 || this.aborted) {
1248
1308
  resolve2(finalResult);
1249
1309
  } else {
1250
1310
  const detail = errorOutput.trim() || finalResult.trim();
@@ -1311,6 +1371,7 @@ class CodexRunner {
1311
1371
  log;
1312
1372
  reasoningEffort;
1313
1373
  activeProcess = null;
1374
+ aborted = false;
1314
1375
  eventEmitter;
1315
1376
  currentToolName;
1316
1377
  timeoutMs;
@@ -1326,11 +1387,13 @@ class CodexRunner {
1326
1387
  }
1327
1388
  abort() {
1328
1389
  if (this.activeProcess && !this.activeProcess.killed) {
1390
+ this.aborted = true;
1329
1391
  this.activeProcess.kill("SIGTERM");
1330
1392
  this.activeProcess = null;
1331
1393
  }
1332
1394
  }
1333
1395
  async run(prompt) {
1396
+ this.aborted = false;
1334
1397
  const maxRetries = 3;
1335
1398
  let lastError = null;
1336
1399
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
@@ -1446,7 +1509,7 @@ class CodexRunner {
1446
1509
  });
1447
1510
  codex.on("close", (code) => {
1448
1511
  this.activeProcess = null;
1449
- if (code === 0) {
1512
+ if (code === 0 || this.aborted) {
1450
1513
  const result = this.readOutput(outputPath, finalOutput);
1451
1514
  this.cleanupTempFile(outputPath);
1452
1515
  if (result && finalContent.trim().length === 0) {
@@ -1559,7 +1622,7 @@ class CodexRunner {
1559
1622
  });
1560
1623
  codex.on("close", (code) => {
1561
1624
  this.activeProcess = null;
1562
- if (code === 0) {
1625
+ if (code === 0 || this.aborted) {
1563
1626
  const result = this.readOutput(outputPath, output);
1564
1627
  this.cleanupTempFile(outputPath);
1565
1628
  resolve2(result);
@@ -2844,16 +2907,22 @@ __export(exports_index_node, {
2844
2907
  getAgentArtifactsPath: () => getAgentArtifactsPath,
2845
2908
  extractJsonFromLLMOutput: () => extractJsonFromLLMOutput,
2846
2909
  detectRemoteProvider: () => detectRemoteProvider,
2910
+ createDefaultRegistry: () => createDefaultRegistry,
2847
2911
  createAiRunner: () => createAiRunner,
2848
2912
  c: () => c,
2849
2913
  buildSummaryPrompt: () => buildSummaryPrompt,
2850
2914
  buildFacilitatorPrompt: () => buildFacilitatorPrompt,
2851
2915
  WorkspacesModule: () => WorkspacesModule,
2916
+ TodoScanJob: () => TodoScanJob,
2917
+ TestScanJob: () => TestScanJob,
2852
2918
  TasksModule: () => TasksModule,
2853
2919
  TaskExecutor: () => TaskExecutor,
2920
+ SuggestionsModule: () => SuggestionsModule,
2854
2921
  SprintsModule: () => SprintsModule,
2922
+ SchedulerEvent: () => SchedulerEvent,
2855
2923
  ReviewerWorker: () => ReviewerWorker,
2856
2924
  ReviewService: () => ReviewService,
2925
+ ProposalEngine: () => ProposalEngine,
2857
2926
  PromptBuilder: () => PromptBuilder,
2858
2927
  PrService: () => PrService,
2859
2928
  PlanningMeeting: () => PlanningMeeting,
@@ -2864,10 +2933,16 @@ __export(exports_index_node, {
2864
2933
  LocusEvent: () => LocusEvent,
2865
2934
  LocusEmitter: () => LocusEmitter,
2866
2935
  LocusClient: () => LocusClient,
2936
+ LintScanJob: () => LintScanJob,
2867
2937
  LOCUS_SCHEMA_BASE_URL: () => LOCUS_SCHEMA_BASE_URL,
2868
2938
  LOCUS_SCHEMAS: () => LOCUS_SCHEMAS,
2869
2939
  LOCUS_GITIGNORE_PATTERNS: () => LOCUS_GITIGNORE_PATTERNS,
2870
2940
  LOCUS_CONFIG: () => LOCUS_CONFIG,
2941
+ JobsModule: () => JobsModule,
2942
+ JobScheduler: () => JobScheduler,
2943
+ JobRunner: () => JobRunner,
2944
+ JobRegistry: () => JobRegistry,
2945
+ JobEvent: () => JobEvent,
2871
2946
  InvitationsModule: () => InvitationsModule,
2872
2947
  InstancesModule: () => InstancesModule,
2873
2948
  HistoryManager: () => HistoryManager,
@@ -2882,8 +2957,10 @@ __export(exports_index_node, {
2882
2957
  DiscussionManager: () => DiscussionManager,
2883
2958
  DiscussionInsightSchema: () => DiscussionInsightSchema,
2884
2959
  DiscussionFacilitator: () => DiscussionFacilitator,
2960
+ DependencyScanJob: () => DependencyScanJob,
2885
2961
  DEFAULT_MODEL: () => DEFAULT_MODEL,
2886
2962
  ContextTracker: () => ContextTracker,
2963
+ ContextGatherer: () => ContextGatherer,
2887
2964
  CodexRunner: () => CodexRunner,
2888
2965
  CodebaseIndexerService: () => CodebaseIndexerService,
2889
2966
  CodebaseIndexer: () => CodebaseIndexer,
@@ -2891,6 +2968,7 @@ __export(exports_index_node, {
2891
2968
  CiModule: () => CiModule,
2892
2969
  CODEX_MODELS: () => CODEX_MODELS,
2893
2970
  CLAUDE_MODELS: () => CLAUDE_MODELS,
2971
+ BaseJob: () => BaseJob,
2894
2972
  AuthModule: () => AuthModule,
2895
2973
  AgentWorker: () => AgentWorker,
2896
2974
  AgentOrchestrator: () => AgentOrchestrator
@@ -4945,206 +5023,2054 @@ init_git_utils();
4945
5023
  // src/index-node.ts
4946
5024
  init_src();
4947
5025
 
4948
- // src/orchestrator/index.ts
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
4949
5051
  init_git_utils();
4950
- init_src();
4951
- init_colors();
4952
- init_resolve_bin();
4953
5052
  var import_node_child_process7 = require("node:child_process");
4954
5053
  var import_node_fs9 = require("node:fs");
4955
5054
  var import_node_path10 = require("node:path");
4956
- var import_node_url = require("node:url");
4957
5055
  var import_shared4 = require("@locusai/shared");
4958
- var import_events4 = require("events");
4959
-
4960
- class AgentOrchestrator extends import_events4.EventEmitter {
4961
- client;
4962
- config;
4963
- isRunning = false;
4964
- processedTasks = new Set;
4965
- resolvedSprintId = null;
4966
- agentState = null;
4967
- heartbeatInterval = null;
4968
- constructor(config) {
4969
- super();
4970
- this.config = config;
4971
- this.client = new LocusClient({
4972
- baseUrl: config.apiBase,
4973
- token: config.apiKey
4974
- });
4975
- }
4976
- async resolveSprintId() {
4977
- if (this.config.sprintId) {
4978
- return this.config.sprintId;
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
+ };
4979
5068
  }
5069
+ let outdated;
4980
5070
  try {
4981
- const sprint = await this.client.sprints.getActive(this.config.workspaceId);
4982
- if (sprint?.id) {
4983
- console.log(c.info(`\uD83D\uDCCB Using active sprint: ${sprint.name}`));
4984
- return sprint.id;
4985
- }
4986
- } catch {}
4987
- console.log(c.dim("ℹ No sprint specified, working with all workspace tasks"));
4988
- return "";
4989
- }
4990
- async start() {
4991
- if (this.isRunning) {
4992
- throw new Error("Orchestrator is already running");
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
+ };
4993
5080
  }
4994
- this.isRunning = true;
4995
- this.processedTasks.clear();
5081
+ let vulnerabilities;
4996
5082
  try {
4997
- await this.orchestrationLoop();
4998
- } catch (error) {
4999
- this.emit("error", error);
5000
- throw error;
5001
- } finally {
5002
- await this.cleanup();
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");
5003
5104
  }
5004
- }
5005
- async orchestrationLoop() {
5006
- this.resolvedSprintId = await this.resolveSprintId();
5007
- this.emit("started", {
5008
- timestamp: new Date,
5009
- config: this.config,
5010
- sprintId: this.resolvedSprintId
5011
- });
5012
- this.printBanner();
5013
- const tasks2 = await this.getAvailableTasks();
5014
- if (tasks2.length === 0) {
5015
- console.log(c.dim("ℹ No available tasks found in the backlog."));
5016
- return;
5105
+ if (vulnerabilities.length > 0) {
5106
+ summaryParts.push(`${vulnerabilities.length} vulnerability(ies)`);
5017
5107
  }
5018
- if (!this.preflightChecks())
5019
- return;
5020
- this.startHeartbeatMonitor();
5021
- await this.spawnAgent();
5022
- await this.waitForAgent();
5023
- console.log(`
5024
- ${c.success("✅ Orchestrator finished")}`);
5025
- }
5026
- printBanner() {
5027
- console.log(`
5028
- ${c.primary("\uD83E\uDD16 Locus Agent Orchestrator")}`);
5029
- console.log(c.dim("----------------------------------------------"));
5030
- console.log(`${c.bold("Workspace:")} ${this.config.workspaceId}`);
5031
- if (this.resolvedSprintId) {
5032
- console.log(`${c.bold("Sprint:")} ${this.resolvedSprintId}`);
5108
+ if (filesChanged > 0) {
5109
+ summaryParts.push(`auto-updated ${patch.length + minor.length} safe package(s)`);
5033
5110
  }
5034
- console.log(`${c.bold("API Base:")} ${this.config.apiBase}`);
5035
- console.log(c.dim(`----------------------------------------------
5036
- `));
5111
+ const summary = `Dependency check: ${summaryParts.join(", ")} (${pm})`;
5112
+ return {
5113
+ summary,
5114
+ suggestions: suggestions2,
5115
+ filesChanged,
5116
+ prUrl
5117
+ };
5037
5118
  }
5038
- preflightChecks() {
5039
- if (!isGitAvailable()) {
5040
- console.log(c.error("git is not installed. Install from https://git-scm.com/"));
5041
- return false;
5042
- }
5043
- if (!isGhAvailable(this.config.projectPath)) {
5044
- 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/"));
5045
- }
5046
- return true;
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;
5047
5129
  }
5048
- async getAvailableTasks() {
5049
- try {
5050
- const tasks2 = await this.client.tasks.getAvailable(this.config.workspaceId, this.resolvedSprintId || undefined);
5051
- return tasks2.filter((task) => !this.processedTasks.has(task.id));
5052
- } catch (error) {
5053
- this.emit("error", error);
5054
- return [];
5130
+ getOutdatedPackages(pm, projectPath) {
5131
+ const output = this.runOutdatedCommand(pm, projectPath);
5132
+ if (pm === "bun") {
5133
+ return this.parseBunOutdated(output.stdout);
5055
5134
  }
5135
+ return this.parseJsonOutdated(pm, output.stdout);
5056
5136
  }
5057
- async spawnAgent() {
5058
- const agentId = `agent-0-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
5059
- this.agentState = {
5060
- id: agentId,
5061
- status: "IDLE",
5062
- currentTaskId: null,
5063
- tasksCompleted: 0,
5064
- tasksFailed: 0,
5065
- lastHeartbeat: new Date
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"]
5066
5143
  };
5067
- console.log(`${c.primary("\uD83D\uDE80 Agent started:")} ${c.bold(agentId)}
5144
+ const [bin, ...args] = commands[pm];
5145
+ return this.exec(bin, args, projectPath);
5146
+ }
5147
+ parseBunOutdated(stdout) {
5148
+ const packages = [];
5149
+ const lines = stdout.split(`
5068
5150
  `);
5069
- const workerPath = this.resolveWorkerPath();
5070
- if (!workerPath) {
5071
- throw new Error("Worker file not found. Make sure the SDK is properly built and installed.");
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
+ });
5072
5165
  }
5073
- const workerArgs = this.buildWorkerArgs(agentId);
5074
- const agentProcess = import_node_child_process7.spawn(process.execPath, [workerPath, ...workerArgs], {
5075
- stdio: ["pipe", "pipe", "pipe"],
5076
- detached: true,
5077
- env: getAugmentedEnv({
5078
- FORCE_COLOR: "1",
5079
- TERM: "xterm-256color",
5080
- LOCUS_WORKER: agentId,
5081
- LOCUS_WORKSPACE: this.config.workspaceId
5082
- })
5083
- });
5084
- this.agentState.process = agentProcess;
5085
- this.attachProcessHandlers(agentId, this.agentState, agentProcess);
5086
- this.emit("agent:spawned", { agentId });
5166
+ return packages;
5087
5167
  }
5088
- async waitForAgent() {
5089
- while (this.agentState && this.isRunning) {
5090
- await sleep(2000);
5168
+ parseJsonOutdated(pm, stdout) {
5169
+ const packages = [];
5170
+ if (!stdout.trim())
5171
+ return packages;
5172
+ if (pm === "yarn") {
5173
+ return this.parseYarnOutdated(stdout);
5091
5174
  }
5175
+ let data;
5176
+ try {
5177
+ data = JSON.parse(stdout);
5178
+ } catch {
5179
+ return packages;
5180
+ }
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
+ });
5192
+ }
5193
+ return packages;
5092
5194
  }
5093
- startHeartbeatMonitor() {
5094
- this.heartbeatInterval = setInterval(() => {
5095
- if (!this.agentState)
5096
- return;
5097
- const now = Date.now();
5098
- if (this.agentState.status === "WORKING" && now - this.agentState.lastHeartbeat.getTime() > import_shared4.STALE_AGENT_TIMEOUT_MS) {
5099
- console.log(c.error(`Agent ${this.agentState.id} is stale (no heartbeat for 10 minutes). Killing.`));
5100
- if (this.agentState.process && !this.agentState.process.killed) {
5101
- killProcessTree(this.agentState.process);
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
+ }
5102
5216
  }
5103
- this.emit("agent:stale", { agentId: this.agentState.id });
5217
+ } catch {}
5218
+ }
5219
+ return packages;
5220
+ }
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"]
5227
+ };
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
+ });
5104
5250
  }
5105
- }, 60000);
5251
+ } catch {}
5252
+ return vulnerabilities;
5106
5253
  }
5107
- async stop() {
5108
- this.isRunning = false;
5109
- await this.cleanup();
5110
- this.emit("stopped", { timestamp: new Date });
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 {}
5272
+ }
5273
+ return vulnerabilities;
5111
5274
  }
5112
- stopAgent(agentId) {
5113
- if (!this.agentState || this.agentState.id !== agentId)
5114
- return false;
5115
- if (this.agentState.process && !this.agentState.process.killed) {
5116
- killProcessTree(this.agentState.process);
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 };
5117
5282
  }
5118
- return true;
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 };
5297
+ }
5298
+ const prUrl = this.commitAndPush(projectPath, changedFiles, safePackages, pm);
5299
+ return { filesChanged: changedFiles.length, prUrl };
5119
5300
  }
5120
- async cleanup() {
5121
- if (this.heartbeatInterval) {
5122
- clearInterval(this.heartbeatInterval);
5123
- this.heartbeatInterval = null;
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;
5124
5316
  }
5125
- if (this.agentState?.process && !this.agentState.process.killed) {
5126
- console.log(`Killing agent: ${this.agentState.id}`);
5127
- killProcessTree(this.agentState.process);
5317
+ }
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);
5327
+ }
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;
5128
5386
  }
5129
5387
  }
5130
- getStats() {
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
+ });
5419
+ }
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
+ });
5434
+ }
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;
5131
5452
  return {
5132
- activeAgents: this.agentState ? 1 : 0,
5133
- totalTasksCompleted: this.agentState?.tasksCompleted ?? 0,
5134
- totalTasksFailed: this.agentState?.tasksFailed ?? 0,
5135
- processedTasks: this.processedTasks.size
5453
+ major: parseInt(match[1], 10),
5454
+ minor: parseInt(match[2], 10),
5455
+ patch: parseInt(match[3], 10)
5136
5456
  };
5137
5457
  }
5138
- getAgentStates() {
5139
- return this.agentState ? [this.agentState] : [];
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
+ }
5140
5470
  }
5141
- buildWorkerArgs(agentId) {
5142
- const args = [
5143
- "--agent-id",
5144
- agentId,
5145
- "--workspace-id",
5146
- this.config.workspaceId,
5147
- "--api-url",
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] : [];
7066
+ }
7067
+ buildWorkerArgs(agentId) {
7068
+ const args = [
7069
+ "--agent-id",
7070
+ agentId,
7071
+ "--workspace-id",
7072
+ this.config.workspaceId,
7073
+ "--api-url",
5148
7074
  this.config.apiBase,
5149
7075
  "--api-key",
5150
7076
  this.config.apiKey,
@@ -5220,14 +7146,14 @@ ${agentId} finished (exit code: ${code})`);
5220
7146
  }
5221
7147
  resolveWorkerPath() {
5222
7148
  const currentModulePath = import_node_url.fileURLToPath("file:///home/runner/work/locusai/locusai/packages/sdk/src/orchestrator/index.ts");
5223
- const currentModuleDir = import_node_path10.dirname(currentModulePath);
7149
+ const currentModuleDir = import_node_path15.dirname(currentModulePath);
5224
7150
  const potentialPaths = [
5225
- import_node_path10.join(currentModuleDir, "..", "agent", "worker.js"),
5226
- import_node_path10.join(currentModuleDir, "agent", "worker.js"),
5227
- import_node_path10.join(currentModuleDir, "worker.js"),
5228
- import_node_path10.join(currentModuleDir, "..", "agent", "worker.ts")
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")
5229
7155
  ];
5230
- return potentialPaths.find((p) => import_node_fs9.existsSync(p));
7156
+ return potentialPaths.find((p) => import_node_fs13.existsSync(p));
5231
7157
  }
5232
7158
  }
5233
7159
  function killProcessTree(proc) {
@@ -5246,11 +7172,11 @@ function sleep(ms) {
5246
7172
  }
5247
7173
  // src/planning/plan-manager.ts
5248
7174
  init_config();
5249
- var import_node_fs10 = require("node:fs");
5250
- var import_node_path11 = require("node:path");
7175
+ var import_node_fs14 = require("node:fs");
7176
+ var import_node_path16 = require("node:path");
5251
7177
 
5252
7178
  // src/planning/sprint-plan.ts
5253
- var import_shared5 = require("@locusai/shared");
7179
+ var import_shared11 = require("@locusai/shared");
5254
7180
  var import_zod2 = require("zod");
5255
7181
 
5256
7182
  // src/utils/structured-output.ts
@@ -5358,7 +7284,7 @@ function plannedTasksToCreatePayloads(plan, sprintId) {
5358
7284
  return plan.tasks.map((task) => ({
5359
7285
  title: task.title,
5360
7286
  description: task.description,
5361
- status: import_shared5.TaskStatus.BACKLOG,
7287
+ status: import_shared11.TaskStatus.BACKLOG,
5362
7288
  assigneeRole: task.assigneeRole,
5363
7289
  priority: task.priority,
5364
7290
  labels: task.labels,
@@ -5412,19 +7338,19 @@ class PlanManager {
5412
7338
  save(plan) {
5413
7339
  this.ensurePlansDir();
5414
7340
  const slug = this.slugify(plan.name);
5415
- const jsonPath = import_node_path11.join(this.plansDir, `${slug}.json`);
5416
- const mdPath = import_node_path11.join(this.plansDir, `sprint-${slug}.md`);
5417
- import_node_fs10.writeFileSync(jsonPath, JSON.stringify(plan, null, 2), "utf-8");
5418
- import_node_fs10.writeFileSync(mdPath, sprintPlanToMarkdown(plan), "utf-8");
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");
5419
7345
  return plan.id;
5420
7346
  }
5421
7347
  load(idOrSlug) {
5422
7348
  this.ensurePlansDir();
5423
- const files = import_node_fs10.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
7349
+ const files = import_node_fs14.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
5424
7350
  for (const file of files) {
5425
- const filePath = import_node_path11.join(this.plansDir, file);
7351
+ const filePath = import_node_path16.join(this.plansDir, file);
5426
7352
  try {
5427
- const plan = JSON.parse(import_node_fs10.readFileSync(filePath, "utf-8"));
7353
+ const plan = JSON.parse(import_node_fs14.readFileSync(filePath, "utf-8"));
5428
7354
  if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
5429
7355
  return plan;
5430
7356
  }
@@ -5434,11 +7360,11 @@ class PlanManager {
5434
7360
  }
5435
7361
  list(status) {
5436
7362
  this.ensurePlansDir();
5437
- const files = import_node_fs10.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
7363
+ const files = import_node_fs14.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
5438
7364
  const plans = [];
5439
7365
  for (const file of files) {
5440
7366
  try {
5441
- const plan = JSON.parse(import_node_fs10.readFileSync(import_node_path11.join(this.plansDir, file), "utf-8"));
7367
+ const plan = JSON.parse(import_node_fs14.readFileSync(import_node_path16.join(this.plansDir, file), "utf-8"));
5442
7368
  if (!status || plan.status === status) {
5443
7369
  plans.push(plan);
5444
7370
  }
@@ -5495,18 +7421,18 @@ class PlanManager {
5495
7421
  }
5496
7422
  delete(idOrSlug) {
5497
7423
  this.ensurePlansDir();
5498
- const files = import_node_fs10.readdirSync(this.plansDir);
7424
+ const files = import_node_fs14.readdirSync(this.plansDir);
5499
7425
  for (const file of files) {
5500
- const filePath = import_node_path11.join(this.plansDir, file);
7426
+ const filePath = import_node_path16.join(this.plansDir, file);
5501
7427
  if (!file.endsWith(".json"))
5502
7428
  continue;
5503
7429
  try {
5504
- const plan = JSON.parse(import_node_fs10.readFileSync(filePath, "utf-8"));
7430
+ const plan = JSON.parse(import_node_fs14.readFileSync(filePath, "utf-8"));
5505
7431
  if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
5506
- import_node_fs10.unlinkSync(filePath);
5507
- const mdPath = import_node_path11.join(this.plansDir, `sprint-${this.slugify(plan.name)}.md`);
5508
- if (import_node_fs10.existsSync(mdPath)) {
5509
- import_node_fs10.unlinkSync(mdPath);
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);
5510
7436
  }
5511
7437
  return;
5512
7438
  }
@@ -5520,8 +7446,8 @@ class PlanManager {
5520
7446
  return sprintPlanToMarkdown(plan);
5521
7447
  }
5522
7448
  ensurePlansDir() {
5523
- if (!import_node_fs10.existsSync(this.plansDir)) {
5524
- import_node_fs10.mkdirSync(this.plansDir, { recursive: true });
7449
+ if (!import_node_fs14.existsSync(this.plansDir)) {
7450
+ import_node_fs14.mkdirSync(this.plansDir, { recursive: true });
5525
7451
  }
5526
7452
  }
5527
7453
  slugify(name) {
@@ -5530,8 +7456,8 @@ class PlanManager {
5530
7456
  }
5531
7457
  // src/planning/planning-meeting.ts
5532
7458
  init_config();
5533
- var import_node_fs11 = require("node:fs");
5534
- var import_node_path12 = require("node:path");
7459
+ var import_node_fs15 = require("node:fs");
7460
+ var import_node_path17 = require("node:path");
5535
7461
 
5536
7462
  // src/planning/agents/planner.ts
5537
7463
  function buildPlannerPrompt(input) {
@@ -5617,8 +7543,8 @@ class PlanningMeeting {
5617
7543
  async run(directive, feedback) {
5618
7544
  this.log("Planning sprint...", "info");
5619
7545
  const plansDir = getLocusPath(this.projectPath, "plansDir");
5620
- if (!import_node_fs11.existsSync(plansDir)) {
5621
- import_node_fs11.mkdirSync(plansDir, { recursive: true });
7546
+ if (!import_node_fs15.existsSync(plansDir)) {
7547
+ import_node_fs15.mkdirSync(plansDir, { recursive: true });
5622
7548
  }
5623
7549
  const ts = Date.now();
5624
7550
  const planId = `plan-${ts}`;
@@ -5632,11 +7558,11 @@ class PlanningMeeting {
5632
7558
  });
5633
7559
  const response = await this.aiRunner.run(prompt);
5634
7560
  this.log("Planning meeting complete.", "success");
5635
- const expectedPath = import_node_path12.join(plansDir, `${fileName}.json`);
7561
+ const expectedPath = import_node_path17.join(plansDir, `${fileName}.json`);
5636
7562
  let plan = null;
5637
- if (import_node_fs11.existsSync(expectedPath)) {
7563
+ if (import_node_fs15.existsSync(expectedPath)) {
5638
7564
  try {
5639
- plan = JSON.parse(import_node_fs11.readFileSync(expectedPath, "utf-8"));
7565
+ plan = JSON.parse(import_node_fs15.readFileSync(expectedPath, "utf-8"));
5640
7566
  } catch {}
5641
7567
  }
5642
7568
  if (!plan) {