prjct-cli 0.35.0 → 0.35.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.2] - 2026-01-17
4
+
5
+ ### Fix: CLI Workflow Now Uses CommandExecutor
6
+
7
+ The `workflow.ts` was calling `templateExecutor` directly, bypassing the orchestrator.
8
+ Now properly routes through `commandExecutor.execute()` which triggers:
9
+ - Domain detection
10
+ - Agent loading
11
+ - Skill loading
12
+ - Task fragmentation
13
+
14
+ **Files Changed:**
15
+ - `core/commands/workflow.ts` - Uses `commandExecutor.execute()` instead of `templateExecutor`
16
+
17
+ ## [0.35.1] - 2026-01-17
18
+
19
+ ### Fix: Orchestrator Now Actually Executes
20
+
21
+ Critical fix - the orchestrator was only generating PATHS to templates, not executing them.
22
+
23
+ #### What Was Broken
24
+ - `p. task` never ran domain detection
25
+ - Agents from `{globalPath}/agents/` were never loaded
26
+ - Tasks were never fragmented into subtasks
27
+ - No context was injected into prompts
28
+
29
+ #### What's Fixed
30
+ - **Created `orchestrator-executor.ts`** - Executes orchestration in TypeScript
31
+ - **Domain Detection** - Analyzes task keywords to detect database, backend, frontend, etc.
32
+ - **Agent Loading** - Loads agent content from `{globalPath}/agents/`
33
+ - **Skill Loading** - Loads skills from agent frontmatter
34
+ - **Task Fragmentation** - Creates subtasks for 3+ domain tasks
35
+ - **Prompt Injection** - Injects loaded agents, skills, and subtasks into prompt
36
+
37
+ #### Verified Flow
38
+ ```
39
+ Task → Orchestrator → 3 subtasks
40
+ Subtask 1 [database] → p.done → ✅
41
+ Subtask 2 [backend] → p.done → ✅
42
+ Subtask 3 [frontend] → p.done → ✅
43
+ ALL COMPLETE (100%)
44
+ ```
45
+
46
+ **Files Changed:**
47
+ - `core/agentic/orchestrator-executor.ts` (NEW - 482 lines)
48
+ - `core/agentic/command-executor.ts` - Calls orchestrator
49
+ - `core/agentic/prompt-builder.ts` - Injects orchestrator context
50
+ - `core/types/agentic.ts` - Added orchestrator types
51
+ - `core/commands/workflow.ts` - **Connects CLI to CommandExecutor** (was bypassing orchestrator)
52
+
3
53
  ## [0.34.0] - 2026-01-15
4
54
 
5
55
  ### Feature: Agentic Orchestrator + MCP Auto-Install
@@ -19,8 +19,10 @@ import memorySystem from './memory-system'
19
19
  import groundTruth from './ground-truth'
20
20
  import planMode from './plan-mode'
21
21
  import templateExecutor from './template-executor'
22
+ import orchestratorExecutor from './orchestrator-executor'
22
23
 
23
24
  import type {
25
+ OrchestratorContext,
24
26
  ExecutionResult,
25
27
  SimpleExecutionResult,
26
28
  ExecutionToolsFn,
@@ -171,6 +173,29 @@ export class CommandExecutor {
171
173
  )
172
174
  const agenticInfo = templateExecutor.buildAgenticPrompt(agenticExecContext)
173
175
 
176
+ // 3.5. ORCHESTRATOR: Execute orchestration for commands that require it
177
+ let orchestratorContext: OrchestratorContext | null = null
178
+ if (templateExecutor.requiresOrchestration(commandName) && taskDescription) {
179
+ try {
180
+ orchestratorContext = await orchestratorExecutor.execute(
181
+ commandName,
182
+ taskDescription,
183
+ projectPath
184
+ )
185
+
186
+ // Log orchestrator results
187
+ console.log(`🎯 Orchestrator:`)
188
+ console.log(` → Domains: ${orchestratorContext.detectedDomains.join(', ')}`)
189
+ console.log(` → Agents: ${orchestratorContext.agents.map(a => a.name).join(', ') || 'none loaded'}`)
190
+ if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
191
+ console.log(` → Subtasks: ${orchestratorContext.subtasks.length}`)
192
+ }
193
+ } catch (error) {
194
+ // Orchestration failed - log warning but continue without it
195
+ console.warn(`⚠️ Orchestrator warning: ${(error as Error).message}`)
196
+ }
197
+ }
198
+
174
199
  // Build context with agent routing info for Claude delegation
