fission-worker 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/docker.d.ts +1 -1
  2. package/dist/docker.js +65 -57
  3. package/dist/docker.js.map +1 -1
  4. package/package.json +4 -3
  5. package/runner/common/decorators/current-user.decorator.js +10 -0
  6. package/runner/common/decorators/public.decorator.js +7 -0
  7. package/runner/common/decorators/roles.decorator.js +7 -0
  8. package/runner/common/services/activity-log.service.js +48 -0
  9. package/runner/common/services/event-bus.service.js +38 -0
  10. package/runner/modules/github/github.controller.js +155 -0
  11. package/runner/modules/github/github.module.js +22 -0
  12. package/runner/modules/github/github.service.js +104 -0
  13. package/runner/modules/pipeline/data/api-data.service.js +140 -0
  14. package/runner/modules/pipeline/data/pipeline-data.interface.js +2 -0
  15. package/runner/modules/pipeline/data/prisma-data.service.js +149 -0
  16. package/runner/modules/pipeline/pipeline-cto.service.js +129 -0
  17. package/runner/modules/pipeline/pipeline-helpers.service.js +318 -0
  18. package/runner/modules/pipeline/pipeline-orchestrator.js +399 -0
  19. package/runner/modules/pipeline/pipeline-queue.service.js +121 -0
  20. package/runner/modules/pipeline/pipeline-techlead.service.js +127 -0
  21. package/runner/modules/pipeline/pipeline-worker.service.js +343 -0
  22. package/runner/modules/pipeline/pipeline.controller.js +310 -0
  23. package/runner/modules/pipeline/pipeline.module.js +51 -0
  24. package/runner/modules/pipeline/pipeline.service.js +706 -0
  25. package/runner/modules/worker-api/worker-api.controller.js +497 -0
  26. package/runner/modules/worker-api/worker-api.guard.js +41 -0
  27. package/runner/modules/worker-api/worker-api.module.js +25 -0
  28. package/runner/modules/worker-api/worker-dispatch.service.js +87 -0
  29. package/runner/pipeline-runner/index.js +108 -0
  30. package/runner/prisma/prisma.service.js +23 -0
