@locusai/server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,304 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ export interface ProcessorConfig {
5
+ ciPresetsPath: string;
6
+ repoPath: string;
7
+ }
8
+
9
+ interface RawTask {
10
+ id: number;
11
+ title: string;
12
+ description: string;
13
+ labels: string;
14
+ acceptanceChecklist: string;
15
+ }
16
+
17
+ export class TaskProcessor {
18
+ constructor(
19
+ private db: Database,
20
+ private config: ProcessorConfig
21
+ ) {}
22
+
23
+ async onStatusChanged(taskId: string, from: string, to: string) {
24
+ console.log(`[TaskProcessor] Task ${taskId} moved from ${from} to ${to}`);
25
+
26
+ if (to === "IN_PROGRESS") {
27
+ await this.handleInProgress(taskId);
28
+ } else if (to === "VERIFICATION") {
29
+ await this.handleVerification(taskId);
30
+ }
31
+ }
32
+
33
+ private async handleInProgress(taskId: string) {
34
+ try {
35
+ const rawTask = this.db
36
+ .prepare("SELECT * FROM tasks WHERE id = ?")
37
+ .get(taskId) as RawTask | undefined;
38
+ if (!rawTask) return;
39
+
40
+ const labels = JSON.parse(rawTask.labels || "[]");
41
+
42
+ // 1. Create a "Technical Implementation Draft" artifact
43
+ await this.createTechnicalDraft(
44
+ taskId,
45
+ rawTask.title,
46
+ rawTask.description
47
+ );
48
+
49
+ // 2. Update acceptance checklist if empty
50
+ await this.initChecklist(taskId, rawTask.acceptanceChecklist);
51
+
52
+ // 3. (Optional) Trigger CI if requested via labels
53
+ const presets = JSON.parse(
54
+ readFileSync(this.config.ciPresetsPath, "utf-8")
55
+ );
56
+ if (presets.quick && labels.includes("auto-ci")) {
57
+ await this.runCi(taskId, "quick");
58
+ }
59
+
60
+ // 4. Create git branch
61
+ const slug = this.slugify(rawTask.title);
62
+ const branchName = `task/${taskId}-${slug}`;
63
+ await this.createBranch(branchName);
64
+ } catch (err) {
65
+ console.error(
66
+ "[TaskProcessor] Failed to process In Progress transition:",
67
+ err
68
+ );
69
+ }
70
+ }
71
+
72
+ private async createTechnicalDraft(
73
+ taskId: string,
74
+ title: string,
75
+ description: string
76
+ ) {
77
+ const draftContent = `
78
+ # Implementation Plan: ${title}
79
+
80
+ ## Objective
81
+ ${description || "No description provided."}
82
+
83
+ ## Suggested Steps
84
+ 1. [ ] Analyze current codebase for related components.
85
+ 2. [ ] Research potential side effects of the change.
86
+ 3. [ ] Implement the core logic.
87
+ 4. [ ] Run automated tests to verify.
88
+ 5. [ ] Perform manual verification.
89
+
90
+ ## Quality Gates
91
+ - [ ] Code follows project style guidelines.
92
+ - [ ] No regression in existing functionality.
93
+ - [ ] Documentation updated if necessary.
94
+
95
+ ---
96
+ *This is an automatically generated draft to kickstart the task.*
97
+ `.trim();
98
+
99
+ this.db
100
+ .prepare(
101
+ `
102
+ INSERT INTO artifacts (taskId, type, title, contentText, createdBy, createdAt)
103
+ VALUES (?, ?, ?, ?, ?, ?)
104
+ `
105
+ )
106
+ .run(
107
+ taskId,
108
+ "TECH_DRAFT",
109
+ `Draft: ${title}`,
110
+ draftContent,
111
+ "system",
112
+ Date.now()
113
+ );
114
+ }
115
+
116
+ private async initChecklist(taskId: string, currentChecklistJson: string) {
117
+ const current = JSON.parse(currentChecklistJson || "[]");
118
+ if (current.length > 0) return; // Don't overwrite existing checklist
119
+
120
+ const defaultChecklist = [
121
+ { id: "step-1", text: "Research & Planning", done: false },
122
+ { id: "step-2", text: "Implementation", done: false },
123
+ { id: "step-3", text: "Testing & Verification", done: false },
124
+ ];
125
+
126
+ this.db
127
+ .prepare(
128
+ "UPDATE tasks SET acceptanceChecklist = ?, updatedAt = ? WHERE id = ?"
129
+ )
130
+ .run(JSON.stringify(defaultChecklist), Date.now(), taskId);
131
+
132
+ this.db
133
+ .prepare(
134
+ "INSERT INTO events (taskId, type, payload, createdAt) VALUES (?, ?, ?, ?)"
135
+ )
136
+ .run(
137
+ taskId,
138
+ "CHECKLIST_INITIALIZED",
139
+ JSON.stringify({ itemCount: defaultChecklist.length }),
140
+ Date.now()
141
+ );
142
+ }
143
+
144
+ private async runCi(taskId: string, preset: string) {
145
+ const presets = JSON.parse(
146
+ readFileSync(this.config.ciPresetsPath, "utf-8")
147
+ );
148
+ const commands = presets[preset];
149
+ if (!commands) return;
150
+
151
+ let allOk = true;
152
+ let combinedOutput = "";
153
+
154
+ for (const cmd of commands) {
155
+ try {
156
+ if (/[;&|><$`\n]/.test(cmd)) throw new Error("Invalid command");
157
+
158
+ const proc = Bun.spawn(cmd.split(" "), {
159
+ cwd: this.config.repoPath,
160
+ stdout: "pipe",
161
+ stderr: "pipe",
162
+ });
163
+
164
+ const stdout = await new Response(proc.stdout).text();
165
+ const stderr = await new Response(proc.stderr).text();
166
+ const exitCode = await proc.exited;
167
+
168
+ combinedOutput += `\n> ${cmd}\n${stdout}${stderr}\n`;
169
+ if (exitCode !== 0) allOk = false;
170
+ } catch (err: unknown) {
171
+ allOk = false;
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ combinedOutput += `\n> ${cmd}\nError: ${message}\n`;
174
+ }
175
+ }
176
+
177
+ const summary = allOk ? "All checks passed" : "Some checks failed";
178
+ const now = Date.now();
179
+
180
+ // Save CI_OUTPUT artifact
181
+ this.db
182
+ .prepare(`
183
+ INSERT INTO artifacts (taskId, type, title, contentText, createdBy, createdAt)
184
+ VALUES (?, ?, ?, ?, ?, ?)
185
+ `)
186
+ .run(
187
+ taskId,
188
+ "CI_OUTPUT",
189
+ `Auto-CI: ${preset}`,
190
+ combinedOutput,
191
+ "system",
192
+ now
193
+ );
194
+
195
+ // Record CI_RAN event
196
+ this.db
197
+ .prepare(
198
+ "INSERT INTO events (taskId, type, payload, createdAt) VALUES (?, ?, ?, ?)"
199
+ )
200
+ .run(
201
+ taskId,
202
+ "CI_RAN",
203
+ JSON.stringify({
204
+ preset,
205
+ ok: allOk,
206
+ summary,
207
+ source: "auto-processor",
208
+ }),
209
+ now
210
+ );
211
+ }
212
+
213
+ private async handleVerification(taskId: string) {
214
+ try {
215
+ const rawTask = this.db
216
+ .prepare("SELECT * FROM tasks WHERE id = ?")
217
+ .get(taskId) as RawTask | undefined;
218
+ if (!rawTask) return;
219
+
220
+ const slug = this.slugify(rawTask.title);
221
+ const branchName = `task/${taskId}-${slug}`;
222
+
223
+ // Create PR (Assumes agent has pushed the branch)
224
+ await this.createPullRequest(
225
+ branchName,
226
+ rawTask.title,
227
+ rawTask.description
228
+ );
229
+ } catch (err) {
230
+ console.error(
231
+ "[TaskProcessor] Failed to process Verification transition:",
232
+ err
233
+ );
234
+ }
235
+ }
236
+
237
+ private slugify(text: string): string {
238
+ return text
239
+ .toLowerCase()
240
+ .replace(/[^a-z0-9]+/g, "-")
241
+ .replace(/^-|-$/g, "");
242
+ }
243
+
244
+ private async createBranch(branchName: string) {
245
+ console.log(`[TaskProcessor] Creating branch ${branchName}...`);
246
+ try {
247
+ const checkProc = Bun.spawn(
248
+ ["git", "show-ref", "--verify", `refs/heads/${branchName}`],
249
+ {
250
+ cwd: this.config.repoPath,
251
+ stdout: "ignore",
252
+ stderr: "ignore",
253
+ }
254
+ );
255
+ const exists = (await checkProc.exited) === 0;
256
+
257
+ if (!exists) {
258
+ // Create branch from HEAD but DO NOT checkout
259
+ // This is safe for parallel agents working in other directories
260
+ await Bun.spawn(["git", "branch", branchName], {
261
+ cwd: this.config.repoPath,
262
+ stdout: "ignore",
263
+ stderr: "pipe",
264
+ }).exited;
265
+ }
266
+ } catch (error) {
267
+ console.error(`[TaskProcessor] Git branch operation failed: ${error}`);
268
+ }
269
+ }
270
+
271
+ private async createPullRequest(
272
+ branchName: string,
273
+ title: string,
274
+ description: string
275
+ ) {
276
+ console.log(`[TaskProcessor] Creating PR for ${branchName}...`);
277
+ try {
278
+ // Create PR using gh cli (headless)
279
+ // We assume the branch has been pushed by the agent
280
+ await Bun.spawn(
281
+ [
282
+ "gh",
283
+ "pr",
284
+ "create",
285
+ "--title",
286
+ `[Task] ${title}`,
287
+ "--body",
288
+ description || "Task implementation",
289
+ "--head",
290
+ branchName,
291
+ "--base",
292
+ "master",
293
+ ],
294
+ {
295
+ cwd: this.config.repoPath,
296
+ stdout: "ignore",
297
+ stderr: "pipe",
298
+ }
299
+ ).exited;
300
+ } catch (error) {
301
+ console.error(`[TaskProcessor] PR creation failed: ${error}`);
302
+ }
303
+ }
304
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }