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 +35 -0
- package/core/agentic/command-executor.ts +55 -5
- package/core/agentic/index.ts +7 -0
- package/core/agentic/orchestrator-executor.ts +482 -0
- package/core/agentic/prompt-builder.ts +67 -1
- package/core/agentic/template-executor.ts +261 -0
- package/core/commands/workflow.ts +28 -6
- package/core/schemas/state.ts +43 -1
- package/core/services/agent-service.ts +36 -45
- package/core/storage/state-storage.ts +259 -1
- package/core/types/agentic.ts +68 -0
- package/core/types/index.ts +5 -0
- package/dist/bin/prjct.mjs +14901 -0
- package/dist/core/infrastructure/command-installer.js +499 -0
- package/dist/core/infrastructure/editors-config.js +157 -0
- package/dist/core/infrastructure/setup.js +934 -0
- package/dist/core/utils/version.js +142 -0
- package/package.json +1 -1
- package/templates/agentic/orchestrator.md +144 -45
- package/templates/agentic/task-fragmentation.md +323 -0
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:
|
|
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:
|
|
168
|
-
agentRoutingPath:
|
|
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(`🤖
|
|
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]),
|
package/core/agentic/index.ts
CHANGED
|
@@ -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
|