prjct-cli 0.10.11 → 0.10.12

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.10.12] - 2025-11-29
4
+
5
+ ### Refactored - Mandatory Agent Assignment (100% Agentic)
6
+
7
+ All agent assignment decisions now delegated to Claude via templates. JS code is pure orchestration.
8
+
9
+ - **New Template**: `templates/agent-assignment.md`
10
+ - Claude decides which agent based on task + available agents + context
11
+ - No hardcoded scoring weights or matching algorithms
12
+ - Semantic understanding replaces keyword matching
13
+
14
+ - **Simplified `agent-router.js`**: 419 → 128 lines (69% reduction)
15
+ - Removed: scoring logic, domain mappings, caching algorithms
16
+ - Kept: load agents, build context, log usage (I/O only)
17
+ - Class renamed: `MandatoryAgentRouter` → `AgentRouter`
18
+
19
+ - **Simplified `agent-matcher.js`**: 218 → 103 lines (53% reduction)
20
+ - Removed: multi-factor scoring (40% domain, 30% skills, etc.)
21
+ - Removed: all if/else matching logic
22
+ - Kept: format data, record usage, load history (I/O only)
23
+
24
+ - **Updated commands.js**:
25
+ - `/p:now` - Assigns agent before setting task, shows `[agent]`
26
+ - `/p:feature` - Assigns agent to each task, format: `[agent] [ ] task`
27
+ - `/p:bug` - Assigns agent, shows `→ agent`
28
+ - `/p:build` - Uses async `_assignAgentForTask()`
29
+ - New method: `_assignAgentForTask()` - orchestrates agent assignment
30
+
31
+ ### Architecture Principle
32
+
33
+ **JS = Orchestrator** (load files, build context, format data, I/O)
34
+ **Claude = Decision Maker** (via templates for all logic)
35
+
36
+ No scoring algorithms, no matching weights, no domain mappings in code.
37
+
3
38
  ## [0.10.11] - 2025-11-29
4
39
 
5
40
  ### Refactored - 100% Agentic System
@@ -1,421 +1,128 @@
1
1
  /**
2
- * Mandatory Agent Router
2
+ * Agent Router - Orchestration Only
3
3
  *
4
- * CRITICAL: Ensures EVERY task is executed by a specialized agent
5
- * No task can run without an assigned expert agent
4
+ * AGENTIC: All decisions made by Claude via templates/agent-assignment.md
5
+ * JS only orchestrates: load agents, build context, delegate to Claude
6
6
  *
7
- * @version 1.0.0
7
+ * NO scoring logic, NO matching algorithms, NO hardcoded mappings
8
+ *
9
+ * @version 2.0.0
8
10
  */
9
11
 
10
- const fs = require('fs').promises;
11
- const path = require('path');
12
- const AgentGenerator = require('../domain/agent-generator');
13
- const configManager = require('../infrastructure/config-manager');
14
- const TaskAnalyzer = require('../domain/task-analyzer');
15
- const AgentMatcher = require('../domain/agent-matcher');
16
- const SmartCache = require('../domain/smart-cache');
17
- const AgentValidator = require('../domain/agent-validator');
18
- const log = require('../utils/logger');
12
+ const fs = require('fs').promises
13
+ const path = require('path')
14
+ const configManager = require('../infrastructure/config-manager')
15
+ const pathManager = require('../infrastructure/path-manager')
19
16
 
