@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.
- package/LICENSE +21 -0
- package/package.json +24 -0
- package/src/controllers/artifact.controller.ts +45 -0
- package/src/controllers/ci.controller.ts +16 -0
- package/src/controllers/doc.controller.ts +116 -0
- package/src/controllers/event.controller.ts +20 -0
- package/src/controllers/sprint.controller.ts +34 -0
- package/src/controllers/task.controller.ts +98 -0
- package/src/db.ts +107 -0
- package/src/index.ts +133 -0
- package/src/middleware/error.middleware.ts +21 -0
- package/src/repositories/artifact.repository.ts +44 -0
- package/src/repositories/base.repository.ts +5 -0
- package/src/repositories/comment.repository.ts +32 -0
- package/src/repositories/event.repository.ts +34 -0
- package/src/repositories/sprint.repository.ts +60 -0
- package/src/repositories/task.repository.ts +159 -0
- package/src/routes/artifacts.routes.ts +11 -0
- package/src/routes/ci.routes.ts +10 -0
- package/src/routes/docs.routes.ts +13 -0
- package/src/routes/events.routes.ts +10 -0
- package/src/routes/sprints.routes.ts +12 -0
- package/src/routes/tasks.routes.ts +18 -0
- package/src/services/ci.service.ts +65 -0
- package/src/services/sprint.service.ts +28 -0
- package/src/services/task.service.ts +182 -0
- package/src/task-processor.ts +304 -0
- package/tsconfig.json +8 -0
|
@@ -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
|
+
}
|