prjct-cli 0.34.0 → 0.35.1

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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.1] - 2026-01-17
4
+
5
+ ### Fix: Orchestrator Now Actually Executes
6
+
7
+ Critical fix - the orchestrator was only generating PATHS to templates, not executing them.
8
+
9
+ #### What Was Broken
10
+ - `p. task` never ran domain detection
11
+ - Agents from `{globalPath}/agents/` were never loaded
12
+ - Tasks were never fragmented into subtasks
13
+ - No context was injected into prompts
14
+
15
+ #### What's Fixed
16
+ - **Created `orchestrator-executor.ts`** - Executes orchestration in TypeScript
17
+ - **Domain Detection** - Analyzes task keywords to detect database, backend, frontend, etc.
18
+ - **Agent Loading** - Loads agent content from `{globalPath}/agents/`
19
+ - **Skill Loading** - Loads skills from agent frontmatter
20
+ - **Task Fragmentation** - Creates subtasks for 3+ domain tasks
21
+ - **Prompt Injection** - Injects loaded agents, skills, and subtasks into prompt
22
+
23
+ #### Verified Flow
24
+ ```
25
+ Task → Orchestrator → 3 subtasks
26
+ Subtask 1 [database] → p.done → ✅
27
+ Subtask 2 [backend] → p.done → ✅
28
+ Subtask 3 [frontend] → p.done → ✅
29
+ ALL COMPLETE (100%)
30
+ ```
31
+
32
+ **Files Changed:**
33
+ - `core/agentic/orchestrator-executor.ts` (NEW - 482 lines)
34
+ - `core/agentic/command-executor.ts` - Calls orchestrator
35
+ - `core/agentic/prompt-builder.ts` - Injects orchestrator context
36
+ - `core/types/agentic.ts` - Added orchestrator types
37
+
3
38
  ## [0.34.0] - 2026-01-15
4
39
 
5
40
  ### Feature: Agentic Orchestrator + MCP Auto-Install
@@ -18,8 +18,11 @@ import chainOfThought from './chain-of-thought'
18
18
  import memorySystem from './memory-system'
19
19
  import groundTruth from './ground-truth'
20
20
  import planMode from './plan-mode'
21
+ import templateExecutor from './template-executor'
22
+ import orchestratorExecutor from './orchestrator-executor'
21
23
 
22
24
  import type {
25
+ OrchestratorContext,
23
26
  ExecutionResult,
24
27
  SimpleExecutionResult,
25
28
  ExecutionToolsFn,
@@ -160,13 +163,48 @@ export class CommandExecutor {
160
163
  }
161
164
  }
162
165
 
163
- // 3. AGENTIC: Claude decides agent assignment via templates
166
+ // 3. AGENTIC: Template-first execution
167
+ // Claude decides agent assignment via templates - no hardcoded routing
168
+ const taskDescription = (params.task as string) || (params.description as string) || ''
169
+ const agenticExecContext = await templateExecutor.buildContext(
170
+ commandName,
171
+ taskDescription,
172
+ projectPath
173
+ )
174
+ const agenticInfo = templateExecutor.buildAgenticPrompt(agenticExecContext)
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
+
164
199
  // Build context with agent routing info for Claude delegation
165
200
  const context: PromptContext = {
166
201
  ...metadataContext,
167
- agentsPath: path.join(os.homedir(), '.prjct-cli', 'projects', metadataContext.projectId || '', 'agents'),
168
- agentRoutingPath: path.join(__dirname, '..', '..', 'templates', 'agentic', 'agent-routing.md'),
202
+ agentsPath: agenticExecContext.paths.agentsDir,
203
+ agentRoutingPath: agenticExecContext.paths.agentRouting,
204
+ orchestratorPath: agenticExecContext.paths.orchestrator,
205
+ taskFragmentationPath: agenticExecContext.paths.taskFragmentation,
169
206
  agenticDelegation: true,
207
+ agenticMode: true,
170
208
  }
171
209
 
172
210
  // 6. Load state with filtered context
@@ -202,6 +240,7 @@ export class CommandExecutor {
202
240
  allowedTools: planMode.getAllowedTools(isInPlanningMode, template.frontmatter['allowed-tools'] || []),
203
241
  }
204
242
  // Agent is null - Claude assigns via Task tool using agent-routing.md
243
+ // Pass orchestratorContext for domain/agent/subtask injection
205
244
  const prompt = promptBuilder.build(
206
245
  template,
207
246
  context,
@@ -210,11 +249,15 @@ export class CommandExecutor {
210
249
  learnedPatterns,
211
250
  null,
212
251
  relevantMemories,
213
- planInfo
252
+ planInfo,
253
+ orchestratorContext
214
254
  )
215
255
 
216
256
  // Log agentic mode
217
- console.log(`🤖 Agentic delegation enabled - Claude will assign agent via Task tool`)
257
+ console.log(`🤖 Template-first execution: Claude reads templates and decides`)
258
+ if (agenticInfo.requiresOrchestration) {
259
+ console.log(` → Orchestration: ${agenticExecContext.paths.orchestrator}`)
260
+ }
218
261
 
219
262
  // Record successful attempt
220
263
  loopDetector.recordSuccess(commandName, loopContext)
@@ -229,12 +272,19 @@ export class CommandExecutor {
229
272
  state,
230
273
  prompt,
231
274
  agenticDelegation: true,
275
+ agenticMode: true,
276
+ agenticExecContext,
277
+ agenticPrompt: agenticInfo.prompt,
278
+ requiresOrchestration: agenticInfo.requiresOrchestration,
232
279
  agentsPath: context.agentsPath as string,
233
280
  agentRoutingPath: context.agentRoutingPath as string,
281
+ orchestratorPath: agenticExecContext.paths.orchestrator,
282
+ taskFragmentationPath: agenticExecContext.paths.taskFragmentation,
234
283
  reasoning,
235
284
  groundTruth: groundTruthResult,
236
285
  learnedPatterns,
237
286
  relevantMemories,
287
+ orchestratorContext,
238
288
  memory: {
239
289
  create: (memory: unknown) =>
240
290
  memorySystem.createMemory(metadataContext.projectId!, memory as Parameters<typeof memorySystem.createMemory>[1]),
@@ -88,6 +88,8 @@ export {
88
88
  // Tool and template management
89
89
  export { default as toolRegistry } from './tool-registry'
90
90
  export { default as templateLoader } from './template-loader'
91
+ export { default as templateExecutor, TemplateExecutor } from './template-executor'
92
+ export { default as orchestratorExecutor, OrchestratorExecutor } from './orchestrator-executor'
91
93
 
92
94
  // ============ Utilities ============
93
95
  // Chain of thought, services
@@ -177,4 +179,9 @@ export type {
177
179
  ReasoningStep,
178
180
  ReasoningResult,
179
181
  ChainOfThoughtResult,
182
+ // Orchestrator types
183
+ OrchestratorContext,
184
+ LoadedAgent,
185
+ LoadedSkill,
186
+ OrchestratorSubtask,
180
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