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 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]),
@@ -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) {