@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.
- package/dist/agent/worker.js +67 -4
- package/dist/ai/claude-runner.d.ts +1 -0
- package/dist/ai/claude-runner.d.ts.map +1 -1
- package/dist/ai/codex-runner.d.ts +1 -0
- package/dist/ai/codex-runner.d.ts.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/index-node.d.ts +1 -0
- package/dist/index-node.d.ts.map +1 -1
- package/dist/index-node.js +2130 -204
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -0
- package/dist/jobs/__tests__/job-runner.test.d.ts +2 -0
- package/dist/jobs/__tests__/job-runner.test.d.ts.map +1 -0
- package/dist/jobs/__tests__/lint-scan.test.d.ts +2 -0
- package/dist/jobs/__tests__/lint-scan.test.d.ts.map +1 -0
- package/dist/jobs/__tests__/scheduler.test.d.ts +2 -0
- package/dist/jobs/__tests__/scheduler.test.d.ts.map +1 -0
- package/dist/jobs/base-job.d.ts +29 -0
- package/dist/jobs/base-job.d.ts.map +1 -0
- package/dist/jobs/default-registry.d.ts +3 -0
- package/dist/jobs/default-registry.d.ts.map +1 -0
- package/dist/jobs/index.d.ts +12 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/job-registry.d.ts +10 -0
- package/dist/jobs/job-registry.d.ts.map +1 -0
- package/dist/jobs/job-runner.d.ts +33 -0
- package/dist/jobs/job-runner.d.ts.map +1 -0
- package/dist/jobs/proposals/context-gatherer.d.ts +35 -0
- package/dist/jobs/proposals/context-gatherer.d.ts.map +1 -0
- package/dist/jobs/proposals/index.d.ts +4 -0
- package/dist/jobs/proposals/index.d.ts.map +1 -0
- package/dist/jobs/proposals/proposal-engine.d.ts +21 -0
- package/dist/jobs/proposals/proposal-engine.d.ts.map +1 -0
- package/dist/jobs/scans/dependency-scan.d.ts +28 -0
- package/dist/jobs/scans/dependency-scan.d.ts.map +1 -0
- package/dist/jobs/scans/index.d.ts +5 -0
- package/dist/jobs/scans/index.d.ts.map +1 -0
- package/dist/jobs/scans/lint-scan.d.ts +20 -0
- package/dist/jobs/scans/lint-scan.d.ts.map +1 -0
- package/dist/jobs/scans/test-scan.d.ts +20 -0
- package/dist/jobs/scans/test-scan.d.ts.map +1 -0
- package/dist/jobs/scans/todo-scan.d.ts +15 -0
- package/dist/jobs/scans/todo-scan.d.ts.map +1 -0
- package/dist/jobs/scheduler.d.ts +80 -0
- package/dist/jobs/scheduler.d.ts.map +1 -0
- package/dist/modules/jobs.d.ts +14 -0
- package/dist/modules/jobs.d.ts.map +1 -0
- package/dist/modules/suggestions.d.ts +12 -0
- package/dist/modules/suggestions.d.ts.map +1 -0
- package/package.json +7 -5
package/dist/index-node.js
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
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
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
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
|
-
|
|
4995
|
-
this.processedTasks.clear();
|
|
5081
|
+
let vulnerabilities;
|
|
4996
5082
|
try {
|
|
4997
|
-
|
|
4998
|
-
} catch
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
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
|
-
|
|
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 (
|
|
5019
|
-
|
|
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
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5111
|
+
const summary = `Dependency check: ${summaryParts.join(", ")} (${pm})`;
|
|
5112
|
+
return {
|
|
5113
|
+
summary,
|
|
5114
|
+
suggestions: suggestions2,
|
|
5115
|
+
filesChanged,
|
|
5116
|
+
prUrl
|
|
5117
|
+
};
|
|
5037
5118
|
}
|
|
5038
|
-
|
|
5039
|
-
if (
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
if (
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
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
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
return
|
|
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
|
-
|
|
5058
|
-
const
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
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
|
-
|
|
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
|
|
5070
|
-
|
|
5071
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
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
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
5251
|
+
} catch {}
|
|
5252
|
+
return vulnerabilities;
|
|
5106
5253
|
}
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
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
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
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
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
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
|
-
|
|
5139
|
-
|
|
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
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
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 =
|
|
7149
|
+
const currentModuleDir = import_node_path15.dirname(currentModulePath);
|
|
5224
7150
|
const potentialPaths = [
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
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) =>
|
|
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
|
|
5250
|
-
var
|
|
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
|
|
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:
|
|
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 =
|
|
5416
|
-
const mdPath =
|
|
5417
|
-
|
|
5418
|
-
|
|
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 =
|
|
7349
|
+
const files = import_node_fs14.readdirSync(this.plansDir).filter((f) => f.endsWith(".json"));
|
|
5424
7350
|
for (const file of files) {
|
|
5425
|
-
const filePath =
|
|
7351
|
+
const filePath = import_node_path16.join(this.plansDir, file);
|
|
5426
7352
|
try {
|
|
5427
|
-
const plan = JSON.parse(
|
|
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 =
|
|
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(
|
|
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 =
|
|
7424
|
+
const files = import_node_fs14.readdirSync(this.plansDir);
|
|
5499
7425
|
for (const file of files) {
|
|
5500
|
-
const filePath =
|
|
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(
|
|
7430
|
+
const plan = JSON.parse(import_node_fs14.readFileSync(filePath, "utf-8"));
|
|
5505
7431
|
if (plan.id === idOrSlug || this.slugify(plan.name) === idOrSlug) {
|
|
5506
|
-
|
|
5507
|
-
const mdPath =
|
|
5508
|
-
if (
|
|
5509
|
-
|
|
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 (!
|
|
5524
|
-
|
|
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
|
|
5534
|
-
var
|
|
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 (!
|
|
5621
|
-
|
|
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 =
|
|
7561
|
+
const expectedPath = import_node_path17.join(plansDir, `${fileName}.json`);
|
|
5636
7562
|
let plan = null;
|
|
5637
|
-
if (
|
|
7563
|
+
if (import_node_fs15.existsSync(expectedPath)) {
|
|
5638
7564
|
try {
|
|
5639
|
-
plan = JSON.parse(
|
|
7565
|
+
plan = JSON.parse(import_node_fs15.readFileSync(expectedPath, "utf-8"));
|
|
5640
7566
|
} catch {}
|
|
5641
7567
|
}
|
|
5642
7568
|
if (!plan) {
|