175
200
  const context: PromptContext = {
176
201
  ...metadataContext,
@@ -215,6 +240,7 @@ export class CommandExecutor {
215
240
  allowedTools: planMode.getAllowedTools(isInPlanningMode, template.frontmatter['allowed-tools'] || []),
216
241
  }
217
242
  // Agent is null - Claude assigns via Task tool using agent-routing.md
243
+ // Pass orchestratorContext for domain/agent/subtask injection
218
244
  const prompt = promptBuilder.build(
219
245
  template,
220
246
  context,
@@ -223,7 +249,8 @@ export class CommandExecutor {
223
249
  learnedPatterns,
224
250
  null,
225
251
  relevantMemories,
226
- planInfo
252
+ planInfo,
253
+ orchestratorContext
227
254
  )
228
255
 
229
256
  // Log agentic mode
@@ -257,6 +284,7 @@ export class CommandExecutor {
257
284
  groundTruth: groundTruthResult,
258
285
  learnedPatterns,
259
286
  relevantMemories,
287
+ orchestratorContext,
260
288
  memory: {
261
289
  create: (memory: unknown) =>
262
290
  memorySystem.createMemory(metadataContext.projectId!, memory as Parameters<typeof memorySystem.createMemory>[1]),
@@ -89,6 +89,7 @@ export {
89
89
  export { default as toolRegistry } from './tool-registry'
90
90
  export { default as templateLoader } from './template-loader'
91
91
  export { default as templateExecutor, TemplateExecutor } from './template-executor'
92
+ export { default as orchestratorExecutor, OrchestratorExecutor } from './orchestrator-executor'
92
93
 
93
94
  // ============ Utilities ============
94
95
  // Chain of thought, services
@@ -178,4 +179,9 @@ export type {
178
179
  ReasoningStep,
179
180
  ReasoningResult,
180
181
  ChainOfThoughtResult,
182
+ // Orchestrator types
183
+ OrchestratorContext,
184
+ LoadedAgent,
185
+ LoadedSkill,
186
+ OrchestratorSubtask,
181
187
  } from '../types'
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Orchestrator Executor
3
+ *
4
+ * EXECUTES orchestration in TypeScript - not just paths.
5
+ * This module:
6
+ * 1. Detects domains from task description
7
+ * 2. Loads relevant agents from {globalPath}/agents/
8
+ * 3. Loads skills from agent frontmatter
9
+ * 4. Determines if task should be fragmented
10
+ * 5. Creates subtasks in state.json if fragmented
11
+ *
12
+ * @module agentic/orchestrator-executor
13
+ * @version 1.0.0
14
+ */
15
+
16
+ import fs from 'fs/promises'
17
+ import path from 'path'
18
+ import os from 'os'
19
+ import pathManager from '../infrastructure/path-manager'
20
+ import configManager from '../infrastructure/config-manager'
21
+ import { stateStorage } from '../storage'
22
+ import { isNotFoundError } from '../types/fs'
23
+ import { parseFrontmatter } from './template-loader'
24
+ import type {
25
+ OrchestratorContext,
26
+ LoadedAgent,
27
+ LoadedSkill,
28
+ OrchestratorSubtask,
29
+ } from '../types'
30
+
31
+ // =============================================================================
32
+ // Domain Detection Keywords
33
+ // =============================================================================
34
+
35
+ /**
36
+ * Keywords that indicate a domain is involved in the task
37
+ * These are hints, not absolute rules - context matters
38
+ */
39
+ const DOMAIN_KEYWORDS: Record<string, string[]> = {
40
+ database: [
41
+ 'database', 'db', 'sql', 'query', 'table', 'schema', 'migration',
42
+ 'postgres', 'mysql', 'sqlite', 'mongo', 'redis', 'prisma', 'drizzle',
43
+ 'orm', 'model', 'entity', 'repository', 'data layer', 'persist',
44
+ ],
45
+ backend: [
46
+ 'api', 'endpoint', 'route', 'server', 'controller', 'service',
47
+ 'middleware', 'auth', 'authentication', 'authorization', 'jwt', 'oauth',
48
+ 'rest', 'graphql', 'trpc', 'express', 'fastify', 'hono', 'nest',
49
+ 'validation', 'business logic',
50
+ ],
51
+ frontend: [
52
+ 'ui', 'component', 'page', 'form', 'button', 'input', 'modal', 'dialog',
53
+ 'react', 'vue', 'svelte', 'angular', 'next', 'nuxt', 'solid',
54
+ 'css', 'style', 'tailwind', 'layout', 'responsive', 'animation',
55
+ 'hook', 'state', 'context', 'redux', 'zustand', 'jotai',
56
+ ],
57
+ testing: [
58
+ 'test', 'spec', 'unit', 'integration', 'e2e', 'jest', 'vitest',
59
+ 'playwright', 'cypress', 'mocha', 'chai', 'mock', 'stub', 'fixture',
60
+ 'coverage', 'assertion',
61
+ ],
62
+ devops: [
63
+ 'docker', 'kubernetes', 'k8s', 'ci', 'cd', 'pipeline', 'deploy',
64
+ 'github actions', 'vercel', 'aws', 'gcp', 'azure', 'terraform',
65
+ 'nginx', 'caddy', 'env', 'environment', 'config', 'secret',
66
+ ],
67
+ uxui: [
68
+ 'design', 'ux', 'user experience', 'accessibility', 'a11y',
69
+ 'color', 'typography', 'spacing', 'prototype', 'wireframe',
70
+ 'figma', 'user flow', 'interaction',
71
+ ],
72
+ }
73
+
74
+ /**
75
+ * Domain dependency order - earlier domains should complete first
76
+ */
77
+ const DOMAIN_DEPENDENCY_ORDER = [
78
+ 'database',
79
+ 'backend',
80
+ 'frontend',
81
+ 'testing',
82
+ 'devops',
83
+ ]
84
+
85
+ // =============================================================================
86
+ // Orchestrator Executor Class
87
+ // =============================================================================
88
+
89
+ export class OrchestratorExecutor {
90
+ /**
91
+ * Main entry point - executes full orchestration
92
+ *
93
+ * @param command - The command being executed (e.g., 'task')
94
+ * @param taskDescription - The task description from user
95
+ * @param projectPath - Path to the project
96
+ * @returns Full orchestrator context with loaded agents, skills, and subtasks
97
+ */
98
+ async execute(
99
+ command: string,
100
+ taskDescription: string,
101
+ projectPath: string
102
+ ): Promise<OrchestratorContext> {
103
+ // Step 1: Get project info
104
+ const projectId = await configManager.getProjectId(projectPath)
105
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
106
+
107
+ // Step 2: Load repo analysis for ecosystem info
108
+ const repoAnalysis = await this.loadRepoAnalysis(globalPath)
109
+
110
+ // Step 3: Detect domains from task + project context
111
+ const { domains, primary } = await this.detectDomains(
112
+ taskDescription,
113
+ projectId,
114
+ repoAnalysis
115
+ )
116
+
117
+ // Step 4: Load agents for detected domains
118
+ const agents = await this.loadAgents(domains, projectId)
119
+
120
+ // Step 5: Load skills from agent frontmatter
121
+ const skills = await this.loadSkills(agents)
122
+
123
+ // Step 6: Determine if fragmentation is needed
124
+ const requiresFragmentation = this.shouldFragment(domains, taskDescription)
125
+
126
+ // Step 7: Create subtasks if fragmentation is required
127
+ let subtasks: OrchestratorSubtask[] | null = null
128
+ if (requiresFragmentation && command === 'task') {
129
+ subtasks = await this.createSubtasks(
130
+ taskDescription,
131
+ domains,
132
+ agents,
133
+ projectId
134
+ )
135
+ }
136
+
137
+ return {
138
+ detectedDomains: domains,
139
+ primaryDomain: primary,
140
+ agents,
141
+ skills,
142
+ requiresFragmentation,
143
+ subtasks,
144
+ project: {
145
+ id: projectId,
146
+ ecosystem: repoAnalysis?.ecosystem || 'unknown',
147
+ conventions: repoAnalysis?.conventions || [],
148
+ },
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Load repo-analysis.json for project context
154
+ */
155
+ private async loadRepoAnalysis(
156
+ globalPath: string
157
+ ): Promise<{ ecosystem: string; conventions: string[]; technologies?: string[] } | null> {
158
+ try {
159
+ const analysisPath = path.join(globalPath, 'analysis', 'repo-analysis.json')
160
+ const content = await fs.readFile(analysisPath, 'utf-8')
161
+ return JSON.parse(content)
162
+ } catch (error) {
163
+ if (isNotFoundError(error)) return null
164
+ console.warn('Failed to load repo-analysis.json:', (error as Error).message)
165
+ return null
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Detect which domains are relevant for this task
171
+ *
172
+ * Uses keyword matching + project context to determine domains.
173
+ * More intelligent than simple string matching - considers:
174
+ * - Task description keywords
175
+ * - Project technology stack
176
+ * - Available agents
177
+ */
178
+ async detectDomains(
179
+ taskDescription: string,
180
+ projectId: string,
181
+ repoAnalysis: { ecosystem: string; technologies?: string[] } | null
182
+ ): Promise<{ domains: string[]; primary: string }> {
183
+ const taskLower = taskDescription.toLowerCase()
184
+ const detectedDomains: Map<string, number> = new Map()
185
+
186
+ // Score each domain based on keyword matches
187
+ for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
188
+ let score = 0
189
+ for (const keyword of keywords) {
190
+ if (taskLower.includes(keyword.toLowerCase())) {
191
+ // Weight multi-word matches higher
192
+ score += keyword.includes(' ') ? 3 : 1
193
+ }
194
+ }
195
+ if (score > 0) {
196
+ detectedDomains.set(domain, score)
197
+ }
198
+ }
199
+
200
+ // Boost scores for domains that match project technologies
201
+ if (repoAnalysis?.technologies) {
202
+ const techStr = repoAnalysis.technologies.join(' ').toLowerCase()
203
+
204
+ // If project has React/Vue/etc, boost frontend
205
+ if (/react|vue|svelte|angular|next|nuxt/.test(techStr)) {
206
+ const current = detectedDomains.get('frontend') || 0
207
+ if (current > 0) detectedDomains.set('frontend', current + 2)
208
+ }
209
+
210
+ // If project has Express/Fastify/etc, boost backend
211
+ if (/express|fastify|hono|nest|koa/.test(techStr)) {
212
+ const current = detectedDomains.get('backend') || 0
213
+ if (current > 0) detectedDomains.set('backend', current + 2)
214
+ }
215
+
216
+ // If project has Prisma/Drizzle/etc, boost database
217
+ if (/prisma|drizzle|mongoose|typeorm|sequelize/.test(techStr)) {
218
+ const current = detectedDomains.get('database') || 0
219
+ if (current > 0) detectedDomains.set('database', current + 2)
220
+ }
221
+ }
222
+
223
+ // Get available agents to filter domains
224
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
225
+ const availableAgents = await this.getAvailableAgentNames(globalPath)
226
+
227
+ // Only include domains that have corresponding agents
228
+ const validDomains = Array.from(detectedDomains.entries())
229
+ .filter(([domain]) => {
230
+ // Check if agent exists for this domain
231
+ return availableAgents.some(
232
+ agent =>
233
+ agent === domain ||
234
+ agent.includes(domain) ||
235
+ domain.includes(agent.replace('.md', ''))
236
+ )
237
+ })
238
+ .sort((a, b) => b[1] - a[1]) // Sort by score descending
239
+ .map(([domain]) => domain)
240
+
241
+ // If no domains detected, default to 'general'
242
+ if (validDomains.length === 0) {
243
+ return { domains: ['general'], primary: 'general' }
244
+ }
245
+
246
+ // Primary is the highest scoring domain
247
+ const primary = validDomains[0]
248
+
249
+ return { domains: validDomains, primary }
250
+ }
251
+
252
+ /**
253
+ * Get list of available agent file names
254
+ */
255
+ private async getAvailableAgentNames(globalPath: string): Promise<string[]> {
256
+ try {
257
+ const agentsDir = path.join(globalPath, 'agents')
258
+ const files = await fs.readdir(agentsDir)
259
+ return files.filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
260
+ } catch {
261
+ return []
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Load agents for the detected domains
267
+ *
268
+ * Reads agent markdown files from {globalPath}/agents/
269
+ * and extracts their content and skills from frontmatter.
270
+ */
271
+ async loadAgents(domains: string[], projectId: string): Promise<LoadedAgent[]> {
272
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
273
+ const agentsDir = path.join(globalPath, 'agents')
274
+ const agents: LoadedAgent[] = []
275
+
276
+ for (const domain of domains) {
277
+ // Try exact match first, then variations
278
+ const possibleNames = [
279
+ `${domain}.md`,
280
+ `${domain}-agent.md`,
281
+ `prjct-${domain}.md`,
282
+ ]
283
+
284
+ for (const fileName of possibleNames) {
285
+ const filePath = path.join(agentsDir, fileName)
286
+ try {
287
+ const content = await fs.readFile(filePath, 'utf-8')
288
+ const { frontmatter, body } = this.parseAgentFile(content)
289
+
290
+ agents.push({
291
+ name: fileName.replace('.md', ''),
292
+ domain,
293
+ content: body,
294
+ skills: frontmatter.skills || [],
295
+ filePath,
296
+ })
297
+
298
+ // Found one, no need to try other variations
299
+ break
300
+ } catch {
301
+ // Try next variation
302
+ continue
303
+ }
304
+ }
305
+ }
306
+
307
+ return agents
308
+ }
309
+
310
+ /**
311
+ * Parse agent markdown file to extract frontmatter and body
312
+ */
313
+ private parseAgentFile(content: string): {
314
+ frontmatter: { skills?: string[]; [key: string]: unknown }
315
+ body: string
316
+ } {
317
+ const parsed = parseFrontmatter(content)
318
+
319
+ // Convert skills from string to array if needed
320
+ const frontmatter = { ...parsed.frontmatter }
321
+ if (typeof frontmatter.skills === 'string') {
322
+ // Handle comma-separated string
323
+ frontmatter.skills = (frontmatter.skills as string).split(',').map(s => s.trim())
324
+ }
325
+
326
+ return {
327
+ frontmatter: frontmatter as { skills?: string[]; [key: string]: unknown },
328
+ body: parsed.content
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Load skills from agent frontmatter
334
+ *
335
+ * Skills are stored in ~/.claude/skills/{name}.md
336
+ */
337
+ async loadSkills(agents: LoadedAgent[]): Promise<LoadedSkill[]> {
338
+ const skillsDir = path.join(os.homedir(), '.claude', 'skills')
339
+ const skills: LoadedSkill[] = []
340
+ const loadedSkillNames = new Set<string>()
341
+
342
+ for (const agent of agents) {
343
+ for (const skillName of agent.skills) {
344
+ // Skip if already loaded
345
+ if (loadedSkillNames.has(skillName)) continue
346
+
347
+ const skillPath = path.join(skillsDir, `${skillName}.md`)
348
+ try {
349
+ const content = await fs.readFile(skillPath, 'utf-8')
350
+ skills.push({
351
+ name: skillName,
352
+ content,
353
+ filePath: skillPath,
354
+ })
355
+ loadedSkillNames.add(skillName)
356
+ } catch {
357
+ // Skill not found - not an error, just skip
358
+ console.warn(`Skill not found: ${skillName}`)
359
+ }
360
+ }
361
+ }
362
+
363
+ return skills
364
+ }
365
+
366
+ /**
367
+ * Determine if task should be fragmented into subtasks
368
+ *
369
+ * Fragmentation is needed when:
370
+ * - 3+ domains are involved
371
+ * - Task explicitly mentions multiple areas
372
+ * - Task is complex (many keywords)
373
+ */
374
+ shouldFragment(domains: string[], taskDescription: string): boolean {
375
+ // Always fragment if 3+ domains
376
+ if (domains.length >= 3) return true
377
+
378
+ // Check for explicit multi-area keywords
379
+ const multiAreaIndicators = [
380
+ 'full stack',
381
+ 'fullstack',
382
+ 'end to end',
383
+ 'e2e',
384
+ 'complete feature',
385
+ 'from database to ui',
386
+ 'across layers',
387
+ ]
388
+
389
+ const taskLower = taskDescription.toLowerCase()
390
+ for (const indicator of multiAreaIndicators) {
391
+ if (taskLower.includes(indicator)) return true
392
+ }
393
+
394
+ // Check word count - very long tasks often need fragmentation
395
+ const wordCount = taskDescription.split(/\s+/).length
396
+ if (wordCount > 30 && domains.length >= 2) return true
397
+
398
+ return false
399
+ }
400
+
401
+ /**
402
+ * Create subtasks for a fragmented task
403
+ *
404
+ * Orders subtasks by domain dependency (database -> backend -> frontend)
405
+ * and stores them in state.json
406
+ */
407
+ async createSubtasks(
408
+ taskDescription: string,
409
+ domains: string[],
410
+ agents: LoadedAgent[],
411
+ projectId: string
412
+ ): Promise<OrchestratorSubtask[]> {
413
+ // Sort domains by dependency order
414
+ const sortedDomains = [...domains].sort((a, b) => {
415
+ const orderA = DOMAIN_DEPENDENCY_ORDER.indexOf(a)
416
+ const orderB = DOMAIN_DEPENDENCY_ORDER.indexOf(b)
417
+ // Unknown domains go last
418
+ return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB)
419
+ })
420
+
421
+ // Create subtask for each domain
422
+ const subtasks: OrchestratorSubtask[] = sortedDomains.map((domain, index) => {
423
+ // Find the agent for this domain
424
+ const agent = agents.find(a => a.domain === domain)
425
+ const agentFile = agent ? `${agent.name}.md` : `${domain}.md`
426
+
427
+ // Determine dependencies - each subtask depends on previous ones
428
+ const dependsOn = sortedDomains.slice(0, index).map((d, i) => `subtask-${i + 1}`)
429
+
430
+ return {
431
+ id: `subtask-${index + 1}`,
432
+ description: this.generateSubtaskDescription(taskDescription, domain),
433
+ domain,
434
+ agent: agentFile,
435
+ status: index === 0 ? 'in_progress' : 'pending',
436
+ dependsOn,
437
+ order: index + 1,
438
+ }
439
+ })
440
+
441
+ // Store subtasks in state.json via state storage
442
+ await stateStorage.createSubtasks(
443
+ projectId,
444
+ subtasks.map(st => ({
445
+ id: st.id,
446
+ description: st.description,
447
+ domain: st.domain,
448
+ agent: st.agent,
449
+ dependsOn: st.dependsOn,
450
+ }))
451
+ )
452
+
453
+ return subtasks
454
+ }
455
+
456
+ /**
457
+ * Generate a domain-specific subtask description
458
+ */
459
+ private generateSubtaskDescription(
460
+ fullTask: string,
461
+ domain: string
462
+ ): string {
463
+ const domainDescriptions: Record<string, string> = {
464
+ database: 'Set up data layer: schema, models, migrations',
465
+ backend: 'Implement API: routes, controllers, services, validation',
466
+ frontend: 'Build UI: components, forms, state management',
467
+ testing: 'Write tests: unit, integration, e2e',
468
+ devops: 'Configure deployment: CI/CD, environment, containers',
469
+ uxui: 'Design user experience: flows, accessibility, styling',
470
+ }
471
+
472
+ const prefix = domainDescriptions[domain] || `Handle ${domain} aspects`
473
+ return `[${domain.toUpperCase()}] ${prefix} for: ${fullTask.substring(0, 80)}${fullTask.length > 80 ? '...' : ''}`
474
+ }
475
+ }
476
+
477
+ // =============================================================================
478
+ // Singleton Export
479
+ // =============================================================================
480
+
481
+ export const orchestratorExecutor = new OrchestratorExecutor()
482
+ export default orchestratorExecutor