prjct-cli 0.34.0 → 0.35.0

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.
@@ -18,6 +18,7 @@ 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'
21
22
 
22
23
  import type {
23
24
  ExecutionResult,
@@ -160,13 +161,25 @@ export class CommandExecutor {
160
161
  }
161
162
  }
162
163
 
163
- // 3. AGENTIC: Claude decides agent assignment via templates
164
+ // 3. AGENTIC: Template-first execution
165
+ // Claude decides agent assignment via templates - no hardcoded routing
166
+ const taskDescription = (params.task as string) || (params.description as string) || ''
167
+ const agenticExecContext = await templateExecutor.buildContext(
168
+ commandName,
169
+ taskDescription,
170
+ projectPath
171
+ )
172
+ const agenticInfo = templateExecutor.buildAgenticPrompt(agenticExecContext)
173
+
164
174
  // Build context with agent routing info for Claude delegation
165
175
  const context: PromptContext = {
166
176
  ...metadataContext,
167
- agentsPath: path.join(os.homedir(), '.prjct-cli', 'projects', metadataContext.projectId || '', 'agents'),
168
- agentRoutingPath: path.join(__dirname, '..', '..', 'templates', 'agentic', 'agent-routing.md'),
177
+ agentsPath: agenticExecContext.paths.agentsDir,
178
+ agentRoutingPath: agenticExecContext.paths.agentRouting,
179
+ orchestratorPath: agenticExecContext.paths.orchestrator,
180
+ taskFragmentationPath: agenticExecContext.paths.taskFragmentation,
169
181
  agenticDelegation: true,
182
+ agenticMode: true,
170
183
  }
171
184
 
172
185
  // 6. Load state with filtered context
@@ -214,7 +227,10 @@ export class CommandExecutor {
214
227
  )
215
228
 
216
229
  // Log agentic mode
217
- console.log(`🤖 Agentic delegation enabled - Claude will assign agent via Task tool`)
230
+ console.log(`🤖 Template-first execution: Claude reads templates and decides`)
231
+ if (agenticInfo.requiresOrchestration) {
232
+ console.log(` → Orchestration: ${agenticExecContext.paths.orchestrator}`)
233
+ }
218
234
 
219
235
  // Record successful attempt
220
236
  loopDetector.recordSuccess(commandName, loopContext)
