@stilero/bankan 1.0.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,1193 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from 'node:fs';
2
+ import { rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { simpleGit } from 'simple-git';
6
+ import config, { loadSettings, getWorkspacesDir } from './config.js';
7
+ import store from './store.js';
8
+ import agentManager from './agents.js';
9
+ import bus from './events.js';
10
+
11
+ const POLL_INTERVAL = 4000;
12
+ const SIGNAL_CHECK_INTERVAL = 2500;
13
+ const PLANNER_TIMEOUT = 5 * 60 * 1000;
14
+ const IMPLEMENTOR_TIMEOUT = 60 * 60 * 1000;
15
+ const REVIEWER_TIMEOUT = 30 * 60 * 1000;
16
+ const STUCK_TIMEOUT = 10 * 60 * 1000;
17
+ const MAX_REVIEW_CYCLES = 3;
18
+
19
+ let pollTimer = null;
20
+ let signalTimer = null;
21
+
22
+ function escapePrompt(text) {
23
+ return text.replace(/'/g, "'\\''");
24
+ }
25
+
26
+ function buildCodexExecCommand(prompt, { captureLastMessage = false, sandbox = 'read-only' } = {}) {
27
+ const escapedPrompt = escapePrompt(prompt);
28
+ if (!captureLastMessage) {
29
+ return `codex exec --sandbox ${sandbox} '${escapedPrompt}'`;
30
+ }
31
+
32
+ return `tmpfile=$(mktemp); codex exec --sandbox ${sandbox} -o "$tmpfile" '${escapedPrompt}'; status=$?; printf '\\n=== CODEX_LAST_MESSAGE_FILE:%s ===\\n' "$tmpfile"; exit $status`;
33
+ }
34
+
35
+ function buildAgentCommand(cliTool, prompt, mode = 'interactive') {
36
+ if (cliTool === 'codex') {
37
+ if (mode === 'plan' || mode === 'review') {
38
+ return buildCodexExecCommand(prompt, { captureLastMessage: true, sandbox: 'read-only' });
39
+ }
40
+ if (mode === 'interactive') {
41
+ return buildCodexExecCommand(prompt, { captureLastMessage: true, sandbox: 'danger-full-access' });
42
+ }
43
+ return buildCodexExecCommand(prompt, { captureLastMessage: false, sandbox: 'read-only' });
44
+ }
45
+
46
+ if (mode === 'print') {
47
+ return `claude --print '${escapePrompt(prompt)}'`;
48
+ }
49
+
50
+ return `claude --dangerously-skip-permissions '${escapePrompt(prompt)}'`;
51
+ }
52
+
53
+ function getLastStructuredBlock(text, startMarker, endMarker) {
54
+ if (typeof text !== 'string' || !text) return null;
55
+ const endIdx = text.lastIndexOf(endMarker);
56
+ if (endIdx === -1) return null;
57
+ const startIdx = text.lastIndexOf(startMarker, endIdx);
58
+ if (startIdx === -1) return null;
59
+ return text.slice(startIdx, endIdx + endMarker.length);
60
+ }
61
+
62
+ function getCodexLastMessagePath(buffer) {
63
+ if (typeof buffer !== 'string' || !buffer) return null;
64
+ const matches = [...buffer.matchAll(/=== CODEX_LAST_MESSAGE_FILE:(.+?) ===/g)];
65
+ if (matches.length === 0) return null;
66
+ return matches[matches.length - 1][1].trim();
67
+ }
68
+
69
+ function readCapturedCodexMessage(buffer, { remove = true } = {}) {
70
+ const outputPath = getCodexLastMessagePath(buffer);
71
+ if (!outputPath || !existsSync(outputPath)) return null;
72
+
73
+ try {
74
+ return readFileSync(outputPath, 'utf-8');
75
+ } catch {
76
+ return null;
77
+ } finally {
78
+ if (remove) {
79
+ try { unlinkSync(outputPath); } catch { /* ignore */ }
80
+ }
81
+ }
82
+ }
83
+
84
+ function hasCodexStructuredOutput(buffer, endMarker) {
85
+ const captured = readCapturedCodexMessage(buffer, { remove: false });
86
+ return Boolean(captured && captured.includes(endMarker));
87
+ }
88
+
89
+ function getImplementationCompletionState(agent, taskId) {
90
+ const completionMarker = `=== IMPLEMENTATION COMPLETE ${taskId} ===`;
91
+ const buf = agent.getBufferString(100);
92
+
93
+ if (agent.cli === 'codex') {
94
+ const captured = readCapturedCodexMessage(buf, { remove: false });
95
+ if (captured) {
96
+ if (captured.includes(completionMarker)) {
97
+ return { complete: true, blockedReason: null };
98
+ }
99
+ const blockedMatch = captured.match(/=== BLOCKED: (.+?) ===/);
100
+ return { complete: false, blockedReason: blockedMatch ? blockedMatch[1] : null };
101
+ }
102
+
103
+ return { complete: false, blockedReason: null };
104
+ }
105
+
106
+ if (buf.includes(completionMarker)) {
107
+ return { complete: true, blockedReason: null };
108
+ }
109
+
110
+ const blockedMatch = buf.match(/=== BLOCKED: (.+?) ===/);
111
+ return { complete: false, blockedReason: blockedMatch ? blockedMatch[1] : null };
112
+ }
113
+
114
+ function summarizeProcessError(prefix, err) {
115
+ const raw = typeof err?.message === 'string' ? err.message : String(err || '');
116
+ const normalized = raw.replace(/\s+/g, ' ').trim();
117
+
118
+ const graphqlMatch = normalized.match(/GraphQL:\s*([^]+?)(?:\(createPullRequest\)|$)/i);
119
+ if (graphqlMatch) {
120
+ return `${prefix}: ${graphqlMatch[1].trim()}`;
121
+ }
122
+
123
+ const failedMatch = normalized.match(/failed:\s*(.+)$/i);
124
+ if (failedMatch) {
125
+ return `${prefix}: ${failedMatch[1].trim()}`;
126
+ }
127
+
128
+ const compact = normalized.slice(0, 240);
129
+ return `${prefix}: ${compact}`;
130
+ }
131
+
132
+ function extractSection(text, label, nextLabels = []) {
133
+ if (typeof text !== 'string' || !text) return '';
134
+ const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
135
+ const nextPattern = nextLabels.length > 0
136
+ ? `(?=${nextLabels.map(item => item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`
137
+ : '$';
138
+ const regex = new RegExp(`${escapedLabel}\\s*([\\s\\S]*?)${nextPattern}`, 'i');
139
+ const match = text.match(regex);
140
+ return match ? match[1].trim() : '';
141
+ }
142
+
143
+ function parseBulletList(sectionText) {
144
+ return (sectionText || '')
145
+ .split('\n')
146
+ .map(line => line.trim())
147
+ .filter(line => line.startsWith('- '))
148
+ .map(line => line.slice(2).trim())
149
+ .filter(Boolean);
150
+ }
151
+
152
+ function extractSingleLine(text, label) {
153
+ if (typeof text !== 'string' || !text) return '';
154
+ const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
155
+ const match = text.match(new RegExp(`${escapedLabel}\\s*(.+)`, 'i'));
156
+ return match ? match[1].trim() : '';
157
+ }
158
+
159
+ function getPromptBody(stage) {
160
+ const settings = loadSettings();
161
+ return settings.prompts?.[stage] || '';
162
+ }
163
+
164
+ function isStageDisabled(stage) {
165
+ const settings = loadSettings();
166
+ if (stage === 'planning') return settings.agents?.planners?.max === 0;
167
+ if (stage === 'review') return settings.agents?.reviewers?.max === 0;
168
+ return false;
169
+ }
170
+
171
+ function slugifyTitle(title) {
172
+ const slug = String(title || 'auto')
173
+ .toLowerCase()
174
+ .replace(/[^a-z0-9]+/g, '-')
175
+ .replace(/^-+|-+$/g, '')
176
+ .slice(0, 40);
177
+ return slug || 'auto';
178
+ }
179
+
180
+ function generateBranchName(task) {
181
+ return `feature/${task.id.toLowerCase()}-${slugifyTitle(task.title)}`;
182
+ }
183
+
184
+ function buildSyntheticPlan(task) {
185
+ return `=== PLAN START ===
186
+ SUMMARY: Planning skipped because planner max is set to 0. Implement the requested task directly.
187
+ BRANCH: ${generateBranchName(task)}
188
+ FILES_TO_MODIFY:
189
+ - Determine the affected files based on the task description during implementation
190
+ STEPS:
191
+ 1. Review the repository context and task details.
192
+ 2. Implement the requested changes for "${task.title}".
193
+ 3. Run the most relevant existing verification before handing off.
194
+ TESTS_NEEDED:
195
+ - Run the most relevant existing tests or checks for the modified area
196
+ RISKS:
197
+ - Planning was skipped, so implementation must validate scope and touched files carefully
198
+ === PLAN END ===`;
199
+ }
200
+
201
+ function buildPullRequestBody(task) {
202
+ const planSummary = extractSingleLine(task.plan, 'SUMMARY:');
203
+ const filesToModify = parseBulletList(
204
+ extractSection(task.plan, 'FILES_TO_MODIFY:', ['STEPS:', 'TESTS_NEEDED:', 'RISKS:'])
205
+ );
206
+ const testsNeeded = parseBulletList(
207
+ extractSection(task.plan, 'TESTS_NEEDED:', ['RISKS:', '=== PLAN END ==='])
208
+ );
209
+ const risks = parseBulletList(
210
+ extractSection(task.plan, 'RISKS:', ['=== PLAN END ==='])
211
+ );
212
+
213
+ const reviewVerdict = extractSingleLine(task.review, 'VERDICT:') || 'N/A';
214
+ const reviewSummary = extractSingleLine(task.review, 'SUMMARY:');
215
+ const criticalIssues = parseBulletList(
216
+ extractSection(task.review, 'CRITICAL_ISSUES:', ['MINOR_ISSUES:', 'SUMMARY:', '=== REVIEW END ==='])
217
+ ).filter(item => item.toLowerCase() !== 'none');
218
+ const minorIssues = parseBulletList(
219
+ extractSection(task.review, 'MINOR_ISSUES:', ['SUMMARY:', '=== REVIEW END ==='])
220
+ ).filter(item => item.toLowerCase() !== 'none');
221
+
222
+ const sections = [
223
+ `## Summary\n\n${planSummary || task.title}`,
224
+ ];
225
+
226
+ if (filesToModify.length > 0) {
227
+ sections.push(`## Key Changes\n\n${filesToModify.slice(0, 6).map(item => `- ${item}`).join('\n')}`);
228
+ }
229
+
230
+ if (testsNeeded.length > 0) {
231
+ sections.push(`## Validation\n\n${testsNeeded.map(item => `- ${item}`).join('\n')}`);
232
+ }
233
+
234
+ const reviewLines = [
235
+ `- Verdict: ${reviewVerdict}`,
236
+ ];
237
+ if (reviewSummary) reviewLines.push(`- Summary: ${reviewSummary}`);
238
+ if (criticalIssues.length > 0) reviewLines.push(`- Critical issues: ${criticalIssues.join('; ')}`);
239
+ if (minorIssues.length > 0) reviewLines.push(`- Minor issues: ${minorIssues.join('; ')}`);
240
+ sections.push(`## Review\n\n${reviewLines.join('\n')}`);
241
+
242
+ if (risks.length > 0) {
243
+ sections.push(`## Risks\n\n${risks.map(item => `- ${item}`).join('\n')}`);
244
+ }
245
+
246
+ return sections.join('\n\n');
247
+ }
248
+
249
+ function getAuthBlockedReason(buffer, cli = '') {
250
+ const text = typeof buffer === 'string' ? buffer : '';
251
+ if (!text) return null;
252
+
253
+ const authPatterns = [
254
+ /not logged in[^\n\r]*/i,
255
+ /please run\s+\/login[^\n\r]*/i,
256
+ /run\s+\/login[^\n\r]*/i,
257
+ /authentication required[^\n\r]*/i,
258
+ /login required[^\n\r]*/i,
259
+ ];
260
+
261
+ for (const pattern of authPatterns) {
262
+ const match = text.match(pattern);
263
+ if (match) {
264
+ const detail = match[0].replace(/\s+/g, ' ').trim();
265
+ const cliLabel = cli || 'agent CLI';
266
+ return `${cliLabel} authentication required: ${detail}`;
267
+ }
268
+ }
269
+
270
+ return null;
271
+ }
272
+
273
+ function buildPlannerPrompt(task) {
274
+ const promptBody = getPromptBody('planning');
275
+ let prompt = `You are a senior software architect. A task has been assigned to you.
276
+ Repository: ${task.repoPath}
277
+ Workspace: ${task.workspacePath}
278
+
279
+ TASK ID: ${task.id}
280
+ TITLE: ${task.title}
281
+ DESCRIPTION: ${task.description || 'No additional description provided.'}
282
+ PRIORITY: ${task.priority}`;
283
+
284
+ if (task.planFeedback) {
285
+ prompt += `\n\nPrevious plan was rejected. Feedback: ${task.planFeedback}\nPlease revise accordingly.`;
286
+ }
287
+
288
+ prompt += `
289
+
290
+ ${promptBody}
291
+ Output ONLY in this exact format, with no text before or after the delimiters:
292
+
293
+ === PLAN START ===
294
+ SUMMARY: (one sentence describing what will be built)
295
+ BRANCH: (${generateBranchName(task).replace(slugifyTitle(task.title), 'short-descriptive-slug')})
296
+ FILES_TO_MODIFY:
297
+ - path/to/file.ts (reason for modification)
298
+ STEPS:
299
+ 1. (detailed, actionable step)
300
+ 2. (detailed, actionable step)
301
+ TESTS_NEEDED:
302
+ - (test description, or 'none')
303
+ RISKS:
304
+ - (potential issue or edge case, or 'none')
305
+ === PLAN END ===`;
306
+
307
+ return prompt;
308
+ }
309
+
310
+ function buildImplementorPrompt(task, workspacePath) {
311
+ const repoDir = workspacePath || task.repoPath;
312
+ const promptBody = getPromptBody('implementation');
313
+ let prompt = `You are an expert software engineer implementing a feature on a real codebase.
314
+
315
+ TASK: ${task.title}
316
+ TASK ID: ${task.id}
317
+ BRANCH: ${task.branch}
318
+ REPO: ${repoDir}`;
319
+
320
+ if (task.reviewFeedback) {
321
+ prompt += `\n\nPREVIOUS REVIEW — ISSUES TO FIX:\n${task.reviewFeedback}\n`;
322
+ }
323
+
324
+ prompt += `
325
+
326
+ IMPLEMENTATION PLAN:
327
+ ${task.plan}
328
+
329
+ Instructions:
330
+ - You are already on branch ${task.branch} in ${repoDir}
331
+ ${promptBody}
332
+ - When fully complete, output this exact string on its own line:
333
+ === IMPLEMENTATION COMPLETE ${task.id} ===
334
+ - If you encounter a blocker you cannot resolve, output:
335
+ === BLOCKED: {reason} ===
336
+
337
+ Begin implementation now.`;
338
+
339
+ return prompt;
340
+ }
341
+
342
+ function buildReviewerPrompt(task) {
343
+ const promptBody = getPromptBody('review').replaceAll('{branch}', task.branch || 'main');
344
+ return `You are a senior code reviewer. A feature branch is ready for review.
345
+
346
+ TASK: ${task.title}
347
+ BRANCH: ${task.branch}
348
+ REPO: ${task.workspacePath || task.repoPath}
349
+
350
+ ORIGINAL PLAN:
351
+ ${task.plan}
352
+
353
+ Instructions:
354
+ ${promptBody}
355
+
356
+ Output ONLY in this exact format:
357
+
358
+ === REVIEW START ===
359
+ VERDICT: PASS
360
+ CRITICAL_ISSUES:
361
+ - none
362
+ MINOR_ISSUES:
363
+ - (issue description, or 'none')
364
+ SUMMARY: (2-3 sentences summarising the review)
365
+ === REVIEW END ===`;
366
+ }
367
+
368
+ // --- Workspace Helpers ---
369
+
370
+ async function setupWorkspace(task) {
371
+ const settings = loadSettings();
372
+ const workspaceRoot = join(getWorkspacesDir(settings), task.id);
373
+ const existingWorkspace = task.workspacePath;
374
+
375
+ if (existingWorkspace && existsSync(existingWorkspace)) {
376
+ return existingWorkspace;
377
+ }
378
+
379
+ if (existsSync(workspaceRoot)) {
380
+ try {
381
+ const entries = readdirSync(workspaceRoot);
382
+ if (entries.length === 0) {
383
+ await rm(workspaceRoot, { recursive: true, force: true });
384
+ } else if (existsSync(join(workspaceRoot, '.git'))) {
385
+ try {
386
+ const wsGit = simpleGit(workspaceRoot);
387
+ const remotes = await wsGit.getRemotes(true);
388
+ const origin = remotes.find(remote => remote.name === 'origin');
389
+ const fetchUrl = origin?.refs?.fetch || '';
390
+ const pushUrl = origin?.refs?.push || '';
391
+
392
+ if ([fetchUrl, pushUrl].includes(task.repoPath)) {
393
+ await wsGit.addConfig('user.email', 'ai-factory@local');
394
+ await wsGit.addConfig('user.name', 'AI Factory');
395
+ try { await wsGit.fetch('origin'); } catch { /* ignore */ }
396
+ return workspaceRoot;
397
+ }
398
+ } catch {
399
+ // Fall through to remove and recreate the workspace.
400
+ }
401
+
402
+ await rm(workspaceRoot, { recursive: true, force: true });
403
+ } else {
404
+ await rm(workspaceRoot, { recursive: true, force: true });
405
+ }
406
+ } catch {
407
+ await rm(workspaceRoot, { recursive: true, force: true });
408
+ }
409
+ }
410
+
411
+ mkdirSync(workspaceRoot, { recursive: true });
412
+
413
+ await simpleGit().clone(task.repoPath, workspaceRoot);
414
+
415
+ const wsGit = simpleGit(workspaceRoot);
416
+ await wsGit.addConfig('user.email', 'ai-factory@local');
417
+ await wsGit.addConfig('user.name', 'AI Factory');
418
+ await wsGit.pull('origin', 'main');
419
+
420
+ return workspaceRoot;
421
+ }
422
+
423
+ async function prepareWorkspaceBranch(task) {
424
+ const workspacePath = await setupWorkspace(task);
425
+ const git = simpleGit(workspacePath);
426
+ const branches = await git.branchLocal();
427
+
428
+ if (!branches.current) {
429
+ await git.checkout('main');
430
+ }
431
+
432
+ if (!branches.all.includes(task.branch)) {
433
+ await git.checkout('main');
434
+ await git.pull('origin', 'main');
435
+ try { await git.push('origin', `:${task.branch}`); } catch { /* ignore */ }
436
+ await git.checkoutLocalBranch(task.branch);
437
+ } else {
438
+ await git.checkout(task.branch);
439
+ }
440
+
441
+ return workspacePath;
442
+ }
443
+
444
+ async function cleanupWorkspace(task) {
445
+ if (task.workspacePath && existsSync(task.workspacePath)) {
446
+ await rm(task.workspacePath, { recursive: true, force: true });
447
+ store.updateTask(task.id, { workspacePath: null });
448
+ }
449
+ }
450
+
451
+ // --- Stage Transitions ---
452
+
453
+ async function startPlanning(task) {
454
+ if (isStageDisabled('planning')) {
455
+ const planText = buildSyntheticPlan(task);
456
+ const branch = extractSingleLine(planText, 'BRANCH:') || generateBranchName(task);
457
+ store.savePlan(task.id, planText);
458
+ store.updateTask(task.id, {
459
+ status: 'queued',
460
+ plan: planText,
461
+ branch,
462
+ review: null,
463
+ reviewFeedback: null,
464
+ reviewCycleCount: 0,
465
+ blockedReason: null,
466
+ assignedTo: null,
467
+ });
468
+ return true;
469
+ }
470
+
471
+ const planner = agentManager.getAvailablePlanner();
472
+ if (!planner) return false;
473
+
474
+ store.updateTask(task.id, { status: 'workspace_setup', assignedTo: planner.id, blockedReason: null });
475
+ planner.currentTask = task.id;
476
+ planner.taskLabel = `Preparing: ${task.title}`;
477
+ planner.status = 'active';
478
+ bus.emit('agent:updated', planner.getStatus());
479
+
480
+ let workspacePath;
481
+ try {
482
+ workspacePath = await setupWorkspace(task);
483
+ } catch (err) {
484
+ store.updateTask(task.id, {
485
+ status: 'blocked',
486
+ blockedReason: `Workspace setup failed: ${err.message}`,
487
+ assignedTo: null,
488
+ });
489
+ planner.currentTask = null;
490
+ planner.taskLabel = '';
491
+ planner.status = 'idle';
492
+ bus.emit('agent:updated', planner.getStatus());
493
+ bus.emit('task:blocked', { taskId: task.id, reason: 'Workspace setup failed' });
494
+ return false;
495
+ }
496
+
497
+ store.updateTask(task.id, {
498
+ status: 'planning',
499
+ assignedTo: planner.id,
500
+ workspacePath,
501
+ blockedReason: null,
502
+ });
503
+ planner.taskLabel = `Planning: ${task.title}`;
504
+
505
+ const prompt = buildPlannerPrompt({ ...task, workspacePath });
506
+ const cmd = buildAgentCommand(planner.cli, prompt, 'plan');
507
+ const plannerCwd = workspacePath;
508
+ const ok = planner.spawn(plannerCwd, cmd);
509
+ if (!ok) {
510
+ store.updateTask(task.id, {
511
+ status: 'blocked',
512
+ blockedReason: `Invalid planner working directory: ${plannerCwd}`,
513
+ assignedTo: null,
514
+ });
515
+ planner.currentTask = null;
516
+ planner.taskLabel = '';
517
+ planner.status = 'idle';
518
+ bus.emit('agent:updated', planner.getStatus());
519
+ return false;
520
+ }
521
+ bus.emit('agent:updated', planner.getStatus());
522
+ return true;
523
+ }
524
+
525
+ function onPlanComplete(agentId, taskId) {
526
+ const planner = agentManager.get(agentId);
527
+ if (!planner) return;
528
+ const bufStr = planner.getBufferString(100);
529
+ const captured = planner.cli === 'codex' ? readCapturedCodexMessage(bufStr) : null;
530
+ const sourceText = captured || bufStr;
531
+
532
+ // Extract plan text
533
+ const planText = getLastStructuredBlock(sourceText, '=== PLAN START ===', '=== PLAN END ===');
534
+ if (!planText) return;
535
+
536
+ // Parse branch name
537
+ const branchMatch = planText.match(/BRANCH:\s*(.+)/);
538
+ const branch = branchMatch ? branchMatch[1].trim() : generateBranchName(store.getTask(taskId) || { id: taskId, title: 'auto' });
539
+
540
+ // Save plan
541
+ store.savePlan(taskId, planText);
542
+ store.updateTask(taskId, {
543
+ status: 'awaiting_approval',
544
+ plan: planText,
545
+ branch,
546
+ review: null,
547
+ reviewFeedback: null,
548
+ reviewCycleCount: 0,
549
+ blockedReason: null,
550
+ assignedTo: null,
551
+ });
552
+
553
+ planner.kill();
554
+ if (planner.draining) agentManager.removeAgent(agentId);
555
+ bus.emit('plan:ready', { taskId, plan: planText });
556
+ }
557
+
558
+ function approvePlan(taskId) {
559
+ const task = store.getTask(taskId);
560
+ if (!task || task.status !== 'awaiting_approval') return;
561
+ startImplementation(task);
562
+ }
563
+
564
+ function rejectPlan(taskId, feedback) {
565
+ const task = store.getTask(taskId);
566
+ if (!task || task.status !== 'awaiting_approval') return;
567
+
568
+ store.updateTask(taskId, {
569
+ status: 'backlog',
570
+ planFeedback: feedback,
571
+ blockedReason: null,
572
+ assignedTo: null,
573
+ });
574
+ }
575
+
576
+ async function startImplementation(task) {
577
+ const agent = agentManager.getAvailableImplementor();
578
+ if (!agent) {
579
+ store.updateTask(task.id, { status: 'queued' });
580
+ return;
581
+ }
582
+
583
+ store.updateTask(task.id, {
584
+ status: 'workspace_setup',
585
+ assignedTo: agent.id,
586
+ blockedReason: null,
587
+ startedAt: task.startedAt || new Date().toISOString(),
588
+ });
589
+ agent.currentTask = task.id;
590
+ agent.taskLabel = `Setting up: ${task.title}`;
591
+ agent.status = 'active';
592
+ bus.emit('agent:updated', agent.getStatus());
593
+
594
+ let workspacePath;
595
+ try {
596
+ workspacePath = await prepareWorkspaceBranch(task);
597
+ } catch (err) {
598
+ console.error(`Workspace setup failed for ${task.id}:`, err.message);
599
+ store.updateTask(task.id, {
600
+ status: 'blocked',
601
+ blockedReason: `Workspace setup failed: ${err.message}`,
602
+ assignedTo: null,
603
+ });
604
+ agent.currentTask = null;
605
+ agent.taskLabel = '';
606
+ agent.status = 'idle';
607
+ bus.emit('agent:updated', agent.getStatus());
608
+ return;
609
+ }
610
+
611
+ store.updateTask(task.id, { status: 'implementing', workspacePath, blockedReason: null });
612
+
613
+ const cliTool = agent.cli;
614
+ const prompt = buildImplementorPrompt(task, workspacePath);
615
+ const cmd = buildAgentCommand(cliTool, prompt, 'interactive');
616
+
617
+ const ok = agent.spawn(workspacePath, cmd);
618
+ if (!ok) {
619
+ store.updateTask(task.id, {
620
+ status: 'blocked',
621
+ blockedReason: `Invalid workspace path: ${workspacePath}`,
622
+ assignedTo: null,
623
+ });
624
+ agent.currentTask = null;
625
+ agent.taskLabel = '';
626
+ agent.status = 'idle';
627
+ bus.emit('agent:updated', agent.getStatus());
628
+ return;
629
+ }
630
+ bus.emit('agent:updated', agent.getStatus());
631
+ }
632
+
633
+ async function onImplementationComplete(agentId) {
634
+ const agent = agentManager.get(agentId);
635
+ if (!agent) return;
636
+ const taskId = agent.currentTask;
637
+ if (!taskId) return;
638
+
639
+ const task = store.getTask(taskId);
640
+
641
+ // Push branch from workspace
642
+ if (task?.workspacePath) {
643
+ try {
644
+ const git = simpleGit(task.workspacePath);
645
+ await git.push('origin', task.branch);
646
+ } catch (err) {
647
+ console.error(`Git push failed:`, err.message);
648
+ store.updateTask(taskId, {
649
+ status: 'blocked',
650
+ blockedReason: `Branch push failed: ${err.message}`,
651
+ assignedTo: null,
652
+ });
653
+ agent.kill();
654
+ if (agent.draining) agentManager.removeAgent(agentId);
655
+ return;
656
+ }
657
+ }
658
+
659
+ store.updateTask(taskId, { status: 'review', assignedTo: null, blockedReason: null });
660
+ agent.kill();
661
+ if (agent.draining) agentManager.removeAgent(agentId);
662
+
663
+ const taskForReview = store.getTask(taskId);
664
+ startReview(taskForReview);
665
+ }
666
+
667
+ function startReview(task) {
668
+ if (isStageDisabled('review')) {
669
+ store.updateTask(task.id, {
670
+ status: 'review',
671
+ assignedTo: 'orch',
672
+ blockedReason: null,
673
+ review: `=== REVIEW START ===
674
+ VERDICT: PASS
675
+ CRITICAL_ISSUES:
676
+ - none
677
+ MINOR_ISSUES:
678
+ - none
679
+ SUMMARY: Review skipped because reviewer max is set to 0.
680
+ === REVIEW END ===`,
681
+ });
682
+ bus.emit('review:passed', { taskId: task.id });
683
+ createPR(task.id);
684
+ return;
685
+ }
686
+
687
+ const reviewer = agentManager.getAvailableReviewer();
688
+ if (!reviewer) return;
689
+
690
+ store.updateTask(task.id, { status: 'review', assignedTo: reviewer.id, blockedReason: null });
691
+ reviewer.currentTask = task.id;
692
+ reviewer.taskLabel = `Reviewing: ${task.title}`;
693
+ reviewer.status = 'active';
694
+
695
+ const prompt = buildReviewerPrompt(task);
696
+ const cmd = buildAgentCommand(reviewer.cli, prompt, 'review');
697
+ const ok = reviewer.spawn(task.workspacePath, cmd);
698
+ if (!ok) {
699
+ store.updateTask(task.id, {
700
+ status: 'blocked',
701
+ blockedReason: `Invalid workspace path for review: ${task.workspacePath}`,
702
+ assignedTo: null,
703
+ });
704
+ reviewer.currentTask = null;
705
+ reviewer.taskLabel = '';
706
+ reviewer.status = 'idle';
707
+ bus.emit('agent:updated', reviewer.getStatus());
708
+ return;
709
+ }
710
+ bus.emit('agent:updated', reviewer.getStatus());
711
+ }
712
+
713
+ async function onReviewComplete(agentId, taskId) {
714
+ const reviewer = agentManager.get(agentId);
715
+ if (!reviewer) return;
716
+ const bufStr = reviewer.getBufferString(100);
717
+
718
+ const captured = reviewer.cli === 'codex' ? readCapturedCodexMessage(bufStr) : null;
719
+ const sourceText = captured || bufStr;
720
+ const reviewText = getLastStructuredBlock(sourceText, '=== REVIEW START ===', '=== REVIEW END ===');
721
+ if (!reviewText) return;
722
+ const verdictMatch = reviewText.match(/VERDICT:\s*(PASS|FAIL)/i);
723
+ const verdict = verdictMatch ? verdictMatch[1].toUpperCase() : 'FAIL';
724
+
725
+ store.updateTask(taskId, { review: reviewText });
726
+ reviewer.kill();
727
+ if (reviewer.draining) agentManager.removeAgent(agentId);
728
+
729
+ if (verdict === 'PASS') {
730
+ bus.emit('review:passed', { taskId });
731
+ await createPR(taskId);
732
+ } else {
733
+ // Extract critical issues
734
+ const issuesMatch = reviewText.match(/CRITICAL_ISSUES:\s*([\s\S]*?)(?=MINOR_ISSUES:|SUMMARY:|=== REVIEW END ===)/i);
735
+ const criticalIssues = issuesMatch ? issuesMatch[1].trim() : 'Critical issues found';
736
+
737
+ const task = store.getTask(taskId);
738
+ const nextReviewCycleCount = (task?.reviewCycleCount || 0) + 1;
739
+
740
+ if (nextReviewCycleCount >= MAX_REVIEW_CYCLES) {
741
+ store.updateTask(taskId, {
742
+ status: 'blocked',
743
+ reviewFeedback: criticalIssues,
744
+ reviewCycleCount: nextReviewCycleCount,
745
+ blockedReason: `Reached maximum review cycles (${MAX_REVIEW_CYCLES}). Human input required.`,
746
+ assignedTo: null,
747
+ });
748
+ bus.emit('task:blocked', { taskId, reason: 'Reached maximum review cycles' });
749
+ return;
750
+ }
751
+
752
+ store.updateTask(taskId, {
753
+ status: 'queued',
754
+ reviewFeedback: criticalIssues,
755
+ reviewCycleCount: nextReviewCycleCount,
756
+ blockedReason: null,
757
+ assignedTo: null,
758
+ });
759
+ bus.emit('review:failed', { taskId, issues: criticalIssues });
760
+ }
761
+ }
762
+
763
+ async function createPR(taskId) {
764
+ const task = store.getTask(taskId);
765
+ try {
766
+ if (!task?.workspacePath || !existsSync(task.workspacePath)) {
767
+ throw new Error('Workspace is missing before PR creation');
768
+ }
769
+
770
+ const git = simpleGit(task.workspacePath);
771
+ await git.fetch('origin', 'main');
772
+ await git.checkout(task.branch);
773
+
774
+ try {
775
+ await git.rebase(['origin/main']);
776
+ } catch (err) {
777
+ try { await git.raw(['rebase', '--abort']); } catch { /* ignore */ }
778
+ throw new Error(`Rebase against origin/main failed: ${err.message}`);
779
+ }
780
+
781
+ await git.raw(['push', '--force-with-lease', 'origin', task.branch]);
782
+ const prBody = buildPullRequestBody(task);
783
+ const prUrl = execFileSync('gh', [
784
+ 'pr', 'create',
785
+ '--title', `[${task.id}] ${task.title}`,
786
+ '--body', prBody,
787
+ '--head', task.branch,
788
+ '--base', 'main',
789
+ ], { cwd: task.workspacePath, encoding: 'utf-8' }).trim();
790
+ store.updateTask(taskId, {
791
+ prUrl,
792
+ assignedTo: null,
793
+ completedAt: new Date().toISOString(),
794
+ });
795
+ bus.emit('pr:created', { taskId, prUrl });
796
+
797
+ await cleanupWorkspace(store.getTask(taskId));
798
+ store.updateTask(taskId, { status: 'done', assignedTo: null });
799
+ } catch (err) {
800
+ console.error(`PR creation error:`, err.message);
801
+ store.updateTask(taskId, {
802
+ status: 'blocked',
803
+ blockedReason: summarizeProcessError('PR finalization failed', err),
804
+ assignedTo: null,
805
+ });
806
+ bus.emit('task:blocked', { taskId, reason: 'PR finalization failed' });
807
+ }
808
+ }
809
+
810
+ async function abortTask(taskId) {
811
+ const task = store.getTask(taskId);
812
+ if (!task || task.status === 'done') return;
813
+
814
+ if (task.assignedTo) {
815
+ const agent = agentManager.get(task.assignedTo);
816
+ if (agent) agent.kill();
817
+ }
818
+
819
+ await cleanupWorkspace(task);
820
+
821
+ store.updateTask(taskId, {
822
+ status: 'aborted',
823
+ assignedTo: null,
824
+ workspacePath: null,
825
+ blockedReason: null,
826
+ reviewFeedback: null,
827
+ previousStatus: null,
828
+ reviewCycleCount: 0,
829
+ });
830
+
831
+ bus.emit('task:aborted', { taskId });
832
+ }
833
+
834
+ async function resetTask(taskId) {
835
+ const task = store.getTask(taskId);
836
+ if (!task || task.status === 'done') return;
837
+
838
+ if (task.assignedTo) {
839
+ const agent = agentManager.get(task.assignedTo);
840
+ if (agent) agent.kill();
841
+ }
842
+
843
+ await cleanupWorkspace(task);
844
+ store.removePlan(taskId);
845
+
846
+ store.updateTask(taskId, {
847
+ status: 'backlog',
848
+ assignedTo: null,
849
+ workspacePath: null,
850
+ branch: null,
851
+ plan: null,
852
+ review: null,
853
+ prUrl: null,
854
+ prNumber: null,
855
+ blockedReason: null,
856
+ reviewFeedback: null,
857
+ planFeedback: null,
858
+ previousStatus: null,
859
+ reviewCycleCount: 0,
860
+ progress: 0,
861
+ totalTokens: 0,
862
+ startedAt: null,
863
+ completedAt: null,
864
+ });
865
+ store.appendLog(taskId, 'Task reset to backlog and workspace deleted');
866
+
867
+ bus.emit('task:reset', { taskId });
868
+ }
869
+
870
+ async function deleteTask(taskId) {
871
+ const task = store.getTask(taskId);
872
+ if (!task || task.status !== 'done') return false;
873
+
874
+ if (task.workspacePath) {
875
+ await cleanupWorkspace(task);
876
+ }
877
+
878
+ store.removePlan(taskId);
879
+ store.deleteTask(taskId);
880
+ return true;
881
+ }
882
+
883
+ // --- Signal Detection ---
884
+
885
+ function checkSignals() {
886
+ // Check planners
887
+ for (const agent of agentManager.getAgentsByRole('plan')) {
888
+ if (agent.status === 'active' && agent.currentTask) {
889
+ const buf = agent.getBufferString(50);
890
+ const planReady = agent.cli === 'codex'
891
+ ? hasCodexStructuredOutput(buf, '=== PLAN END ===')
892
+ : buf.includes('=== PLAN END ===');
893
+ if (planReady) {
894
+ onPlanComplete(agent.id, agent.currentTask);
895
+ } else {
896
+ // Live plan streaming
897
+ if (!buf.includes('=== PLAN END ===') && buf.includes('=== PLAN START ===')) {
898
+ const partial = buf.slice(buf.indexOf('=== PLAN START ==='));
899
+ bus.emit('plan:partial', { taskId: agent.currentTask, plan: partial });
900
+ }
901
+ if (agent.startedAt && Date.now() - agent.startedAt > PLANNER_TIMEOUT) {
902
+ markBlocked(agent, 'Planner timed out');
903
+ }
904
+ }
905
+ }
906
+ }
907
+
908
+ // Check implementors
909
+ for (const agent of agentManager.getAgentsByRole('imp')) {
910
+ if (agent.status === 'active' && agent.currentTask) {
911
+ const buf = agent.getBufferString(50);
912
+ const implementationState = getImplementationCompletionState(agent, agent.currentTask);
913
+ if (implementationState.complete) {
914
+ onImplementationComplete(agent.id);
915
+ } else {
916
+ const trustMatch = buf.match(/trust the files|Do you trust|allow.*to run in this/i);
917
+ if (trustMatch && !implementationState.complete) {
918
+ store.updateTask(agent.currentTask, {
919
+ status: 'blocked',
920
+ blockedReason: 'Agent is awaiting user input — open the terminal and respond to the prompt',
921
+ assignedTo: agent.id,
922
+ });
923
+ agent.status = 'blocked';
924
+ bus.emit('task:blocked', { taskId: agent.currentTask, reason: 'Awaiting user input' });
925
+ bus.emit('agent:updated', agent.getStatus());
926
+ } else {
927
+ if (implementationState.blockedReason) {
928
+ const reason = implementationState.blockedReason;
929
+ store.updateTask(agent.currentTask, {
930
+ status: 'blocked',
931
+ blockedReason: reason,
932
+ assignedTo: null,
933
+ });
934
+ agent.kill();
935
+ if (agent.draining) agentManager.removeAgent(agent.id);
936
+ else {
937
+ agent.status = 'blocked';
938
+ bus.emit('task:blocked', { taskId: agent.currentTask, reason });
939
+ bus.emit('agent:updated', agent.getStatus());
940
+ }
941
+ } else if (agent.startedAt && Date.now() - agent.startedAt > IMPLEMENTOR_TIMEOUT) {
942
+ markBlocked(agent, 'Implementor timed out');
943
+ }
944
+ }
945
+ }
946
+ }
947
+ }
948
+
949
+ // Check reviewers
950
+ for (const agent of agentManager.getAgentsByRole('rev')) {
951
+ if (agent.status === 'active' && agent.currentTask) {
952
+ const buf = agent.getBufferString(50);
953
+ const reviewReady = agent.cli === 'codex'
954
+ ? hasCodexStructuredOutput(buf, '=== REVIEW END ===')
955
+ : buf.includes('=== REVIEW END ===');
956
+ if (reviewReady) {
957
+ onReviewComplete(agent.id, agent.currentTask);
958
+ } else if (agent.startedAt && Date.now() - agent.startedAt > REVIEWER_TIMEOUT) {
959
+ markBlocked(agent, 'Reviewer timed out');
960
+ }
961
+ }
962
+ }
963
+ }
964
+
965
+ function markBlocked(agent, reason) {
966
+ if (agent.currentTask) {
967
+ store.updateTask(agent.currentTask, {
968
+ status: 'blocked',
969
+ blockedReason: reason,
970
+ assignedTo: null,
971
+ });
972
+ bus.emit('task:blocked', { taskId: agent.currentTask, reason });
973
+ }
974
+ agent.kill();
975
+ if (agent.draining) {
976
+ agentManager.removeAgent(agent.id);
977
+ } else {
978
+ agent.status = 'blocked';
979
+ bus.emit('agent:updated', agent.getStatus());
980
+ }
981
+ }
982
+
983
+ // --- Poll Loop ---
984
+
985
+ function pollLoop() {
986
+ const tasks = store.getAllTasks();
987
+
988
+ // Assign backlog → available planners (loop to fill multiple planners)
989
+ const backlogTasks = tasks
990
+ .filter(t => t.status === 'backlog')
991
+ .sort((a, b) => {
992
+ const prio = { critical: 0, high: 1, medium: 2, low: 3 };
993
+ return (prio[a.priority] ?? 2) - (prio[b.priority] ?? 2);
994
+ });
995
+ for (const backlogTask of backlogTasks) {
996
+ if (isStageDisabled('planning')) {
997
+ startPlanning(backlogTask);
998
+ continue;
999
+ }
1000
+ if (!agentManager.getAvailablePlanner()) {
1001
+ // Try to scale up if there's demand
1002
+ agentManager.scaleUp('planners');
1003
+ if (!agentManager.getAvailablePlanner()) break;
1004
+ }
1005
+ startPlanning(backlogTask);
1006
+ }
1007
+
1008
+ // Assign queued → implementor
1009
+ const queuedTasks = tasks.filter(t => t.status === 'queued');
1010
+ for (const task of queuedTasks) {
1011
+ if (!agentManager.getAvailableImplementor()) {
1012
+ agentManager.scaleUp('implementors');
1013
+ }
1014
+ const imp = agentManager.getAvailableImplementor();
1015
+ if (imp) {
1016
+ startImplementation(task);
1017
+ } else {
1018
+ break;
1019
+ }
1020
+ }
1021
+
1022
+ // Assign review tasks with no assignee → available reviewers
1023
+ const reviewTasks = tasks.filter(t => t.status === 'review' && !t.assignedTo);
1024
+ for (const task of reviewTasks) {
1025
+ if (!agentManager.getAvailableReviewer()) {
1026
+ agentManager.scaleUp('reviewers');
1027
+ if (!agentManager.getAvailableReviewer()) break;
1028
+ }
1029
+ startReview(task);
1030
+ }
1031
+
1032
+ // Detect orphaned tasks: agents that are idle with no process but still have currentTask
1033
+ for (const [, agent] of agentManager.agents) {
1034
+ if (agent.id === 'orch') continue;
1035
+ if (agent.status === 'idle' && !agent.process && agent.currentTask) {
1036
+ const taskId = agent.currentTask;
1037
+ agent.currentTask = null;
1038
+ agent.taskLabel = '';
1039
+ bus.emit('agent:updated', agent.getStatus());
1040
+ const task = store.getTask(taskId);
1041
+ if (task && !['blocked', 'done', 'aborted', 'backlog', 'paused', 'workspace_setup'].includes(task.status)) {
1042
+ const buf = agent.getBufferString(100);
1043
+ const isPlanner = agent.id.startsWith('plan-');
1044
+ const isImplementor = agent.id.startsWith('imp-');
1045
+ const isReviewer = agent.id.startsWith('rev-');
1046
+
1047
+ if (isPlanner) {
1048
+ const planReady = agent.cli === 'codex'
1049
+ ? hasCodexStructuredOutput(buf, '=== PLAN END ===')
1050
+ : buf.includes('=== PLAN END ===');
1051
+ if (planReady) {
1052
+ onPlanComplete(agent.id, taskId);
1053
+ } else {
1054
+ store.updateTask(taskId, {
1055
+ status: 'blocked',
1056
+ blockedReason: 'Agent process exited unexpectedly',
1057
+ assignedTo: null,
1058
+ });
1059
+ }
1060
+ } else if (isImplementor) {
1061
+ const implementationState = getImplementationCompletionState(agent, taskId);
1062
+ if (implementationState.complete) {
1063
+ onImplementationComplete(agent.id);
1064
+ } else if (implementationState.blockedReason) {
1065
+ store.updateTask(taskId, {
1066
+ status: 'blocked',
1067
+ blockedReason: implementationState.blockedReason,
1068
+ assignedTo: null,
1069
+ });
1070
+ } else {
1071
+ store.updateTask(taskId, {
1072
+ status: 'blocked',
1073
+ blockedReason: 'Agent process exited unexpectedly',
1074
+ assignedTo: null,
1075
+ });
1076
+ }
1077
+ } else if (isReviewer) {
1078
+ const reviewReady = agent.cli === 'codex'
1079
+ ? hasCodexStructuredOutput(buf, '=== REVIEW END ===')
1080
+ : buf.includes('=== REVIEW END ===');
1081
+ if (reviewReady) {
1082
+ onReviewComplete(agent.id, taskId);
1083
+ } else {
1084
+ store.updateTask(taskId, {
1085
+ status: 'blocked',
1086
+ blockedReason: 'Agent process exited unexpectedly',
1087
+ assignedTo: null,
1088
+ });
1089
+ }
1090
+ } else {
1091
+ onPlanComplete(agent.id, taskId);
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ // Check stuck agents
1098
+ for (const [, agent] of agentManager.agents) {
1099
+ if (agent.id === 'orch') continue;
1100
+ if (agent.status === 'active' && agent.lastOutputAt) {
1101
+ if (Date.now() - agent.lastOutputAt > STUCK_TIMEOUT) {
1102
+ markBlocked(agent, 'No output for 10 minutes');
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ // Broadcast agent status
1108
+ bus.emit('agents:updated', agentManager.getAllStatus());
1109
+ }
1110
+
1111
+ // --- Event Handlers ---
1112
+
1113
+ bus.on('plan:approved', (taskId) => approvePlan(taskId));
1114
+ bus.on('plan:rejected', ({ taskId, feedback }) => rejectPlan(taskId, feedback));
1115
+
1116
+ bus.on('agent:unexpected-exit', ({ agentId, taskId }) => {
1117
+ const agent = agentManager.get(agentId);
1118
+ let authBlockedReason = null;
1119
+ if (agent) {
1120
+ const buf = agent.getBufferString(100);
1121
+ authBlockedReason = getAuthBlockedReason(buf, agent.cli);
1122
+ const isPlanner = agentId.startsWith('plan-');
1123
+ const isImplementor = agentId.startsWith('imp-');
1124
+ const isReviewer = agentId.startsWith('rev-');
1125
+
1126
+ if (isPlanner) {
1127
+ const planReady = agent.cli === 'codex'
1128
+ ? hasCodexStructuredOutput(buf, '=== PLAN END ===')
1129
+ : buf.includes('=== PLAN END ===');
1130
+ if (planReady) {
1131
+ onPlanComplete(agentId, taskId);
1132
+ return;
1133
+ }
1134
+ } else if (isImplementor) {
1135
+ const implementationState = getImplementationCompletionState(agent, taskId);
1136
+ if (implementationState.complete) {
1137
+ onImplementationComplete(agentId);
1138
+ return;
1139
+ }
1140
+ if (implementationState.blockedReason) {
1141
+ authBlockedReason = implementationState.blockedReason;
1142
+ }
1143
+ } else if (isReviewer) {
1144
+ const reviewReady = agent.cli === 'codex'
1145
+ ? hasCodexStructuredOutput(buf, '=== REVIEW END ===')
1146
+ : buf.includes('=== REVIEW END ===');
1147
+ if (reviewReady) {
1148
+ onReviewComplete(agentId, taskId);
1149
+ return;
1150
+ }
1151
+ }
1152
+ console.error(`[unexpected-exit] agent=${agentId} task=${taskId} last output:\n${buf.slice(-500)}`);
1153
+ agent.currentTask = null;
1154
+ agent.taskLabel = '';
1155
+ agent.status = 'idle';
1156
+ bus.emit('agent:updated', agent.getStatus());
1157
+ }
1158
+ const task = store.getTask(taskId);
1159
+ if (task && !['blocked', 'done', 'aborted', 'backlog', 'paused'].includes(task.status)) {
1160
+ store.updateTask(taskId, {
1161
+ status: 'blocked',
1162
+ blockedReason: authBlockedReason || 'Agent process exited unexpectedly',
1163
+ assignedTo: null,
1164
+ });
1165
+ }
1166
+ });
1167
+
1168
+ bus.on('settings:changed', (settings) => {
1169
+ agentManager.reconfigure(settings);
1170
+ bus.emit('agents:updated', agentManager.getAllStatus());
1171
+ bus.emit('repos:updated', settings.repos || []);
1172
+ });
1173
+
1174
+ // --- Public API ---
1175
+
1176
+ const orchestrator = {
1177
+ start() {
1178
+ console.log('Orchestrator started');
1179
+ pollTimer = setInterval(pollLoop, POLL_INTERVAL);
1180
+ signalTimer = setInterval(checkSignals, SIGNAL_CHECK_INTERVAL);
1181
+ // Run once immediately
1182
+ pollLoop();
1183
+ },
1184
+ stop() {
1185
+ if (pollTimer) clearInterval(pollTimer);
1186
+ if (signalTimer) clearInterval(signalTimer);
1187
+ },
1188
+ abortTask,
1189
+ resetTask,
1190
+ deleteTask,
1191
+ };
1192
+
1193
+ export default orchestrator;