prjct-cli 0.35.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 +29 -1
- package/core/agentic/index.ts +6 -0
- package/core/agentic/orchestrator-executor.ts +482 -0
- package/core/agentic/prompt-builder.ts +67 -1
- package/core/types/agentic.ts +62 -0
- package/core/types/index.ts +5 -0
- package/dist/bin/prjct.mjs +646 -123
- package/package.json +1 -1
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
|
|
@@ -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]),
|
package/core/agentic/index.ts
CHANGED
|
@@ -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
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
ThinkBlock,
|
|
25
25
|
Memory,
|
|
26
26
|
PlanInfo,
|
|
27
|
+
OrchestratorContext,
|
|
27
28
|
} from '../types'
|
|
28
29
|
|
|
29
30
|
// Re-export types for convenience
|
|
@@ -287,7 +288,8 @@ class PromptBuilder {
|
|
|
287
288
|
learnedPatterns: LearnedPatterns | null = null,
|
|
288
289
|
thinkBlock: ThinkBlock | null = null,
|
|
289
290
|
relevantMemories: Memory[] | null = null,
|
|
290
|
-
planInfo: PlanInfo | null = null
|
|
291
|
+
planInfo: PlanInfo | null = null,
|
|
292
|
+
orchestratorContext: OrchestratorContext | null = null
|
|
291
293
|
): string {
|
|
292
294
|
const parts: string[] = []
|
|
293
295
|
|
|
@@ -326,6 +328,70 @@ class PromptBuilder {
|
|
|
326
328
|
// This ensures Claude sees ALL instructions including critical rules at the top
|
|
327
329
|
parts.push(template.content)
|
|
328
330
|
|
|
331
|
+
// ORCHESTRATOR CONTEXT: Inject loaded agents, skills, and subtasks
|
|
332
|
+
if (orchestratorContext) {
|
|
333
|
+
parts.push('\n## ORCHESTRATOR CONTEXT\n')
|
|
334
|
+
parts.push(`**Primary Domain**: ${orchestratorContext.primaryDomain}\n`)
|
|
335
|
+
parts.push(`**Domains**: ${orchestratorContext.detectedDomains.join(', ')}\n`)
|
|
336
|
+
parts.push(`**Ecosystem**: ${orchestratorContext.project.ecosystem}\n\n`)
|
|
337
|
+
|
|
338
|
+
// Inject loaded agent content (truncated for context efficiency)
|
|
339
|
+
if (orchestratorContext.agents.length > 0) {
|
|
340
|
+
parts.push('### LOADED AGENTS (Project-Specific Specialists)\n\n')
|
|
341
|
+
for (const agent of orchestratorContext.agents) {
|
|
342
|
+
parts.push(`#### Agent: ${agent.name} (${agent.domain})\n`)
|
|
343
|
+
if (agent.skills.length > 0) {
|
|
344
|
+
parts.push(`Skills: ${agent.skills.join(', ')}\n`)
|
|
345
|
+
}
|
|
346
|
+
// Include first 1500 chars of agent content
|
|
347
|
+
const truncatedContent = agent.content.length > 1500
|
|
348
|
+
? agent.content.substring(0, 1500) + '\n... (truncated, read full file for more)'
|
|
349
|
+
: agent.content
|
|
350
|
+
parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Inject loaded skill content (truncated)
|
|
355
|
+
if (orchestratorContext.skills.length > 0) {
|
|
356
|
+
parts.push('### LOADED SKILLS (From Agent Frontmatter)\n\n')
|
|
357
|
+
for (const skill of orchestratorContext.skills) {
|
|
358
|
+
parts.push(`#### Skill: ${skill.name}\n`)
|
|
359
|
+
// Include first 1000 chars of skill content
|
|
360
|
+
const truncatedContent = skill.content.length > 1000
|
|
361
|
+
? skill.content.substring(0, 1000) + '\n... (truncated)'
|
|
362
|
+
: skill.content
|
|
363
|
+
parts.push(`\`\`\`markdown\n${truncatedContent}\n\`\`\`\n\n`)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Inject subtasks if fragmented
|
|
368
|
+
if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
|
|
369
|
+
parts.push('### SUBTASKS (Execute in Order)\n\n')
|
|
370
|
+
parts.push('**IMPORTANT**: Focus on the CURRENT subtask. Use `p. done` when complete to advance.\n\n')
|
|
371
|
+
parts.push('| # | Domain | Description | Status |\n')
|
|
372
|
+
parts.push('|---|--------|-------------|--------|\n')
|
|
373
|
+
|
|
374
|
+
for (const subtask of orchestratorContext.subtasks) {
|
|
375
|
+
const statusIcon = subtask.status === 'in_progress' ? '▶️ **CURRENT**'
|
|
376
|
+
: subtask.status === 'completed' ? '✅ Done'
|
|
377
|
+
: subtask.status === 'failed' ? '❌ Failed'
|
|
378
|
+
: '⏳ Pending'
|
|
379
|
+
parts.push(`| ${subtask.order} | ${subtask.domain} | ${subtask.description} | ${statusIcon} |\n`)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Find and highlight current subtask
|
|
383
|
+
const currentSubtask = orchestratorContext.subtasks.find(s => s.status === 'in_progress')
|
|
384
|
+
if (currentSubtask) {
|
|
385
|
+
parts.push(`\n**FOCUS ON SUBTASK #${currentSubtask.order}**: ${currentSubtask.description}\n`)
|
|
386
|
+
parts.push(`Agent: ${currentSubtask.agent} | Domain: ${currentSubtask.domain}\n`)
|
|
387
|
+
if (currentSubtask.dependsOn.length > 0) {
|
|
388
|
+
parts.push(`Dependencies: ${currentSubtask.dependsOn.join(', ')}\n`)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
parts.push('\n')
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
329
395
|
// Current state (only if exists and relevant)
|
|
330
396
|
const relevantState = this.filterRelevantState(state)
|
|
331
397
|
if (relevantState) {
|