@@ -0,0 +1,399 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runPipelineLoop = runPipelineLoop;
37
+ const common_1 = require("@nestjs/common");
38
+ const logger = new common_1.Logger("PipelineOrchestrator");
39
+ /**
40
+ * Core pipeline orchestration loop — portable across monolith and worker containers.
41
+ * The caller provides all context (repos, project context, data source).
42
+ * This function runs the checkAndSpawn loop, captures diffs, and generates summaries.
43
+ */
44
+ async function runPipelineLoop(ctx) {
45
+ const { data, helpers, cto, techLead, worker, sessionId, projectId, repoPath, repoPaths, repoList, projectContext, userId, stopSignal, } = ctx;
46
+ // Capture git HEAD for each repo before any work starts (for diff)
47
+ const preWorkHeads = {};
48
+ const repoDirs = repoList.map((r) => r.repoPath);
49
+ for (const dir of repoDirs) {
50
+ try {
51
+ preWorkHeads[dir] = helpers.gitRevParse(dir);
52
+ }
53
+ catch {
54
+ // not a git repo — ignore
55
+ }
56
+ }
57
+ // Clear stale state from previous runs
58
+ helpers.workerTasks.clear();
59
+ helpers.slot3Info = null;
60
+ worker.clearSessions();
61
+ const MAX_WORKERS = 2;
62
+ const MAX_ITERATIONS = 20;
63
+ let iteration = 0;
64
+ let activeWorkers = 0;
65
+ let techLeadActive = false;
66
+ let ctoActive = false;
67
+ // Track running promises
68
+ let runningTasks = [];
69
+ const isStopped = () => stopSignal?.stopped === true;
70
+ const checkAndSpawn = async () => {
71
+ if (isStopped())
72
+ return;
73
+ // ============================================================ //
74
+ // Query all bucket states //
75
+ // ============================================================ //
76
+ const qaQueue = await data.findTasks(projectId, {
77
+ status: "IN_REVIEW",
78
+ orderBy: [{ priority: "asc" }, { createdAt: "asc" }],
79
+ });
80
+ const devQueue = await data.findTasks(projectId, {
81
+ status: "READY_TO_START",
82
+ retryCountLt: 3,
83
+ orderBy: [{ priority: "asc" }, { createdAt: "asc" }],
84
+ });
85
+ const failedTasks = await data.findTasks(projectId, {
86
+ status: "FAILED",
87
+ include: { finding: { select: { id: true } } },
88
+ });
89
+ const plannedQueue = await data.findTasks(projectId, {
90
+ status: "PLANNED",
91
+ orderBy: [{ priority: "asc" }, { createdAt: "asc" }],
92
+ });
93
+ const planningFindings = await data.findFindings(projectId, {
94
+ status: "PLANNING",
95
+ });
96
+ const backlogFindings = await data.findFindings(projectId, {
97
+ status: ["NEW", "TRIAGED"],
98
+ });
99
+ const userDirections = await data.findFindings(projectId, {
100
+ source: "user-direction",
101
+ status: ["NEW", "TRIAGED"],
102
+ });
103
+ // ============================================================ //
104
+ // Emit work state for monitoring //
105
+ // ============================================================ //
106
+ data.emit(projectId, "work_state", {
107
+ qaQueue: qaQueue.length,
108
+ devQueue: devQueue.length,
109
+ failedTasks: failedTasks.length,
110
+ plannedTasks: plannedQueue.length,
111
+ backlogFindings: backlogFindings.length,
112
+ planningFindings: planningFindings.length,
113
+ userDirections: userDirections.length,
114
+ iteration,
115
+ activeWorkers,
116
+ techLeadActive,
117
+ ctoActive,
118
+ agents: {
119
+ worker1: helpers.workerTasks.has(0) ? { taskTitle: helpers.workerTasks.get(0), status: "building" } : null,
120
+ worker2: helpers.workerTasks.has(1) ? { taskTitle: helpers.workerTasks.get(1), status: "building" } : null,
121
+ slot3: helpers.slot3Info ? { role: helpers.slot3Info.role, taskTitle: helpers.slot3Info.task, status: "working" } : null,
122
+ },
123
+ });
124
+ // ============================================================ //
125
+ // Worker bucket 3: Auto-replan FAILED tasks (no slot needed) //
126
+ // ============================================================ //
127
+ if (qaQueue.length === 0 && devQueue.length === 0 && failedTasks.length > 0) {
128
+ for (const task of failedTasks) {
129
+ await worker.replanFailedTask({
130
+ id: task.id,
131
+ title: task.title,
132
+ objective: task.objective,
133
+ projectId: task.projectId,
134
+ findingId: task.findingId,
135
+ });
136
+ }
137
+ }
138
+ // ============================================================ //
139
+ // Worker slots (0-1): Priority queue //
140
+ // ============================================================ //
141
+ const workQueue = [
142
+ ...qaQueue.map((t) => ({ task: t, action: "qa" })),
143
+ ...devQueue.map((t) => ({ task: t, action: "build" })),
144
+ ...plannedQueue.map((t) => ({ task: t, action: "build" })),
145
+ ];
146
+ const maxWorkersNow = Math.min(MAX_WORKERS, 3 - (techLeadActive || ctoActive ? 1 : 0));
147
+ let workerSlot = 0;
148
+ while (helpers.workerTasks.has(workerSlot) && workerSlot < MAX_WORKERS)
149
+ workerSlot++;
150
+ while (activeWorkers < maxWorkersNow && workQueue.length > 0) {
151
+ const { task, action } = workQueue.shift();
152
+ const slot = workerSlot;
153
+ workerSlot++;
154
+ while (helpers.workerTasks.has(workerSlot) && workerSlot < MAX_WORKERS)
155
+ workerSlot++;
156
+ activeWorkers++;
157
+ if (action === "qa") {
158
+ logger.log(`[${sessionId}] Spawning QA for: ${task.title} (slot: ${slot})`);
159
+ const qaPromise = (async () => {
160
+ try {
161
+ await worker.runQaAgent(task, projectId, repoPath, slot);
162
+ }
163
+ finally {
164
+ activeWorkers--;
165
+ }
166
+ })();
167
+ runningTasks.push(qaPromise);
168
+ }
169
+ else {
170
+ logger.log(`[${sessionId}] Spawning worker for: ${task.title} (slot: ${slot}, workers: ${activeWorkers})`);
171
+ const buildPromise = (async () => {
172
+ try {
173
+ await worker.runBuildAgent(task, projectId, repoPaths, repoPath, sessionId, projectContext, slot);
174
+ }
175
+ finally {
176
+ activeWorkers--;
177
+ }
178
+ })();
179
+ runningTasks.push(buildPromise);
180
+ }
181
+ }
182
+ // ============================================================ //
183
+ // Slot 3: Tech Lead / CTO priority queue //
184
+ // ============================================================ //
185
+ if (!techLeadActive && !ctoActive) {
186
+ const tlHasWork = planningFindings.length > 0 || backlogFindings.length > 0;
187
+ if (tlHasWork && (activeWorkers + 1) <= 3) {
188
+ techLeadActive = true;
189
+ logger.log(`[${sessionId}] Spawning tech lead (planning: ${planningFindings.length}, backlog: ${backlogFindings.length})`);
190
+ const tlPromise = (async () => {
191
+ try {
192
+ await techLead.runTechLeadAgent(projectId, repoPath, sessionId, projectContext, userId);
193
+ }
194
+ finally {
195
+ techLeadActive = false;
196
+ }
197
+ })();
198
+ runningTasks.push(tlPromise);
199
+ }
200
+ else if (!tlHasWork && activeWorkers === 0) {
201
+ ctoActive = true;
202
+ logger.log(`[${sessionId}] Spawning CTO (all buckets empty)`);
203
+ const ctoPromise = (async () => {
204
+ try {
205
+ await cto.runCtoAgent(projectId, repoPath, sessionId, projectContext, userId, repoPaths, repoList, userDirections);
206
+ }
207
+ finally {
208
+ ctoActive = false;
209
+ }
210
+ })();
211
+ runningTasks.push(ctoPromise);
212
+ }
213
+ }
214
+ };
215
+ try {
216
+ // Main loop
217
+ while (iteration < MAX_ITERATIONS) {
218
+ if (isStopped())
219
+ break;
220
+ iteration++;
221
+ await checkAndSpawn();
222
+ // If nothing is running and nothing to spawn, check one more time then exit
223
+ if (runningTasks.length === 0 && activeWorkers === 0 && !techLeadActive && !ctoActive) {
224
+ const anyWork = await data.findFirstTask(projectId, {
225
+ status: ["READY_TO_START", "PLANNED", "IN_PROGRESS", "IN_REVIEW"],
226
+ });
227
+ const anyFailed = await data.findFirstTask(projectId, { status: "FAILED" });
228
+ const anyPlanning = await data.findFirstFinding(projectId, { status: "PLANNING" });
229
+ const anyBacklog = await data.findFirstFinding(projectId, { status: ["NEW", "TRIAGED"] });
230
+ if (!anyWork && !anyFailed && !anyBacklog && !anyPlanning)
231
+ break;
232
+ }
233
+ // Wait for at least one task to complete before re-checking
234
+ if (runningTasks.length > 0) {
235
+ const settledFlags = runningTasks.map(() => false);
236
+ runningTasks.forEach((p, i) => {
237
+ p.then(() => { settledFlags[i] = true; }).catch(() => { settledFlags[i] = true; });
238
+ });
239
+ await Promise.race(runningTasks.map((p) => p.catch(() => { })));
240
+ await new Promise((r) => setTimeout(r, 1000));
241
+ runningTasks = runningTasks.filter((_, i) => !settledFlags[i]);
242
+ }
243
+ else {
244
+ await new Promise((r) => setTimeout(r, 2000));
245
+ }
246
+ }
247
+ // Wait for ALL remaining tasks to finish before declaring complete
248
+ if (runningTasks.length > 0) {
249
+ logger.log(`[${sessionId}] Waiting for ${runningTasks.length} remaining agent(s) to finish...`);
250
+ await Promise.all(runningTasks.map((p) => p.catch(() => { })));
251
+ }
252
+ // Capture git diffs for the session
253
+ const diffs = {};
254
+ for (const dir of repoDirs) {
255
+ const oldHead = preWorkHeads[dir];
256
+ if (!oldHead)
257
+ continue;
258
+ try {
259
+ const newHead = helpers.gitRevParse(dir);
260
+ if (oldHead !== newHead) {
261
+ diffs[dir] = {
262
+ diff: helpers.gitDiff(dir, oldHead, newHead),
263
+ diffStat: helpers.gitDiffStat(dir, oldHead, newHead),
264
+ };
265
+ }
266
+ }
267
+ catch (err) {
268
+ logger.warn(`Failed to capture post-work diff for ${dir}: ${err}`);
269
+ }
270
+ }
271
+ if (Object.keys(diffs).length > 0) {
272
+ const allDiffs = Object.values(diffs).map((d) => d.diff).join("\n");
273
+ const allDiffStats = Object.values(diffs).map((d) => d.diffStat).join("\n");
274
+ await data.updateSession(sessionId, {
275
+ metadata: { diff: allDiffs.slice(0, 50_000), diffStat: allDiffStats },
276
+ });
277
+ }
278
+ }
279
+ catch (err) {
280
+ logger.error(`Pipeline ${sessionId} failed: ${err}`);
281
+ await data.log("PIPELINE_ERROR", "Session", sessionId, userId, { error: String(err) });
282
+ }
283
+ finally {
284
+ helpers.workerTasks.clear();
285
+ helpers.slot3Info = null;
286
+ worker.clearSessions();
287
+ await data.updateSession(sessionId, { phase: "SHIP" });
288
+ data.emit(projectId, "phase_changed", { sessionId, phase: "SHIP" });
289
+ data.emit(projectId, "pipeline_complete", { sessionId });
290
+ // Generate and store run summary
291
+ const summary = await generateRunSummary(ctx, repoDirs, preWorkHeads).catch((err) => {
292
+ logger.warn(`Failed to generate run summary: ${err}`);
293
+ return null;
294
+ });
295
+ if (summary) {
296
+ const existingSession = await data.findSession(sessionId);
297
+ const existingMeta = existingSession?.metadata || {};
298
+ await data.updateSession(sessionId, {
299
+ metadata: { ...existingMeta, summary },
300
+ }).catch((err) => logger.warn(`Failed to store run summary: ${err}`));
301
+ data.emit(projectId, "pipeline_summary", { sessionId, summary });
302
+ await data.log("PIPELINE_SUMMARY", "Session", sessionId, userId, {
303
+ duration: summary.duration,
304
+ tasksCompleted: summary.tasksCompleted.length,
305
+ tasksFailed: summary.tasksFailed.length,
306
+ findingsCreated: summary.findingsCreated,
307
+ filesChanged: summary.filesChanged.length,
308
+ commits: summary.commitMessages.length,
309
+ });
310
+ }
311
+ await data.updateSession(sessionId, { status: "TERMINATED", endedAt: new Date() });
312
+ await data.log("PIPELINE_FINISH", "Session", sessionId, userId);
313
+ logger.log(`[${sessionId}] All agents finished`);
314
+ }
315
+ }
316
+ /**
317
+ * Generate a structured summary of a pipeline run.
318
+ * Portable — uses only PipelineDataSource + git helpers.
319
+ */
320
+ async function generateRunSummary(ctx, repoDirs, preWorkHeads) {
321
+ const { data, helpers, sessionId, projectId } = ctx;
322
+ const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
323
+ const session = await data.findSession(sessionId);
324
+ const startedAt = session?.startedAt ?? new Date();
325
+ const endedAt = new Date();
326
+ const durationMs = endedAt.getTime() - new Date(startedAt).getTime();
327
+ const totalSeconds = Math.floor(durationMs / 1000);
328
+ const minutes = Math.floor(totalSeconds / 60);
329
+ const seconds = totalSeconds % 60;
330
+ const duration = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
331
+ // Use data source to get completed/failed tasks (with time filter approximation)
332
+ const completedTasks = await data.findTasks(projectId, { status: "COMPLETED" });
333
+ const failedTasks = await data.findTasks(projectId, { status: "FAILED" });
334
+ const tasksCompleted = completedTasks
335
+ .filter((t) => new Date(t.updatedAt) >= new Date(startedAt))
336
+ .map((t) => ({
337
+ id: t.id,
338
+ title: t.title,
339
+ summary: t.metadata?.summary,
340
+ }));
341
+ const tasksFailed = failedTasks
342
+ .filter((t) => new Date(t.updatedAt) >= new Date(startedAt))
343
+ .map((t) => ({
344
+ id: t.id,
345
+ title: t.title,
346
+ error: t.metadata?.error,
347
+ }));
348
+ // Count findings created during this session
349
+ const allFindings = await data.findFindings(projectId, {});
350
+ const findingsCreated = allFindings.filter((f) => new Date(f.createdAt) >= new Date(startedAt)).length;
351
+ const filesChanged = [];
352
+ const commitMessages = [];
353
+ for (const dir of repoDirs) {
354
+ const oldHead = preWorkHeads[dir];
355
+ if (!oldHead)
356
+ continue;
357
+ try {
358
+ const newHead = helpers.gitRevParse(dir);
359
+ if (oldHead === newHead)
360
+ continue;
361
+ const diffStat = helpers.gitDiffStat(dir, oldHead, newHead);
362
+ const fileLines = diffStat.split("\n").filter((l) => l.includes("|"));
363
+ for (const line of fileLines) {
364
+ const fileName = line.split("|")[0].trim();
365
+ if (fileName)
366
+ filesChanged.push(fileName);
367
+ }
368
+ if (helpers.isValidSha(oldHead) && helpers.isValidSha(newHead)) {
369
+ try {
370
+ helpers.validateRepoPath(dir);
371
+ const logOutput = execSync(`git log --oneline ${oldHead}..${newHead}`, {
372
+ cwd: dir,
373
+ encoding: "utf-8",
374
+ });
375
+ for (const line of logOutput.trim().split("\n")) {
376
+ if (line.trim())
377
+ commitMessages.push(line.trim());
378
+ }
379
+ }
380
+ catch {
381
+ // ignore — no commits
382
+ }
383
+ }
384
+ }
385
+ catch {
386
+ // ignore
387
+ }
388
+ }
389
+ return {
390
+ sessionId,
391
+ projectName: session?.project?.name ?? "Unknown",
392
+ duration,
393
+ tasksCompleted,
394
+ tasksFailed,
395
+ findingsCreated,
396
+ filesChanged,
397
+ commitMessages,
398
+ };
399
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var PipelineQueueService_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.PipelineQueueService = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ /**
13
+ * Global singleton queue that ensures only one pipeline runs at a time.
14
+ * Other pipelines wait in FIFO order until the current one finishes.
15
+ */
16
+ let PipelineQueueService = PipelineQueueService_1 = class PipelineQueueService {
17
+ logger = new common_1.Logger(PipelineQueueService_1.name);
18
+ running = null;
19
+ queue = [];
20
+ /** Callback set by PipelineService to actually start execution. */
21
+ startCallback = null;
22
+ /** Register the function that starts pipeline execution. */
23
+ onStartNext(cb) {
24
+ this.startCallback = cb;
25
+ }
26
+ /** True if a pipeline is currently executing. */
27
+ isRunning() {
28
+ return this.running !== null;
29
+ }
30
+ /**
31
+ * Returns the queue position for a session.
32
+ * -1 = not found, 0 = currently running, 1+ = waiting in queue.
33
+ */
34
+ getQueuePosition(sessionId) {
35
+ if (this.running?.sessionId === sessionId)
36
+ return 0;
37
+ const idx = this.queue.findIndex((e) => e.sessionId === sessionId);
38
+ return idx === -1 ? -1 : idx + 1;
39
+ }
40
+ /** Number of entries waiting (not including the currently running one). */
41
+ getQueueLength() {
42
+ return this.queue.length;
43
+ }
44
+ /** Full status snapshot for the REST endpoint. */
45
+ getStatus() {
46
+ return {
47
+ running: this.running
48
+ ? { sessionId: this.running.sessionId, projectId: this.running.projectId }
49
+ : null,
50
+ queue: this.queue.map((e) => ({
51
+ sessionId: e.sessionId,
52
+ projectId: e.projectId,
53
+ })),
54
+ };
55
+ }
56
+ /**
57
+ * Add a pipeline to the queue.
58
+ * If the slot is empty, starts it immediately (position 0).
59
+ * Otherwise queues it (position 1+).
60
+ */
61
+ enqueue(entry) {
62
+ if (!this.running) {
63
+ this.running = entry;
64
+ this.logger.log(`Pipeline ${entry.sessionId} starts immediately (no queue)`);
65
+ this.startCallback?.(entry);
66
+ return { position: 0 };
67
+ }
68
+ this.queue.push(entry);
69
+ const position = this.queue.length;
70
+ this.logger.log(`Pipeline ${entry.sessionId} queued at position ${position}`);
71
+ return { position };
72
+ }
73
+ /**
74
+ * Called when the current pipeline finishes (success or error).
75
+ * Promotes the next entry in the queue, if any.
76
+ */
77
+ dequeue() {
78
+ const finished = this.running;
79
+ this.running = null;
80
+ if (finished) {
81
+ this.logger.log(`Pipeline ${finished.sessionId} finished, slot freed`);
82
+ }
83
+ if (this.queue.length > 0) {
84
+ const next = this.queue.shift();
85
+ this.running = next;
86
+ this.logger.log(`Pipeline ${next.sessionId} promoted from queue, starting`);
87
+ this.startCallback?.(next);
88
+ }
89
+ }
90
+ /**
91
+ * Cancel a session — remove from queue or mark running slot as free.
92
+ * Returns true if the session was found and removed.
93
+ */
94
+ cancel(sessionId) {
95
+ // If it's the running entry, clear the slot (caller handles killing the process)
96
+ if (this.running?.sessionId === sessionId) {
97
+ this.logger.log(`Cancelling running pipeline ${sessionId}`);
98
+ this.running = null;
99
+ // Start the next queued entry
100
+ if (this.queue.length > 0) {
101
+ const next = this.queue.shift();
102
+ this.running = next;
103
+ this.logger.log(`Pipeline ${next.sessionId} promoted from queue after cancel`);
104
+ this.startCallback?.(next);
105
+ }
106
+ return true;
107
+ }
108
+ // If it's in the queue, remove it
109
+ const idx = this.queue.findIndex((e) => e.sessionId === sessionId);
110
+ if (idx !== -1) {
111
+ this.queue.splice(idx, 1);
112
+ this.logger.log(`Removed queued pipeline ${sessionId} from position ${idx + 1}`);
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ };
118
+ exports.PipelineQueueService = PipelineQueueService;
119
+ exports.PipelineQueueService = PipelineQueueService = PipelineQueueService_1 = __decorate([
120
+ (0, common_1.Injectable)()
121
+ ], PipelineQueueService);
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var PipelineTechLeadService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PipelineTechLeadService = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const common_2 = require("@nestjs/common");
19
+ const pipeline_helpers_service_1 = require("./pipeline-helpers.service");
20
+ let PipelineTechLeadService = PipelineTechLeadService_1 = class PipelineTechLeadService {
21
+ data;
22
+ helpers;
23
+ logger = new common_1.Logger(PipelineTechLeadService_1.name);
24
+ constructor(data, helpers) {
25
+ this.data = data;
26
+ this.helpers = helpers;
27
+ }
28
+ async runTechLeadAgent(projectId, repoPath, sessionId, projectContext, userId) {
29
+ this.data.emit(projectId, "phase_changed", { sessionId, phase: "PLAN", reason: "Tech Lead planning" });
30
+ this.logger.log(`[${sessionId}] Tech Lead: planning backlog findings`);
31
+ this.helpers.slot3Info = { role: "Tech Lead", task: "Planning backlog" };
32
+ this.helpers.emitAgentState(projectId);
33
+ // Priority 1: Resume interrupted PLANNING findings from previous runs
34
+ const planningFindings = await this.data.findFindings(projectId, {
35
+ status: "PLANNING",
36
+ orderBy: [{ severity: "asc" }, { createdAt: "asc" }],
37
+ take: 2,
38
+ });
39
+ // Priority 2: Pick up NEW/TRIAGED findings from backlog
40
+ const remainingSlots = 2 - planningFindings.length;
41
+ const newFindings = remainingSlots > 0
42
+ ? await this.data.findFindings(projectId, {
43
+ status: ["NEW", "TRIAGED"],
44
+ orderBy: [{ severity: "asc" }, { createdAt: "asc" }],
45
+ take: remainingSlots,
46
+ })
47
+ : [];
48
+ const findings = [...planningFindings, ...newFindings];
49
+ if (findings.length === 0)
50
+ return;
51
+ // Move NEW/TRIAGED findings to PLANNING status (already-PLANNING ones stay)
52
+ const toMarkPlanning = newFindings.filter((f) => f.status !== "PLANNING").map((f) => f.id);
53
+ if (toMarkPlanning.length > 0) {
54
+ await this.data.updateManyFindings(toMarkPlanning, { status: "PLANNING" });
55
+ }
56
+ const findingsSummary = findings.map((f) => ({
57
+ id: f.id,
58
+ title: f.title,
59
+ description: f.description,
60
+ severity: f.severity,
61
+ category: f.category,
62
+ }));
63
+ this.helpers.slot3Info = { role: "Tech Lead", task: `Planning: ${findings[0].title}` };
64
+ this.helpers.emitAgentState(projectId);
65
+ const planPrompt = (projectContext ? `Project context:\n${projectContext}\n\n` : "") +
66
+ `You are a Tech Lead. Analyze these findings and break each into concrete implementation tasks.\n` +
67
+ `Findings:\n${JSON.stringify(findingsSummary)}\n\n` +
68
+ `For EACH finding, create 1-3 specific implementation tasks. Each task should be:\n` +
69
+ `- Small enough for one developer to complete\n` +
70
+ `- Specific about what files/modules to change\n` +
71
+ `- Clear about the expected outcome\n\n` +
72
+ `Return JSON array: [{title, objective (1-2 sentences), priority (CRITICAL/HIGH/MEDIUM/LOW), effort (small/medium/large), findingId}]\n` +
73
+ `ONLY valid JSON.`;
74
+ const planResult = await this.helpers.spawnClaude(planPrompt, repoPath, "claude-sonnet-4-6", undefined, { projectId, phase: "PLAN" });
75
+ const tasks = this.helpers.parseClaudeJson(planResult.stdout);
76
+ let tasksCreated = 0;
77
+ if (tasks && Array.isArray(tasks)) {
78
+ for (const t of tasks) {
79
+ try {
80
+ let validFindingId = null;
81
+ if (t.findingId) {
82
+ const exists = await this.data.findFindingById(t.findingId);
83
+ if (exists)
84
+ validFindingId = t.findingId;
85
+ }
86
+ const created = await this.data.createTask({
87
+ projectId,
88
+ findingId: validFindingId,
89
+ title: t.title,
90
+ objective: t.objective || null,
91
+ priority: this.helpers.toTaskPriority(t.priority),
92
+ status: "READY_TO_START",
93
+ effort: t.effort || null,
94
+ });
95
+ // Add tech-lead comment documenting creation context
96
+ const linkedFinding = findingsSummary.find((f) => f.id === validFindingId);
97
+ await this.data.createTaskComment({
98
+ taskId: created.id,
99
+ author: "tech-lead",
100
+ content: `Created from finding: "${linkedFinding?.title || "unknown"}". ${t.objective || ""}. Effort: ${t.effort || "unknown"}.`,
101
+ });
102
+ this.data.emit(projectId, "task_created", { id: created.id, title: created.title, priority: created.priority, status: created.status });
103
+ if (validFindingId) {
104
+ await this.data.updateFinding(validFindingId, { status: "TASKED" });
105
+ }
106
+ tasksCreated++;
107
+ }
108
+ catch (err) {
109
+ this.logger.warn(`Failed to create task: ${err}`);
110
+ }
111
+ }
112
+ }
113
+ await this.data.log("WORK_COMPLETED", "Session", sessionId, userId, {
114
+ type: "PLAN",
115
+ tasksCreated,
116
+ });
117
+ // Clear slot 3 tracking
118
+ this.helpers.slot3Info = null;
119
+ }
120
+ };
121
+ exports.PipelineTechLeadService = PipelineTechLeadService;
122
+ exports.PipelineTechLeadService = PipelineTechLeadService = PipelineTechLeadService_1 = __decorate([
123
+ (0, common_1.Injectable)(),
124
+ __param(0, (0, common_2.Optional)()),
125
+ __param(0, (0, common_2.Inject)("PIPELINE_DATA")),
126
+ __metadata("design:paramtypes", [Object, pipeline_helpers_service_1.PipelineHelpersService])
127
+ ], PipelineTechLeadService);