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