@@ -229,8 +245,14 @@ export class CommandExecutor {
229
245
  state,
230
246
  prompt,
231
247
  agenticDelegation: true,
248
+ agenticMode: true,
249
+ agenticExecContext,
250
+ agenticPrompt: agenticInfo.prompt,
251
+ requiresOrchestration: agenticInfo.requiresOrchestration,
232
252
  agentsPath: context.agentsPath as string,
233
253
  agentRoutingPath: context.agentRoutingPath as string,
254
+ orchestratorPath: agenticExecContext.paths.orchestrator,
255
+ taskFragmentationPath: agenticExecContext.paths.taskFragmentation,
234
256
  reasoning,
235
257
  groundTruth: groundTruthResult,
236
258
  learnedPatterns,
@@ -88,6 +88,7 @@ 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'
91
92
 
92
93
  // ============ Utilities ============
93
94
  // Chain of thought, services
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Template Executor
3
+ *
4
+ * Executes templates as the entry point for agentic command execution.
5
+ * TypeScript provides infrastructure (paths, reads/writes).
6
+ * Claude takes all agentic decisions via templates.
7
+ *
8
+ * @module agentic/template-executor
9
+ * @version 1.0.0
10
+ */
11
+
12
+ import fs from 'fs/promises'
13
+ import path from 'path'
14
+ import os from 'os'
15
+ import configManager from '../infrastructure/config-manager'
16
+ import pathManager from '../infrastructure/path-manager'
17
+ import { isNotFoundError } from '../types/fs'
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ export interface TemplateExecutionContext {
24
+ projectPath: string
25
+ projectId: string
26
+ globalPath: string
27
+ command: string
28
+ args: string
29
+
30
+ // Paths for Claude (not content - Claude reads what it needs)
31
+ paths: {
32
+ orchestrator: string
33
+ agentRouting: string
34
+ taskFragmentation: string
35
+ commandTemplate: string
36
+ repoAnalysis: string
37
+ agentsDir: string
38
+ skillsDir: string
39
+ stateJson: string
40
+ }
41
+ }
42
+
43
+ export interface AgenticPromptInfo {
44
+ prompt: string
45
+ context: TemplateExecutionContext
46
+ requiresOrchestration: boolean
47
+ }
48
+
49
+ // Commands that require orchestration (task routing, fragmentation)
50
+ const ORCHESTRATED_COMMANDS = ['task', 'done', 'ship', 'resume', 'bug', 'enrich']
51
+
52
+ // Commands that do NOT need orchestration
53
+ const SIMPLE_COMMANDS = ['init', 'sync', 'pause', 'next', 'dash', 'history', 'undo', 'redo']
54
+
55
+ // =============================================================================
56
+ // Template Executor Class
57
+ // =============================================================================
58
+
59
+ export class TemplateExecutor {
60
+ /**
61
+ * Get npm root for templates path
62
+ */
63
+ private async getNpmRoot(): Promise<string> {
64
+ // Get the templates path relative to this module
65
+ // In production, templates are in the npm package
66
+ const moduleDir = path.dirname(require.resolve('prjct-cli/package.json'))
67
+ return moduleDir
68
+ }
69
+
70
+ /**
71
+ * Get project ID from local config
72
+ */
73
+ private async getProjectId(projectPath: string): Promise<string> {
74
+ return configManager.getProjectId(projectPath)
75
+ }
76
+
77
+ /**
78
+ * Build execution context with paths only
79
+ * Claude reads files agentically - we don't inject content
80
+ */
81
+ async buildContext(
82
+ command: string,
83
+ args: string,
84
+ projectPath: string
85
+ ): Promise<TemplateExecutionContext> {
86
+ const projectId = await this.getProjectId(projectPath)
87
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
88
+
89
+ // Get templates directory - use local path during development
90
+ let templatesDir: string
91
+ try {
92
+ const npmRoot = await this.getNpmRoot()
93
+ templatesDir = path.join(npmRoot, 'templates')
94
+ } catch {
95
+ // Fallback to local templates during development
96
+ templatesDir = path.join(__dirname, '..', '..', 'templates')
97
+ }
98
+
99
+ return {
100
+ projectPath,
101
+ projectId,
102
+ globalPath,
103
+ command,
104
+ args,
105
+ paths: {
106
+ orchestrator: path.join(templatesDir, 'agentic', 'orchestrator.md'),
107
+ agentRouting: path.join(templatesDir, 'agentic', 'agent-routing.md'),
108
+ taskFragmentation: path.join(templatesDir, 'agentic', 'task-fragmentation.md'),
109
+ commandTemplate: path.join(templatesDir, 'commands', `${command}.md`),
110
+ repoAnalysis: path.join(globalPath, 'analysis', 'repo-analysis.json'),
111
+ agentsDir: path.join(globalPath, 'agents'),
112
+ skillsDir: path.join(os.homedir(), '.claude', 'skills'),
113
+ stateJson: path.join(globalPath, 'storage', 'state.json'),
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if a command requires orchestration
120
+ */
121
+ requiresOrchestration(command: string): boolean {
122
+ if (ORCHESTRATED_COMMANDS.includes(command)) return true
123
+ if (SIMPLE_COMMANDS.includes(command)) return false
124
+ // Default: assume orchestration needed for unknown commands
125
+ return true
126
+ }
127
+
128
+ /**
129
+ * Check if agents exist for the project
130
+ */
131
+ async hasAgents(projectPath: string): Promise<boolean> {
132
+ try {
133
+ const projectId = await this.getProjectId(projectPath)
134
+ const agentsDir = path.join(pathManager.getGlobalProjectPath(projectId), 'agents')
135
+ const files = await fs.readdir(agentsDir)
136
+ return files.some(f => f.endsWith('.md'))
137
+ } catch (error) {
138
+ if (isNotFoundError(error)) return false
139
+ return false
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get list of available agent names
145
+ */
146
+ async getAvailableAgents(projectPath: string): Promise<string[]> {
147
+ try {
148
+ const projectId = await this.getProjectId(projectPath)
149
+ const agentsDir = path.join(pathManager.getGlobalProjectPath(projectId), 'agents')
150
+ const files = await fs.readdir(agentsDir)
151
+ return files
152
+ .filter(f => f.endsWith('.md'))
153
+ .map(f => f.replace('.md', ''))
154
+ } catch {
155
+ return []
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Build prompt that tells Claude to execute templates agentically
161
+ */
162
+ buildAgenticPrompt(context: TemplateExecutionContext): AgenticPromptInfo {
163
+ const requiresOrchestration = this.requiresOrchestration(context.command)
164
+
165
+ const prompt = `
166
+ ## Agentic Execution Mode
167
+
168
+ You are executing a prjct command. Follow the template-first approach.
169
+
170
+ ### Context
171
+ - Command: ${context.command}
172
+ - Args: ${context.args}
173
+ - Project: ${context.projectPath}
174
+ - Project ID: ${context.projectId}
175
+
176
+ ### Paths (Read as needed)
177
+ - Orchestrator: ${context.paths.orchestrator}
178
+ - Agent Routing: ${context.paths.agentRouting}
179
+ - Task Fragmentation: ${context.paths.taskFragmentation}
180
+ - Command Template: ${context.paths.commandTemplate}
181
+ - Repo Analysis: ${context.paths.repoAnalysis}
182
+ - Agents Directory: ${context.paths.agentsDir}
183
+ - Skills Directory: ${context.paths.skillsDir}
184
+ - State JSON: ${context.paths.stateJson}
185
+
186
+ ### Instructions
187
+
188
+ 1. **Read the command template** (${context.paths.commandTemplate})
189
+
190
+ 2. **Check if orchestration is needed**
191
+ - This command ${requiresOrchestration ? 'REQUIRES' : 'does NOT require'} orchestration
192
+ ${requiresOrchestration ? `
193
+ 3. **Orchestration steps:**
194
+ - Read: ${context.paths.orchestrator}
195
+ - Read: ${context.paths.repoAnalysis} to understand project technologies
196
+ - Analyze the task: "${context.args}"
197
+ - Determine which domains are ACTUALLY relevant based on:
198
+ a) What the task requires
199
+ b) What technologies exist in this project
200
+ c) What agents are available in ${context.paths.agentsDir}
201
+
202
+ - **IMPORTANTE**: Los agentes en ${context.paths.agentsDir} YA son específicos del proyecto
203
+ (fueron generados durante p. sync con las tecnologías reales)
204
+
205
+ - SIEMPRE usar el especialista si existe para el dominio
206
+ - Solo usar generalista si NO existe agente para ese dominio
207
+
208
+ - Check if task should be fragmented (read: ${context.paths.taskFragmentation})
209
+ - If agents loaded, check their skills and load from ${context.paths.skillsDir}
210
+ ` : `
211
+ 3. **Simple execution:**
212
+ - Execute the command template directly
213
+ - No agent routing needed
214
+ `}
215
+
216
+ 4. **Execute the command template** with full context
217
+
218
+ 5. **Return results**
219
+
220
+ ### Agentic Decision Making
221
+
222
+ YOU decide:
223
+ - Whether to run orchestration (based on command type)
224
+ - Which domains the task involves (frontend, backend, database, etc.)
225
+ - Whether to fragment the task into subtasks
226
+ - Which specialist agents to delegate to
227
+
228
+ ALWAYS:
229
+ - Use specialist agents when they exist (they're already project-specific)
230
+ - Delegate subtasks to the appropriate specialist via Task tool
231
+ - Let specialists handle their domain (they have the project patterns)
232
+ - Generate and store summaries when subtasks complete
233
+
234
+ ONLY use generalist when:
235
+ - No specialist agent exists for that domain
236
+ - Task is completely outside project scope
237
+ - Extreme edge case
238
+
239
+ ### Subtask Management
240
+
241
+ When fragmenting tasks:
242
+ 1. Store subtasks in state.json under currentTask.subtasks
243
+ 2. Track progress: currentSubtaskIndex, subtaskProgress
244
+ 3. Each completed subtask generates a summary
245
+ 4. Pass summary to next agent for context handoff
246
+ `
247
+
248
+ return {
249
+ prompt,
250
+ context,
251
+ requiresOrchestration
252
+ }
253
+ }
254
+ }
255
+
256
+ // =============================================================================
257
+ // Singleton Export
258
+ // =============================================================================
259
+
260
+ export const templateExecutor = new TemplateExecutor()
261
+ export default templateExecutor
@@ -3,6 +3,9 @@
3
3
  * Core task management - Write-Through Architecture
4
4
  *
5
5
  * Uses storage layer: JSON (source) → MD (context) → Event (sync)
6
+ *
7
+ * AGENTIC: Uses template-executor for Claude-driven decisions.
8
+ * TypeScript provides infrastructure; Claude decides via templates.
6
9
  */
7
10
 
8
11
  import type { CommandResult, ProjectContext } from '../types'
@@ -15,6 +18,7 @@ import {
15
18
  out
16
19
  } from './base'
17
20
  import { stateStorage, queueStorage } from '../storage'
21
+ import { templateExecutor } from '../agentic/template-executor'
18
22
 
19
23
  export class WorkflowCommands extends PrjctCommandsBase {
20
24
  /**
@@ -32,9 +36,12 @@ export class WorkflowCommands extends PrjctCommandsBase {
32
36
  }
33
37
 
34
38
  if (task) {
35
- const context = await contextBuilder.build(projectPath, { task }) as ProjectContext
36
- const agentResult = await this._assignAgentForTask(task, projectPath, context)
37
- const agent = agentResult.agent?.name || 'generalist'
39
+ // AGENTIC: Build execution context for Claude to decide
40
+ const execContext = await templateExecutor.buildContext('task', task, projectPath)
41
+ const agenticInfo = templateExecutor.buildAgenticPrompt(execContext)
42
+
43
+ // Get available agents for context
44
+ const availableAgents = await templateExecutor.getAvailableAgents(projectPath)
38
45
 
39
46
  // Write-through: JSON → MD → Event
40
47
  await stateStorage.startTask(projectId, {
@@ -43,14 +50,29 @@ export class WorkflowCommands extends PrjctCommandsBase {
43
50
  sessionId: generateUUID()
44
51
  })
45
52
 
46
- out.done(`${task} [${agent}]`)
53
+ // AGENTIC: Log that Claude will decide via templates
54
+ const agentsList = availableAgents.length > 0
55
+ ? availableAgents.join(', ')
56
+ : 'none (run p. sync)'
57
+
58
+ console.log(`🤖 Agentic mode: Claude will read templates and decide`)
59
+ out.done(`${task} [specialists: ${agentsList}]`)
47
60
 
48
61
  await this.logToMemory(projectPath, 'task_started', {
49
62
  task,
50
- agent,
63
+ agenticMode: true,
64
+ availableAgents,
51
65
  timestamp: dateHelper.getTimestamp(),
52
66
  })
53
- return { success: true, task, agent }
67
+
68
+ return {
69
+ success: true,
70
+ task,
71
+ agenticMode: true,
72
+ availableAgents,
73
+ execContext,
74
+ agenticPrompt: agenticInfo.prompt,
75
+ }
54
76
  } else {
55
77
  // Read from storage (JSON is source of truth)
56
78
  const currentTask = await stateStorage.getCurrentTask(projectId)
@@ -17,15 +17,53 @@ import { z } from 'zod'
17
17
  export const PrioritySchema = z.enum(['low', 'medium', 'high', 'critical'])
18
18
  export const TaskTypeSchema = z.enum(['feature', 'bug', 'improvement', 'chore'])
19
19
  export const TaskSectionSchema = z.enum(['active', 'backlog', 'previously_active'])
20
- export const TaskStatusSchema = z.enum(['pending', 'in_progress', 'completed', 'blocked', 'paused'])
20
+ export const TaskStatusSchema = z.enum(['pending', 'in_progress', 'completed', 'blocked', 'paused', 'failed'])
21
21
  export const ActivityTypeSchema = z.enum(['task_completed', 'feature_shipped', 'idea_captured', 'session_started'])
22
22
 
23
+ // Subtask summary for context handoff between agents
24
+ export const SubtaskSummarySchema = z.object({
25
+ title: z.string(),
26
+ description: z.string(),
27
+ filesChanged: z.array(z.object({
28
+ path: z.string(),
29
+ action: z.enum(['created', 'modified', 'deleted']),
30
+ })),
31
+ whatWasDone: z.array(z.string()),
32
+ outputForNextAgent: z.string().optional(),
33
+ notes: z.string().optional(),
34
+ })
35
+
36
+ // Subtask schema for task fragmentation
37
+ export const SubtaskSchema = z.object({
38
+ id: z.string(), // subtask-xxx
39
+ description: z.string(),
40
+ domain: z.string(), // frontend, backend, database, testing, etc.
41
+ agent: z.string(), // agent file name (e.g., "frontend.md")
42
+ status: TaskStatusSchema,
43
+ dependsOn: z.array(z.string()), // IDs of dependent subtasks
44
+ startedAt: z.string().optional(), // ISO8601
45
+ completedAt: z.string().optional(), // ISO8601
46
+ output: z.string().optional(), // Brief output description
47
+ summary: SubtaskSummarySchema.optional(), // Full summary for context handoff
48
+ })
49
+
50
+ // Subtask progress tracking
51
+ export const SubtaskProgressSchema = z.object({
52
+ completed: z.number(),
53
+ total: z.number(),
54
+ percentage: z.number(),
55
+ })
56
+
23
57
  export const CurrentTaskSchema = z.object({
24
58
  id: z.string(), // task_xxxxxxxx
25
59
  description: z.string(),
26
60
  startedAt: z.string(), // ISO8601
27
61
  sessionId: z.string(), // sess_xxxxxxxx
28
62
  featureId: z.string().optional(), // feat_xxxxxxxx
63
+ // Subtask tracking for fragmented tasks
64
+ subtasks: z.array(SubtaskSchema).optional(),
65
+ currentSubtaskIndex: z.number().optional(),
66
+ subtaskProgress: SubtaskProgressSchema.optional(),
29
67
  })
30
68
 
31
69
  export const PreviousTaskSchema = z.object({
@@ -99,6 +137,10 @@ export type TaskSection = z.infer<typeof TaskSectionSchema>
99
137
  export type TaskStatus = z.infer<typeof TaskStatusSchema>
100
138
  export type ActivityType = z.infer<typeof ActivityTypeSchema>
101
139
 
140
+ export type Subtask = z.infer<typeof SubtaskSchema>
141
+ export type SubtaskSummary = z.infer<typeof SubtaskSummarySchema>
142
+ export type SubtaskProgress = z.infer<typeof SubtaskProgressSchema>
143
+
102
144
  export type CurrentTask = z.infer<typeof CurrentTaskSchema>
103
145
  export type PreviousTask = z.infer<typeof PreviousTaskSchema>
104
146
  export type StateJson = z.infer<typeof StateJsonSchema>
@@ -68,8 +68,13 @@ export class AgentService {
68
68
  }
69
69
 
70
70
  /**
71
- * Assign agent for a task using AgentRouter
72
- * Returns agent info for Claude to delegate work
71
+ * Assign agent for a task - AGENTIC APPROACH
72
+ *
73
+ * NO keyword matching. Returns available agents and context
74
+ * for Claude to make the decision via templates/orchestrator.md
75
+ *
76
+ * The agents in {agentsDir} are already project-specific
77
+ * (generated during p. sync with real technologies)
73
78
  */
74
79
  async assignForTask(
75
80
  task: string,
@@ -81,67 +86,53 @@ export class AgentService {
81
86
  const agents = await this.agentRouter.getAgentNames()
82
87
 
83
88
  if (agents.length === 0) {
89
+ // No agents available - suggest running sync
84
90
  return {
85
- agent: { name: 'generalist' },
91
+ agent: null, // Claude decides - don't default to generalist
86
92
  routing: {
87
- confidence: 1.0,
88
- reason: 'No specialized agents available',
93
+ confidence: 0,
94
+ reason: 'No specialized agents available. Run "p. sync" to generate agents.',
89
95
  availableAgents: [],
90
96
  },
97
+ _agenticNote: 'AGENTIC: Claude reads orchestrator.md and decides how to proceed',
91
98
  }
92
99
  }
93
100
 
94
- // Simple keyword matching for agent assignment
95
- // Claude will make the final decision via templates
96
- const taskLower = task.toLowerCase()
97
- let bestMatch = 'generalist'
98
-
99
- for (const agentName of agents) {
100
- const nameLower = agentName.toLowerCase()
101
- if (taskLower.includes(nameLower) || nameLower.includes('general')) {
102
- bestMatch = agentName
103
- break
104
- }
105
- // Common domain keywords
106
- if (
107
- (nameLower.includes('fe') || nameLower.includes('frontend')) &&
108
- (taskLower.includes('ui') ||
109
- taskLower.includes('component') ||
110
- taskLower.includes('react'))
111
- ) {
112
- bestMatch = agentName
113
- break
114
- }
115
- if (
116
- (nameLower.includes('be') || nameLower.includes('backend')) &&
117
- (taskLower.includes('api') ||
118
- taskLower.includes('server') ||
119
- taskLower.includes('database'))
120
- ) {
121
- bestMatch = agentName
122
- break
123
- }
124
- }
125
-
126
- await this.agentRouter.logUsage(task, bestMatch, projectPath)
101
+ // AGENTIC: No keyword matching here
102
+ // Claude reads the orchestrator template and decides based on:
103
+ // 1. Task analysis (what domains are involved)
104
+ // 2. Available agents (from agents directory)
105
+ // 3. Whether to fragment into subtasks
106
+ //
107
+ // The TypeScript code just provides the list of available agents
127
108
 
128
109
  return {
129
- agent: { name: bestMatch },
110
+ agent: null, // Claude decides via templates
130
111
  routing: {
131
- confidence: 0.7,
132
- reason: 'Keyword-based agent matching',
112
+ confidence: 0, // Claude determines confidence
113
+ reason: 'AGENTIC: Claude will analyze task and select appropriate specialist agents',
133
114
  availableAgents: agents,
134
115
  },
135
- _agenticNote: 'Claude should verify this assignment using agent context',
116
+ _agenticNote: `
117
+ AGENTIC EXECUTION:
118
+ - Read: templates/agentic/orchestrator.md
119
+ - Analyze task: "${task}"
120
+ - Available specialists: ${agents.join(', ')}
121
+ - Claude decides which agent(s) to use
122
+ - Always prefer specialists over generalist
123
+ - Fragment complex tasks into subtasks
124
+ `,
136
125
  }
137
126
  } catch (_error) {
138
127
  // Agent routing unavailable - expected for new projects
139
128
  return {
140
- agent: { name: 'generalist' },
129
+ agent: null,
141
130
  routing: {
142
- confidence: 1.0,
143
- reason: 'Agent routing unavailable',
131
+ confidence: 0,
132
+ reason: 'Agent routing unavailable - run "p. sync" first',
133
+ availableAgents: [],
144
134
  },
135
+ _agenticNote: 'AGENTIC: Suggest running p. sync to generate agents',
145
136
  }
146
137
  }
147
138
  }