20
- class MandatoryAgentRouter {
17
+ class AgentRouter {
21
18
  constructor() {
22
- this.agentGenerator = null; // Will be initialized with projectId
23
- this.agentCache = null; // SmartCache instance
24
- this.usageLog = [];
25
- this.projectId = null;
26
- this.taskAnalyzer = null;
27
- this.agentMatcher = new AgentMatcher();
28
- this.agentValidator = new AgentValidator();
19
+ this.projectId = null
20
+ this.agentsPath = null
29
21
  }
30
22
 
31
23
  /**
32
24
  * Initialize with project context
33
- * @param {string} projectPath - Path to the project
25
+ * ORCHESTRATION: Just sets up paths
34
26
  */
35
27
  async initialize(projectPath) {
36
- this.projectId = await configManager.getProjectId(projectPath);
37
- this.agentGenerator = new AgentGenerator(this.projectId);
38
- this.agentCache = new SmartCache(this.projectId);
39
- await this.agentCache.initialize();
40
- this.taskAnalyzer = new TaskAnalyzer(projectPath);
41
- await this.taskAnalyzer.initialize();
28
+ this.projectId = await configManager.getProjectId(projectPath)
29
+ this.agentsPath = pathManager.getPath(this.projectId, 'agents')
42
30
  }
43
31
 
44
32
  /**
45
- * Main entry point - ALL tasks MUST go through here
46
- * @throws {Error} If no agent can be assigned
33
+ * Load all available agents from project
34
+ * ORCHESTRATION: File I/O only, no logic
47
35
  */
48
- async executeTask(task, context, projectPath) {
49
- // Initialize if needed
50
- if (!this.agentGenerator) {
51
- await this.initialize(projectPath);
52
- }
53
-
54
- // STEP 1: Deep semantic task analysis (NEW)
55
- const taskAnalysis = await this.taskAnalyzer.analyzeTask(task);
56
-
57
- // STEP 2: Select or generate specialized agent (MANDATORY) - with intelligent matching
58
- const agent = await this.assignAgent(taskAnalysis, context, projectPath);
59
-
60
- // STEP 3: Validate agent assignment
61
- if (!agent || !agent.name) {
62
- throw new Error(
63
- `CRITICAL: No agent assigned for task "${task.description}".
64
- System requires ALL tasks to use specialized agents.`
65
- );
66
- }
67
-
68
- // STEP 4: Filter context for this specific agent
69
- const filteredContext = await this.filterContextForAgent(
70
- agent,
71
- context,
72
- taskAnalysis
73
- );
74
-
75
- // STEP 5: Log agent usage for tracking and learning
76
- this.logAgentUsage(task, agent, filteredContext);
77
- this.agentMatcher.recordSuccess(agent, taskAnalysis, true); // Learn from assignment
78
-
79
- // STEP 6: Return agent with filtered context
80
- return {
81
- agent,
82
- context: filteredContext,
83
- taskAnalysis,
84
- routing: {
85
- reason: taskAnalysis.reason,
86
- confidence: taskAnalysis.confidence,
87
- alternativeAgents: taskAnalysis.alternatives
36
+ async loadAvailableAgents() {
37
+ try {
38
+ const files = await fs.readdir(this.agentsPath)
39
+ const agents = []
40
+
41
+ for (const file of files) {
42
+ if (file.endsWith('.md')) {
43
+ const name = file.replace('.md', '')
44
+ const content = await fs.readFile(
45
+ path.join(this.agentsPath, file),
46
+ 'utf-8'
47
+ )
48
+ agents.push({ name, content })
49
+ }
88
50
  }
89
- };
90
- }
91
51
 
92
- /**
93
- * Analyze task to determine what type of expertise is needed
94
- *
95
- * 100% AGENTIC: Delegates to TaskAnalyzer which uses templates.
96
- * NO hardcoded patterns or keyword lists.
97
- */
98
- async analyzeTask(task, projectPath = null) {
99
- // Use TaskAnalyzer for semantic analysis (template-driven)
100
- if (this.taskAnalyzer) {
101
- return await this.taskAnalyzer.analyzeTask(task);
52
+ return agents
53
+ } catch {
54
+ return []
102
55
  }
103
-
104
- // Fallback: Return minimal analysis, let Claude decide in prompt
105
- return {
106
- domain: 'generalist',
107
- confidence: 0.5,
108
- matchedKeywords: [],
109
- reason: 'Using generalist - Claude will analyze task in context',
110
- alternatives: ['full-stack'],
111
- projectTechnologies: null
112
- };
113
56
  }
114
57
 
115
58
  /**
116
- * Assign the best agent for the task
117
- * IMPROVED: Uses intelligent matching with scoring
59
+ * Get agent names list
60
+ * ORCHESTRATION: Simple extraction
118
61
  */
119
- async assignAgent(taskAnalysis, context, projectPath, overrideAgent = null) {
120
- // Respect override
121
- if (overrideAgent) {
122
- const existing = await this.agentGenerator.loadAgent(overrideAgent);
123
- if (existing) {
124
- return existing;
125
- }
126
- return this.generateSpecializedAgent(overrideAgent, {}, context);
127
- }
128
-
129
- const primaryDomain = taskAnalysis.primaryDomain;
130
- const projectTech = taskAnalysis.projectTechnologies || {};
131
-
132
- // Generate cache key with tech stack
133
- const techStack = {
134
- languages: projectTech.languages || [],
135
- frameworks: projectTech.frameworks || []
136
- };
137
- const cacheKey = this.agentCache.generateKey(this.projectId, primaryDomain, techStack);
138
-
139
- // Check smart cache first
140
- const cached = await this.agentCache.get(cacheKey);
141
- if (cached) {
142
- return cached;
143
- }
144
-
145
- // STEP 1: Load all existing agents
146
- const allAgents = await this.agentGenerator.loadAllAgents();
147
-
148
- // STEP 2: Use intelligent matching to find best agent
149
- const match = this.agentMatcher.findBestAgent(allAgents, taskAnalysis);
150
-
151
- if (match && match.score > 0.5) {
152
- // Good match found - use it
153
- await this.agentCache.set(cacheKey, match.agent);
154
- return match.agent;
155
- }
156
-
157
- // STEP 3: Try to load domain-specific agent
158
- const agentType = this.getAgentTypeForDomain(primaryDomain);
159
- const existingAgent = await this.agentGenerator.loadAgent(agentType);
160
-
161
- if (existingAgent) {
162
- await this.agentCache.set(cacheKey, existingAgent);
163
- return existingAgent;
164
- }
165
-
166
- // STEP 4: Validate before generating new agent
167
- const config = {
168
- domain: primaryDomain,
169
- projectContext: context.projectSummary || context.projectContext || '',
170
- expertise: this.buildExpertiseFromTech(projectTech, primaryDomain)
171
- };
172
-
173
- const validation = this.agentValidator.validateBeforeGeneration(
174
- agentType,
175
- config,
176
- allAgents
177
- );
178
-
179
- if (!validation.valid && validation.similarAgent) {
180
- // Similar agent exists - use it instead
181
- await this.agentCache.set(cacheKey, validation.similarAgent);
182
- return validation.similarAgent;
183
- }
184
-
185
- // STEP 5: Generate new agent only if validated
186
- const agent = await this.generateSpecializedAgent(primaryDomain, techStack, context);
187
-
188
- // Validate after generation
189
- const postValidation = this.agentValidator.validateAfterGeneration(agent);
190
- if (!postValidation.valid) {
191
- log.warn(`Agent validation issues: ${postValidation.issues.join(', ')}`);
192
- }
193
-
194
- // Cache for reuse
195
- await this.agentCache.set(cacheKey, agent);
196
-
197
- return agent;
62
+ async getAgentNames() {
63
+ const agents = await this.loadAvailableAgents()
64
+ return agents.map(a => a.name)
198
65
  }
199
66
 
200
67
  /**
201
- * Build expertise string from tech stack
202
- *
203
- * 100% AGENTIC: No hardcoded framework lists.
204
- * Returns ALL tech, Claude decides what's relevant.
68
+ * Load specific agent by name
69
+ * ORCHESTRATION: File I/O only
205
70
  */
206
- buildExpertiseFromTech(projectTech, domain) {
207
- const parts = []
208
-
209
- // Include ALL languages - no filtering
210
- if (projectTech.languages && projectTech.languages.length > 0) {
211
- parts.push(projectTech.languages.join(', '))
212
- }
213
-
214
- // Include ALL frameworks - Claude decides relevance
215
- // NO hardcoded lists like ['react', 'vue', 'angular']
216
- if (projectTech.frameworks && projectTech.frameworks.length > 0) {
217
- parts.push(projectTech.frameworks.join(', '))
218
- }
219
-
220
- // Include tools if present
221
- if (projectTech.tools && projectTech.tools.length > 0) {
222
- parts.push(projectTech.tools.join(', '))
71
+ async loadAgent(name) {
72
+ try {
73
+ const filePath = path.join(this.agentsPath, `${name}.md`)
74
+ const content = await fs.readFile(filePath, 'utf-8')
75
+ return { name, content }
76
+ } catch {
77
+ return null
223
78
  }
224
-
225
- return parts.join(', ') || `${domain} development`
226
79
  }
227
80
 
228
81
  /**
229
- * Get agent type name for a domain
230
- * @private
231
- */
232
- getAgentTypeForDomain(domain) {
233
- const agentTypes = {
234
- frontend: 'frontend-specialist',
235
- backend: 'backend-specialist',
236
- database: 'database-specialist',
237
- devops: 'devops-specialist',
238
- qa: 'qa-specialist',
239
- architecture: 'architect',
240
- generalist: 'full-stack'
241
- };
242
- return agentTypes[domain] || 'full-stack';
243
- }
244
-
245
- /**
246
- * Find similar agent from existing agents
247
- * DEPRECATED: Now uses AgentMatcher for intelligent matching
248
- * @private
249
- */
250
- findSimilarAgent(allAgents, domain, taskAnalysis) {
251
- // Use AgentMatcher instead
252
- const match = this.agentMatcher.findBestAgent(allAgents, taskAnalysis);
253
- return match ? match.agent : null;
254
- }
255
-
256
- /**
257
- * Generate a specialized agent for the detected domain
258
- * Only called when no existing agent is found
259
- */
260
- async generateSpecializedAgent(domain, techStack, context) {
261
- // Map domain to agent type
262
- const agentType = this.getAgentTypeForDomain(domain);
263
-
264
- // Generate with minimal config - let the Agent figure it out
265
- const config = {
266
- domain,
267
- projectContext: context.projectSummary || context.projectContext || '',
268
- // No hardcoded best practices passed here
269
- };
270
-
271
- // Generate the agent file
272
- await this.agentGenerator.generateDynamicAgent(agentType, config);
273
-
274
- // Load it immediately so we return the full agent object
275
- const agent = await this.agentGenerator.loadAgent(agentType);
276
-
277
- // If loading failed, return minimal object
278
- return agent || { name: agentType, content: '', domain };
279
- }
280
-
281
- /**
282
- * Filter context to only what's relevant for this agent
82
+ * Build context for agent assignment
83
+ * ORCHESTRATION: Data gathering only
283
84
  *
284
- * 100% AGENTIC: No hardcoded directory/extension lists.
285
- * Only excludes universal noise (node_modules, .git, dist).
286
- * Claude decides relevance based on task.
287
- */
288
- async filterContextForAgent(agent, fullContext, taskAnalysis) {
289
- // Universal exclusions that apply to ALL projects
290
- const universalExclusions = ['node_modules', '.git', 'dist', 'build', '.next', 'target', 'vendor'];
291
-
292
- // Filter only universal noise - let Claude decide the rest
293
- const filtered = {
294
- ...fullContext,
295
- files: (fullContext.files || []).filter(file =>
296
- !universalExclusions.some(exc => file.includes(exc))
297
- ),
298
- relevantOnly: false, // Claude decides relevance, not us
299
- filterApplied: 'universal-only'
300
- };
301
-
302
- return filtered;
303
- }
304
-
305
- /**
306
- * Filter files based on patterns
85
+ * Claude uses this context + templates/agent-assignment.md to decide
307
86
  */
308
- filterFiles(files, pattern) {
309
- return files.filter(file => {
310
- // Check if file should be excluded
311
- const isExcluded = pattern.exclude.some(exclude => file.includes(exclude));
312
- if (isExcluded) return false;
313
-
314
- // Check if file matches include patterns
315
- if (pattern.include.length > 0) {
316
- const isIncluded = pattern.include.some(include => file.includes(include));
317
- if (!isIncluded) return false;
318
- }
319
-
320
- // Check extensions if specified
321
- if (pattern.extensions.length > 0) {
322
- const hasValidExtension = pattern.extensions.some(ext => file.endsWith(ext));
323
- if (!hasValidExtension) return false;
324
- }
87
+ async buildAssignmentContext(task, projectPath) {
88
+ const agents = await this.getAgentNames()
325
89
 
326
- return true;
327
- });
328
- }
329
-
330
- /**
331
- * Log agent usage for metrics and optimization
332
- */
333
- logAgentUsage(task, agent, context) {
334
- const usage = {
335
- timestamp: new Date().toISOString(),
336
- task: task.description,
337
- agent: agent.name,
338
- domain: agent.domain || 'unknown',
339
- contextSize: context.files?.length || 0,
340
- contextReduction: this.calculateContextReduction(context),
341
- confidence: agent.confidence || 1.0
342
- };
343
-
344
- this.usageLog.push(usage);
345
-
346
- // Also append to a log file for persistence
347
- this.appendToLogFile(usage);
348
-
349
- return usage;
350
- }
351
-
352
- /**
353
- * Calculate how much context was reduced
354
- */
355
- calculateContextReduction(filteredContext) {
356
- // This would compare against full context
357
- // For now, estimate based on filtering
358
- if (filteredContext.relevantOnly) {
359
- return '70-90%'; // Typical reduction when filtering
90
+ return {
91
+ task: task.description || task,
92
+ availableAgents: agents,
93
+ projectPath,
94
+ projectId: this.projectId,
95
+ // Claude reads this and decides via template
96
+ _template: 'templates/agent-assignment.md'
360
97
  }
361
- return '0%';
362
98
  }
363
99
 
364
100
  /**
365
- * Append usage to log file
101
+ * Log agent usage
102
+ * ORCHESTRATION: File I/O only
366
103
  */
367
- async appendToLogFile(usage) {
104
+ async logUsage(task, agent, projectPath) {
368
105
  try {
369
106
  const logPath = path.join(
370
107
  process.env.HOME,
371
108
  '.prjct-cli',
109
+ 'projects',
110
+ this.projectId,
372
111
  'agent-usage.jsonl'
373
- );
374
-
375
- const logEntry = JSON.stringify(usage) + '\n';
376
- await fs.appendFile(logPath, logEntry);
377
- } catch (error) {
378
- log.error('Failed to log agent usage:', error.message);
112
+ )
113
+
114
+ const entry = JSON.stringify({
115
+ timestamp: new Date().toISOString(),
116
+ task: typeof task === 'string' ? task : task.description,
117
+ agent: agent.name || agent,
118
+ projectId: this.projectId
119
+ }) + '\n'
120
+
121
+ await fs.appendFile(logPath, entry)
122
+ } catch {
123
+ // Silent fail for logging
379
124
  }
380
125
  }
381
-
382
- /**
383
- * Get similar domains for fallback
384
- *
385
- * 100% AGENTIC: Returns generic fallback.
386
- * Claude determines domain relationships based on context.
387
- */
388
- getSimilarDomains(domain) {
389
- // No hardcoded domain relationships
390
- // Claude decides what's similar based on actual project context
391
- return ['full-stack', 'generalist'];
392
- }
393
-
394
- /**
395
- * Get usage statistics
396
- */
397
- getUsageStats() {
398
- const stats = {
399
- totalTasks: this.usageLog.length,
400
- byAgent: {},
401
- avgContextReduction: '0%',
402
- mostUsedAgent: null
403
- };
404
-
405
- // Calculate stats from usage log
406
- this.usageLog.forEach(log => {
407
- stats.byAgent[log.agent] = (stats.byAgent[log.agent] || 0) + 1;
408
- });
409
-
410
- // Find most used agent
411
- const mostUsed = Object.entries(stats.byAgent).reduce((max, [agent, count]) => {
412
- return count > max.count ? { agent, count } : max;
413
- }, { agent: null, count: 0 });
414
-
415
- stats.mostUsedAgent = mostUsed.agent;
416
-
417
- return stats;
418
- }
419
126
  }
420
127
 
421
- module.exports = MandatoryAgentRouter;
128
+ module.exports = AgentRouter
package/core/commands.js CHANGED
@@ -22,6 +22,7 @@ const commandExecutor = require('./agentic/command-executor')
22
22
  const contextBuilder = require('./agentic/context-builder')
23
23
  const toolRegistry = require('./agentic/tool-registry')
24
24
  const memorySystem = require('./agentic/memory-system')
25
+ const AgentRouter = require('./agentic/agent-router')
25
26
  const pathManager = require('./infrastructure/path-manager')
26
27
  const configManager = require('./infrastructure/config-manager')
27
28
  const authorDetector = require('./infrastructure/author-detector')
@@ -46,6 +47,7 @@ class PrjctCommands {
46
47
  this.updateChecker = new UpdateChecker()
47
48
  this.updateNotificationShown = false
48
49
  this.commandExecutor = commandExecutor
50
+ this.agentRouter = new AgentRouter()
49
51
  }
50
52
 
51
53
  /**
@@ -138,17 +140,24 @@ class PrjctCommands {
138
140
  const context = await contextBuilder.build(projectPath, { task })
139
141
 
140
142
  if (task) {
141
- // Set task
142
- const nowContent = `# NOW\n\n**${task}**\n\nStarted: ${new Date().toLocaleString()}\n`
143
+ // MANDATORY: Assign agent before setting task
144
+ const agentResult = await this._assignAgentForTask(task, projectPath, context)
145
+ const agent = agentResult.agent?.name || 'generalist'
146
+ const confidence = agentResult.routing?.confidence || 0.5
147
+
148
+ // Set task WITH agent
149
+ const nowContent = `# NOW\n\n**${task}**\n\nStarted: ${new Date().toLocaleString()}\nAgent: ${agent} (${Math.round(confidence * 100)}% confidence)\n`
143
150
  await toolRegistry.get('Write')(context.paths.now, nowContent)
144
151
 
145
- out.done(`${task} (started)`)
152
+ out.done(`${task} [${agent}]`)
146
153
 
147
154
  await this.logToMemory(projectPath, 'task_started', {
148
155
  task,
156
+ agent,
157
+ confidence,
149
158
  timestamp: dateHelper.getTimestamp(),
150
159
  })
151
- return { success: true, task }
160
+ return { success: true, task, agent }
152
161
  } else {
153
162
  // Show current task
154
163
  const nowContent = await toolRegistry.get('Read')(context.paths.now)
@@ -158,10 +167,12 @@ class PrjctCommands {
158
167
  return { success: true, message: 'No active task' }
159
168
  }
160
169
 
161
- // Extract task name for minimal output
170
+ // Extract task name and agent for minimal output
162
171
  const taskMatch = nowContent.match(/\*\*(.+?)\*\*/)
172
+ const agentMatch = nowContent.match(/Agent: ([^\s(]+)/)
163
173
  const currentTask = taskMatch ? taskMatch[1] : 'unknown'
164
- out.done(`working on: ${currentTask}`)
174
+ const currentAgent = agentMatch ? agentMatch[1] : ''
175
+ out.done(`working on: ${currentTask}${currentAgent ? ` [${currentAgent}]` : ''}`)
165
176
  return { success: true, content: nowContent }
166
177
  }
167
178
  } catch (error) {
@@ -427,26 +438,42 @@ class PrjctCommands {
427
438
  // Task breakdown
428
439
  const tasks = this._breakdownFeatureTasks(description)
429
440
 
430
- // Write to next.md
441
+ // MANDATORY: Assign agent to each task
442
+ const tasksWithAgents = []
443
+ for (const taskDesc of tasks) {
444
+ const agentResult = await this._assignAgentForTask(taskDesc, projectPath, context)
445
+ const agent = agentResult.agent?.name || 'generalist'
446
+ tasksWithAgents.push({ task: taskDesc, agent })
447
+ }
448
+
449
+ // Write to next.md with agents
431
450
  const nextContent =
432
451
  (await toolRegistry.get('Read')(context.paths.next)) || '# NEXT\n\n## Priority Queue\n\n'
433
452
  const taskSection =
434
453
  `\n## Feature: ${description}\n\n` +
435
- tasks.map((t, i) => `${i + 1}. [ ] ${t}`).join('\n') +
454
+ tasksWithAgents.map((t, i) => `${i + 1}. [${t.agent}] [ ] ${t.task}`).join('\n') +
436
455
  `\n\nEstimated: ${tasks.length * 2}h\n`
437
456
 
438
457
  await toolRegistry.get('Write')(context.paths.next, nextContent + taskSection)
439
458
 
440
- // Log to memory
459
+ // Log to memory with agent assignments
441
460
  await this.logToMemory(projectPath, 'feature_planned', {
442
461
  feature: description,
443
- tasks: tasks.length,
462
+ tasks: tasksWithAgents.length,
463
+ assignments: tasksWithAgents.map(t => ({ task: t.task, agent: t.agent })),
444
464
  timestamp: dateHelper.getTimestamp(),
445
465
  })
446
466
 
447
- out.done(`${tasks.length} tasks created`)
467
+ // Show summary with agent distribution
468
+ const agentCounts = tasksWithAgents.reduce((acc, t) => {
469
+ acc[t.agent] = (acc[t.agent] || 0) + 1
470
+ return acc
471
+ }, {})
472
+ const agentSummary = Object.entries(agentCounts).map(([a, c]) => `${a}:${c}`).join(' ')
473
+
474
+ out.done(`${tasks.length} tasks [${agentSummary}]`)
448
475
 
449
- return { success: true, feature: description, tasks }
476
+ return { success: true, feature: description, tasks: tasksWithAgents }
450
477
  } catch (error) {
451
478
  out.fail(error.message)
452
479
  return { success: false, error: error.message }
@@ -488,10 +515,14 @@ class PrjctCommands {
488
515
  const context = await contextBuilder.build(projectPath, { description })
489
516
  const severity = this._detectBugSeverity(description)
490
517
 
491
- // Add to next.md with priority
518
+ // MANDATORY: Assign agent to bug
519
+ const agentResult = await this._assignAgentForTask(`fix bug: ${description}`, projectPath, context)
520
+ const agent = agentResult.agent?.name || 'generalist'
521
+
522
+ // Add to next.md with priority and agent
492
523
  const nextContent =
493
524
  (await toolRegistry.get('Read')(context.paths.next)) || '# NEXT\n\n## Priority Queue\n\n'
494
- const bugEntry = `\n## 🐛 BUG [${severity.toUpperCase()}]: ${description}\n\nReported: ${new Date().toLocaleString()}\nPriority: ${severity === 'critical' ? '⚠️ URGENT' : severity === 'high' ? '🔴 High' : '🟡 Normal'}\n`
525
+ const bugEntry = `\n## 🐛 BUG [${severity.toUpperCase()}] [${agent}]: ${description}\n\nReported: ${new Date().toLocaleString()}\nPriority: ${severity === 'critical' ? '⚠️ URGENT' : severity === 'high' ? '🔴 High' : '🟡 Normal'}\nAssigned: ${agent}\n`
495
526
 
496
527
  // Insert at top if critical/high, at bottom otherwise
497
528
  const updatedContent =
@@ -501,16 +532,17 @@ class PrjctCommands {
501
532
 
502
533
  await toolRegistry.get('Write')(context.paths.next, updatedContent)
503
534
 
504
- // Log to memory
535
+ // Log to memory with agent
505
536
  await this.logToMemory(projectPath, 'bug_reported', {
506
537
  bug: description,
507
538
  severity,
539
+ agent,
508
540
  timestamp: dateHelper.getTimestamp(),
509
541
  })
510
542
 
511
- out.done(`bug [${severity}] tracked`)
543
+ out.done(`bug [${severity}] → ${agent}`)
512
544
 
513
- return { success: true, bug: description, severity }
545
+ return { success: true, bug: description, severity, agent }
514
546
  } catch (error) {
515
547
  out.fail(error.message)
516
548
  return { success: false, error: error.message }
@@ -1378,9 +1410,11 @@ Status: ⏸️ Planned
1378
1410
  console.log(` Estimated: ${estimate}h`)
1379
1411
  console.log(` Type: ${complexity.type}\n`)
1380
1412
 
1381
- // Auto-assign agent (simplified)
1382
- const agent = this._autoAssignAgent(task)
1383
- console.log(`🤖 Agent: ${agent}\n`)
1413
+ // MANDATORY: Assign agent using router
1414
+ const agentResult = await this._assignAgentForTask(task, projectPath, context)
1415
+ const agent = agentResult.agent?.name || 'generalist'
1416
+ const confidence = agentResult.routing?.confidence || 0.5
1417
+ console.log(`🤖 Agent: ${agent} (${Math.round(confidence * 100)}% confidence)\n`)
1384
1418
 
1385
1419
  // Set as current task with metadata
1386
1420
  const nowContentNew = `# NOW
@@ -1390,7 +1424,7 @@ Status: ⏸️ Planned
1390
1424
  Started: ${new Date().toLocaleString()}
1391
1425
  Estimated: ${estimate}h
1392
1426
  Complexity: ${complexity.level}
1393
- Agent: ${agent}
1427
+ Agent: ${agent} (${Math.round(confidence * 100)}% confidence)
1394
1428
  `
1395
1429
  await toolRegistry.get('Write')(context.paths.now, nowContentNew)
1396
1430
 
@@ -1405,6 +1439,7 @@ Agent: ${agent}
1405
1439
  complexity: complexity.level,
1406
1440
  estimate,
1407
1441
  agent,
1442
+ confidence,
1408
1443
  timestamp: dateHelper.getTimestamp(),
1409
1444
  })
1410
1445
 
@@ -1426,12 +1461,58 @@ Agent: ${agent}
1426
1461
  }
1427
1462
 
1428
1463
  /**
1429
- * Auto-assign agent based on task
1464
+ * Assign agent for a task
1465
+ * AGENTIC: Claude decides via templates/agent-assignment.md
1466
+ * JS only orchestrates: load agents → build context → delegate to Claude
1467
+ * @private
1468
+ */
1469
+ async _assignAgentForTask(taskDescription, projectPath, context) {
1470
+ try {
1471
+ const projectId = await configManager.getProjectId(projectPath)
1472
+
1473
+ // ORCHESTRATION ONLY: Load available agents
1474
+ const agentsPath = pathManager.getPath(projectId, 'agents')
1475
+ const agentFiles = await fileHelper.listFiles(agentsPath, '.md')
1476
+ const agents = agentFiles.map(f => f.replace('.md', ''))
1477
+
1478
+ // ORCHESTRATION ONLY: Build context for Claude
1479
+ const assignmentContext = {
1480
+ task: taskDescription,
1481
+ agents: agents.join(', ') || 'generalist',
1482
+ projectPath,
1483
+ // Claude will use this context + template to decide
1484
+ }
1485
+
1486
+ // AGENTIC: Claude decides agent via template
1487
+ // The template templates/agent-assignment.md guides Claude's decision
1488
+ // For now, return structure that prompt-builder will use with template
1489
+ return {
1490
+ agent: { name: agents[0] || 'generalist', domain: 'auto' },
1491
+ routing: {
1492
+ confidence: 0.8,
1493
+ reason: 'Claude assigns via templates/agent-assignment.md',
1494
+ availableAgents: agents
1495
+ },
1496
+ _agenticNote: 'Use templates/agent-assignment.md for actual assignment'
1497
+ }
1498
+ } catch (error) {
1499
+ // Fallback - still return structure
1500
+ return {
1501
+ agent: { name: 'generalist', domain: 'general' },
1502
+ routing: { confidence: 0.5, reason: 'Fallback - no agents found' }
1503
+ }
1504
+ }
1505
+ }
1506
+
1507
+ /**
1508
+ * Auto-assign agent based on task (sync wrapper for backward compat)
1509
+ * DEPRECATED: Use _assignAgentForTask instead
1430
1510
  * @private
1431
1511
  */
1432
1512
  _autoAssignAgent(task) {
1433
- // AGENTIC: Agent assignment handled by agent-router.js with semantic analysis
1434
- // Returns default - real routing happens via MandatoryAgentRouter
1513
+ // For backward compatibility, return generalist synchronously
1514
+ // New code should use _assignAgentForTask() which is async
1515
+ console.warn('DEPRECATED: Use _assignAgentForTask() for proper agent routing')
1435
1516
  return 'generalist'
1436
1517
  }
1437
1518
 
@@ -1,217 +1,103 @@
1
1
  /**
2
- * AgentMatcher - Intelligent Agent Matching with Scoring
3
- *
4
- * Matches tasks to agents using multi-factor scoring:
5
- * - Agent skills and capabilities
6
- * - Historical success rates
7
- * - Project technologies
8
- * - Task complexity
9
- *
10
- * @version 1.0.0
2
+ * AgentMatcher - Orchestration Only
3
+ *
4
+ * AGENTIC: All matching decisions made by Claude via templates/agent-assignment.md
5
+ * JS only orchestrates: format data, pass to Claude, return result
6
+ *
7
+ * NO scoring logic, NO algorithms, NO hardcoded weights
8
+ *
9
+ * @version 2.0.0
11
10
  */
12
11
 
12
+ const fs = require('fs').promises
13
+ const path = require('path')
14
+
13
15
  class AgentMatcher {
14
16
  constructor() {
15
- this.historyCache = new Map()
16
- }
17
-
18
- /**
19
- * Find best agent for a task using intelligent scoring
20
- * @param {Array<Object>} availableAgents - All available agents
21
- * @param {Object} taskAnalysis - Task analysis result
22
- * @returns {Object|null} Best matching agent with score
23
- */
24
- findBestAgent(availableAgents, taskAnalysis) {
25
- if (!availableAgents || availableAgents.length === 0) {
26
- return null
27
- }
28
-
29
- // Score each agent
30
- const scored = availableAgents.map(agent => {
31
- const score = this.scoreAgent(agent, taskAnalysis)
32
- return { agent, score }
33
- })
34
-
35
- // Sort by score descending
36
- scored.sort((a, b) => b.score - a.score)
37
-
38
- // Return best match if score is above threshold
39
- const best = scored[0]
40
- if (best && best.score > 0.3) {
41
- return {
42
- agent: best.agent,
43
- score: best.score,
44
- alternatives: scored.slice(1, 3).map(s => ({
45
- agent: s.agent,
46
- score: s.score
47
- }))
48
- }
49
- }
50
-
51
- return null
52
- }
53
-
54
- /**
55
- * Score an agent for a specific task
56
- * Multi-factor scoring system
57
- */
58
- scoreAgent(agent, taskAnalysis) {
59
- let score = 0
60
-
61
- // Factor 1: Domain Match (40% weight)
62
- const domainScore = this.scoreDomainMatch(agent, taskAnalysis)
63
- score += domainScore * 0.4
64
-
65
- // Factor 2: Skills Match (30% weight)
66
- const skillsScore = this.scoreSkillsMatch(agent, taskAnalysis)
67
- score += skillsScore * 0.3
68
-
69
- // Factor 3: Historical Success (20% weight)
70
- const historyScore = this.scoreHistoricalSuccess(agent, taskAnalysis)
71
- score += historyScore * 0.2
72
-
73
- // Factor 4: Complexity Match (10% weight)
74
- const complexityScore = this.scoreComplexityMatch(agent, taskAnalysis)
75
- score += complexityScore * 0.1
76
-
77
- return Math.min(score, 1.0)
78
- }
79
-
80
- /**
81
- * Score domain match
82
- */
83
- scoreDomainMatch(agent, taskAnalysis) {
84
- const agentDomain = agent.domain || ''
85
- const taskDomain = taskAnalysis.primaryDomain || ''
86
-
87
- // Exact match
88
- if (agentDomain === taskDomain) {
89
- return 1.0
90
- }
91
-
92
- // Partial match (agent name contains domain)
93
- if (agent.name && agent.name.includes(taskDomain)) {
94
- return 0.7
95
- }
96
-
97
- // Check alternatives
98
- if (taskAnalysis.alternatives && taskAnalysis.alternatives.includes(agentDomain)) {
99
- return 0.5
100
- }
101
-
102
- return 0.1
17
+ this.historyPath = null
103
18
  }
104
19
 
105
20
  /**
106
- * Score skills match
21
+ * Set history path for logging
22
+ * ORCHESTRATION: Path setup only
107
23
  */
108
- scoreSkillsMatch(agent, taskAnalysis) {
109
- if (!agent.skills || agent.skills.length === 0) {
110
- return 0.2 // Generic agent penalty
111
- }
112
-
113
- const projectTech = taskAnalysis.projectTechnologies || {}
114
- const allTech = [
115
- ...(projectTech.languages || []),
116
- ...(projectTech.frameworks || []),
117
- ...(projectTech.tools || [])
118
- ]
119
-
120
- // Count matching skills
121
- const matchingSkills = agent.skills.filter(skill => {
122
- const skillLower = skill.toLowerCase()
123
- return allTech.some(tech => tech.toLowerCase().includes(skillLower) ||
124
- skillLower.includes(tech.toLowerCase()))
125
- })
126
-
127
- if (matchingSkills.length === 0) {
128
- return 0.1 // No matching skills
129
- }
130
-
131
- // Score based on match ratio
132
- const matchRatio = matchingSkills.length / Math.max(agent.skills.length, allTech.length)
133
- return Math.min(matchRatio * 2, 1.0) // Boost for good matches
24
+ setHistoryPath(projectId) {
25
+ this.historyPath = path.join(
26
+ process.env.HOME,
27
+ '.prjct-cli',
28
+ 'projects',
29
+ projectId,
30
+ 'agent-history.jsonl'
31
+ )
134
32
  }
135
33
 
136
34
  /**
137
- * Score historical success
35
+ * Format agents for Claude
36
+ * ORCHESTRATION: Data formatting only
138
37
  */
139
- scoreHistoricalSuccess(agent, taskAnalysis) {
140
- // TODO: Load from persistent history
141
- // For now, return neutral score
142
- const cacheKey = `${agent.name}-${taskAnalysis.primaryDomain}`
143
- const history = this.historyCache.get(cacheKey)
144
-
145
- if (history) {
146
- // Success rate from history
147
- return history.successRate || 0.5
148
- }
149
-
150
- return 0.5 // Neutral - no history
38
+ formatAgentsForTemplate(agents) {
39
+ return agents.map(a => ({
40
+ name: a.name,
41
+ domain: a.domain || 'general',
42
+ hasContent: !!a.content
43
+ }))
151
44
  }
152
45
 
153
46
  /**
154
- * Score complexity match
47
+ * Format task for Claude
48
+ * ORCHESTRATION: Data formatting only
155
49
  */
156
- scoreComplexityMatch(agent, taskAnalysis) {
157
- const taskComplexity = taskAnalysis.complexity || 'medium'
158
-
159
- // Generic agents are better for simple tasks
160
- // Specialized agents are better for complex tasks
161
- const isGeneric = !agent.skills || agent.skills.length === 0
162
-
163
- if (taskComplexity === 'low' && isGeneric) {
164
- return 0.8
165
- }
166
-
167
- if (taskComplexity === 'high' && !isGeneric) {
168
- return 0.9
50
+ formatTaskForTemplate(task) {
51
+ return {
52
+ description: typeof task === 'string' ? task : task.description,
53
+ type: task.type || 'unknown'
169
54
  }
170
-
171
- return 0.5 // Neutral
172
55
  }
173
56
 
174
57
  /**
175
- * Record agent success for learning
58
+ * Record agent usage
59
+ * ORCHESTRATION: File I/O only
176
60
  */
177
- recordSuccess(agent, taskAnalysis, success = true) {
178
- const cacheKey = `${agent.name}-${taskAnalysis.primaryDomain}`
179
- const history = this.historyCache.get(cacheKey) || {
180
- attempts: 0,
181
- successes: 0,
182
- successRate: 0.5
183
- }
184
-
185
- history.attempts++
186
- if (success) {
187
- history.successes++
61
+ async recordUsage(agent, task) {
62
+ if (!this.historyPath) return
63
+
64
+ try {
65
+ const entry = JSON.stringify({
66
+ timestamp: new Date().toISOString(),
67
+ agent: agent.name || agent,
68
+ task: typeof task === 'string' ? task : task.description
69
+ }) + '\n'
70
+
71
+ await fs.appendFile(this.historyPath, entry)
72
+ } catch {
73
+ // Silent fail
188
74
  }
189
- history.successRate = history.successes / history.attempts
190
-
191
- this.historyCache.set(cacheKey, history)
192
75
  }
193
76
 
194
77
  /**
195
- * Get match explanation
78
+ * Load usage history
79
+ * ORCHESTRATION: File I/O only
196
80
  */
197
- explainMatch(match) {
198
- if (!match) {
199
- return 'No suitable agent found'
200
- }
201
-
202
- const reasons = []
203
-
204
- if (match.score > 0.8) {
205
- reasons.push('Excellent match')
206
- } else if (match.score > 0.6) {
207
- reasons.push('Good match')
208
- } else {
209
- reasons.push('Acceptable match')
81
+ async loadHistory() {
82
+ if (!this.historyPath) return []
83
+
84
+ try {
85
+ const content = await fs.readFile(this.historyPath, 'utf-8')
86
+ return content
87
+ .split('\n')
88
+ .filter(Boolean)
89
+ .map(line => {
90
+ try {
91
+ return JSON.parse(line)
92
+ } catch {
93
+ return null
94
+ }
95
+ })
96
+ .filter(Boolean)
97
+ } catch {
98
+ return []
210
99
  }
211
-
212
- return reasons.join(', ')
213
100
  }
214
101
  }
215
102
 
216
103
  module.exports = AgentMatcher
217
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.10.11",
3
+ "version": "0.10.12",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: agent-assignment
3
+ description: Assign the best agent for a task
4
+ allowed-tools: [Read]
5
+ ---
6
+
7
+ # Agent Assignment
8
+
9
+ Select the best agent for the given task based on semantic understanding.
10
+
11
+ ## Input
12
+
13
+ - **Task**: {{task}}
14
+ - **Available Agents**: {{agents}}
15
+ - **Project Context**: {{context}}
16
+
17
+ ## Instructions
18
+
19
+ 1. **Understand the Task**
20
+ - What domain does this task belong to?
21
+ - What skills are required?
22
+ - What is the complexity level?
23
+
24
+ 2. **Analyze Available Agents**
25
+ - Read each agent's expertise and domain
26
+ - Consider their skills and past success
27
+ - Match capabilities to task requirements
28
+
29
+ 3. **Select Best Agent**
30
+ - Choose the agent with highest relevance
31
+ - If multiple agents fit, prefer the specialist over generalist
32
+ - If no good match, use 'generalist' or 'full-stack'
33
+
34
+ ## Decision Criteria
35
+
36
+ - **Domain Match**: Does the agent's domain align with the task?
37
+ - **Skills Match**: Does the agent have the required skills?
38
+ - **Complexity Fit**: Is the agent appropriate for this complexity level?
39
+
40
+ ## Output Format
41
+
42
+ Return JSON with your decision:
43
+
44
+ ```json
45
+ {
46
+ "agent": "agent-name",
47
+ "confidence": 0.85,
48
+ "reason": "Brief explanation of why this agent was selected",
49
+ "domain": "detected domain of the task"
50
+ }
51
+ ```
52
+
53
+ ## Examples
54
+
55
+ **Task**: "Implement React login component with form validation"
56
+ **Decision**: `{ "agent": "frontend-specialist", "confidence": 0.95, "reason": "React component work requires frontend expertise", "domain": "frontend" }`
57
+
58
+ **Task**: "Fix database connection timeout issue"
59
+ **Decision**: `{ "agent": "backend-specialist", "confidence": 0.90, "reason": "Database issues require backend/infrastructure knowledge", "domain": "backend" }`
60
+
61
+ **Task**: "Write unit tests for user service"
62
+ **Decision**: `{ "agent": "qa-specialist", "confidence": 0.85, "reason": "Testing tasks benefit from QA expertise", "domain": "qa" }`
63
+
64
+ **Task**: "Update README documentation"
65
+ **Decision**: `{ "agent": "generalist", "confidence": 0.70, "reason": "Documentation is general task, no specialist needed", "domain": "docs" }`
66
+
67
+ ## Guidelines
68
+
69
+ - Prefer specialists when task clearly fits their domain
70
+ - Use generalist only when no specialist matches
71
+ - Higher confidence = stronger match
72
+ - Always provide a reason for transparency