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,318 @@
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 PipelineHelpersService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PipelineHelpersService = exports.COMMAND_TIMEOUT_MS = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const child_process_1 = require("child_process");
19
+ const promises_1 = require("fs/promises");
20
+ const fs_1 = require("fs");
21
+ const path_1 = require("path");
22
+ const common_2 = require("@nestjs/common");
23
+ /** 20 minutes in milliseconds — hard ceiling for any single claude -p invocation. */
24
+ exports.COMMAND_TIMEOUT_MS = 20 * 60 * 1000;
25
+ let PipelineHelpersService = PipelineHelpersService_1 = class PipelineHelpersService {
26
+ data;
27
+ logger = new common_1.Logger(PipelineHelpersService_1.name);
28
+ /** Track what each worker slot is doing (slot index → task title). */
29
+ workerTasks = new Map();
30
+ /** Track slot 3 (shared by Tech Lead / CTO). */
31
+ slot3Info = null;
32
+ constructor(data) {
33
+ this.data = data;
34
+ }
35
+ /** Emit current agent slot state to all SSE listeners for a project. */
36
+ emitAgentState(projectId) {
37
+ this.data.emit(projectId, "work_state", {
38
+ plannedTasks: 0,
39
+ backlogFindings: 0,
40
+ userDirections: 0,
41
+ iteration: 0,
42
+ activeWorkers: this.workerTasks.size,
43
+ techLeadActive: this.slot3Info?.role === "Tech Lead",
44
+ ctoActive: this.slot3Info?.role === "CTO",
45
+ agents: {
46
+ worker1: this.workerTasks.has(0) ? { taskTitle: this.workerTasks.get(0), status: "building" } : null,
47
+ worker2: this.workerTasks.has(1) ? { taskTitle: this.workerTasks.get(1), status: "building" } : null,
48
+ slot3: this.slot3Info ? { role: this.slot3Info.role, taskTitle: this.slot3Info.task, status: "working" } : null,
49
+ },
50
+ });
51
+ }
52
+ // ------------------------------------------------------------------ //
53
+ // Claude spawn //
54
+ // ------------------------------------------------------------------ //
55
+ spawnClaude(prompt, cwd, model, timeoutMs, streamContext, options) {
56
+ return new Promise((resolve, reject) => {
57
+ const args = ["-p", prompt, "--verbose"];
58
+ if (model)
59
+ args.push("--model", model);
60
+ if (options?.resumeSessionId)
61
+ args.push("--resume", options.resumeSessionId);
62
+ if (options?.jsonOutput)
63
+ args.push("--output-format", "json");
64
+ const child = (0, child_process_1.spawn)("claude", args, { cwd, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
65
+ let stdout = "";
66
+ let stderr = "";
67
+ // stdout = final answer (captured for JSON parsing, NOT streamed to SSE)
68
+ child.stdout?.on("data", (chunk) => {
69
+ stdout += chunk.toString();
70
+ });
71
+ // stderr = real-time activity (tool calls, file reads, thinking) — stream to SSE
72
+ child.stderr?.on("data", (chunk) => {
73
+ stderr += chunk.toString();
74
+ if (streamContext) {
75
+ this.data.emit(streamContext.projectId, "claude_output", {
76
+ text: chunk.toString(),
77
+ phase: streamContext.phase,
78
+ });
79
+ }
80
+ });
81
+ const timeout = timeoutMs ?? exports.COMMAND_TIMEOUT_MS;
82
+ const timer = setTimeout(() => {
83
+ child.kill("SIGTERM");
84
+ reject(new Error(`claude -p timed out after ${timeout / 1000}s`));
85
+ }, timeout);
86
+ child.on("close", (code) => {
87
+ clearTimeout(timer);
88
+ // Extract session_id and result from JSON output format
89
+ let sessionId;
90
+ if (options?.jsonOutput) {
91
+ try {
92
+ const json = JSON.parse(stdout.trim());
93
+ sessionId = json.session_id;
94
+ // Replace stdout with just the result text for downstream parsing
95
+ stdout = json.result ?? stdout;
96
+ }
97
+ catch {
98
+ // JSON parse failed — leave stdout as-is
99
+ }
100
+ }
101
+ resolve({ stdout, stderr, exitCode: code ?? 1, sessionId });
102
+ });
103
+ child.on("error", (err) => {
104
+ clearTimeout(timer);
105
+ reject(err);
106
+ });
107
+ });
108
+ }
109
+ // ------------------------------------------------------------------ //
110
+ // JSON parsing //
111
+ // ------------------------------------------------------------------ //
112
+ /**
113
+ * Attempt to parse Claude's stdout as JSON.
114
+ * Without --output-format json, Claude returns plain text. The response
115
+ * might be pure JSON, JSON with surrounding prose, or just text.
116
+ */
117
+ parseClaudeJson(output) {
118
+ const trimmed = output.trim();
119
+ if (!trimmed) {
120
+ this.logger.warn("Empty Claude output — nothing to parse");
121
+ return null;
122
+ }
123
+ // First try: the entire output is valid JSON
124
+ try {
125
+ return JSON.parse(trimmed);
126
+ }
127
+ catch {
128
+ // ignore
129
+ }
130
+ // Second try: extract the first JSON array or object from the text.
131
+ const arrayStart = trimmed.indexOf("[");
132
+ const objectStart = trimmed.indexOf("{");
133
+ const starts = [];
134
+ if (arrayStart !== -1)
135
+ starts.push(arrayStart);
136
+ if (objectStart !== -1)
137
+ starts.push(objectStart);
138
+ for (const start of starts.sort((a, b) => a - b)) {
139
+ const closing = start === arrayStart ? "]" : "}";
140
+ const lastClose = trimmed.lastIndexOf(closing);
141
+ if (lastClose > start) {
142
+ try {
143
+ return JSON.parse(trimmed.slice(start, lastClose + 1));
144
+ }
145
+ catch {
146
+ // ignore — try next candidate
147
+ }
148
+ }
149
+ }
150
+ this.logger.warn("Failed to parse Claude output as JSON");
151
+ return null;
152
+ }
153
+ // ------------------------------------------------------------------ //
154
+ // Git helpers //
155
+ // ------------------------------------------------------------------ //
156
+ /** Validate that a string is a 40-character lowercase hex SHA. */
157
+ isValidSha(sha) {
158
+ return /^[0-9a-f]{40}$/.test(sha);
159
+ }
160
+ /**
161
+ * Validate that a repo path exists and is within an allowed directory prefix.
162
+ * Prevents path traversal and arbitrary directory access in shell commands.
163
+ */
164
+ validateRepoPath(repoPath) {
165
+ if (!repoPath) {
166
+ throw new Error("Repository path is required");
167
+ }
168
+ // Block path traversal
169
+ if (repoPath.includes("..")) {
170
+ throw new Error("Path traversal is not allowed in repo path");
171
+ }
172
+ const normalized = (0, path_1.normalize)((0, path_1.resolve)(repoPath));
173
+ // Must actually exist on disk
174
+ if (!(0, fs_1.existsSync)(normalized)) {
175
+ throw new Error(`Repository path does not exist: ${normalized}`);
176
+ }
177
+ }
178
+ /** Get the current git HEAD SHA for a directory. */
179
+ gitRevParse(cwd) {
180
+ this.validateRepoPath(cwd);
181
+ return (0, child_process_1.execSync)("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
182
+ }
183
+ /** Get the full diff between two commits. */
184
+ gitDiff(cwd, oldHead, newHead) {
185
+ if (!this.isValidSha(oldHead) || !this.isValidSha(newHead)) {
186
+ this.logger.warn(`Invalid SHA detected, skipping diff`);
187
+ return "";
188
+ }
189
+ this.validateRepoPath(cwd);
190
+ try {
191
+ return (0, child_process_1.execSync)(`git diff ${oldHead}..${newHead}`, {
192
+ cwd,
193
+ encoding: "utf-8",
194
+ maxBuffer: 5 * 1024 * 1024,
195
+ });
196
+ }
197
+ catch {
198
+ return "";
199
+ }
200
+ }
201
+ /** Get the diff stat summary between two commits. */
202
+ gitDiffStat(cwd, oldHead, newHead) {
203
+ if (!this.isValidSha(oldHead) || !this.isValidSha(newHead)) {
204
+ this.logger.warn(`Invalid SHA detected, skipping diff stat`);
205
+ return "";
206
+ }
207
+ this.validateRepoPath(cwd);
208
+ try {
209
+ return (0, child_process_1.execSync)(`git diff ${oldHead}..${newHead} --stat`, {
210
+ cwd,
211
+ encoding: "utf-8",
212
+ });
213
+ }
214
+ catch {
215
+ return "";
216
+ }
217
+ }
218
+ // ------------------------------------------------------------------ //
219
+ // Filesystem helpers //
220
+ // ------------------------------------------------------------------ //
221
+ /** Read a directory listing, returning [] if the directory does not exist. */
222
+ async safeReadDir(dir) {
223
+ try {
224
+ return await (0, promises_1.readdir)(dir);
225
+ }
226
+ catch {
227
+ return [];
228
+ }
229
+ }
230
+ /**
231
+ * Parse YAML-style frontmatter from a markdown file.
232
+ * Handles the block between leading `---` fences.
233
+ */
234
+ parseFrontmatter(content) {
235
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
236
+ if (!match)
237
+ return {};
238
+ const result = {};
239
+ for (const line of match[1].split(/\r?\n/)) {
240
+ const idx = line.indexOf(":");
241
+ if (idx === -1)
242
+ continue;
243
+ const key = line.slice(0, idx).trim();
244
+ const value = line.slice(idx + 1).trim();
245
+ if (key)
246
+ result[key] = value;
247
+ }
248
+ return result;
249
+ }
250
+ /** Return the markdown body (everything after the frontmatter block). */
251
+ extractBody(content) {
252
+ const stripped = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
253
+ return stripped.trim();
254
+ }
255
+ // ------------------------------------------------------------------ //
256
+ // Enum mappers (lenient — default to safe values) //
257
+ // ------------------------------------------------------------------ //
258
+ toFindingSeverity(raw) {
259
+ const upper = raw?.toUpperCase();
260
+ if (upper === "CRITICAL" || upper === "HIGH" || upper === "MEDIUM" || upper === "LOW") {
261
+ return upper;
262
+ }
263
+ return "MEDIUM";
264
+ }
265
+ toFindingCategory(raw) {
266
+ const upper = raw?.toUpperCase();
267
+ if (upper === "BUG" ||
268
+ upper === "SECURITY" ||
269
+ upper === "PERFORMANCE" ||
270
+ upper === "UX" ||
271
+ upper === "FEATURE" ||
272
+ upper === "MARKET") {
273
+ return upper;
274
+ }
275
+ return "FEATURE";
276
+ }
277
+ toFindingStatus(raw) {
278
+ const upper = raw?.toUpperCase();
279
+ if (upper === "NEW" ||
280
+ upper === "TRIAGED" ||
281
+ upper === "PLANNING" ||
282
+ upper === "TASKED" ||
283
+ upper === "DEFERRED" ||
284
+ upper === "DISMISSED") {
285
+ return upper;
286
+ }
287
+ return "NEW";
288
+ }
289
+ toTaskPriority(raw) {
290
+ const upper = raw?.toUpperCase();
291
+ if (upper === "CRITICAL" || upper === "HIGH" || upper === "MEDIUM" || upper === "LOW") {
292
+ return upper;
293
+ }
294
+ return "MEDIUM";
295
+ }
296
+ toTaskStatus(raw) {
297
+ const upper = raw?.toUpperCase();
298
+ if (upper === "PLANNED" ||
299
+ upper === "READY_TO_START" ||
300
+ upper === "IN_PROGRESS" ||
301
+ upper === "COMPLETED" ||
302
+ upper === "FAILED" ||
303
+ upper === "ARCHIVED") {
304
+ return upper;
305
+ }
306
+ // Map legacy PLANNING to READY_TO_START
307
+ if (upper === "PLANNING")
308
+ return "READY_TO_START";
309
+ return "PLANNED";
310
+ }
311
+ };
312
+ exports.PipelineHelpersService = PipelineHelpersService;
313
+ exports.PipelineHelpersService = PipelineHelpersService = PipelineHelpersService_1 = __decorate([
314
+ (0, common_1.Injectable)(),
315
+ __param(0, (0, common_2.Optional)()),
316
+ __param(0, (0, common_2.Inject)("PIPELINE_DATA")),
317
+ __metadata("design:paramtypes", [Object])
318
+ ], PipelineHelpersService);