prjct-cli 0.10.11 → 0.10.13

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
@@ -15,6 +15,9 @@
15
15
  * Source: Claude Code, Devin, Augment Code patterns
16
16
  */
17
17
 
18
+ const fs = require('fs')
19
+ const path = require('path')
20
+ const os = require('os')
18
21
  const templateLoader = require('./template-loader')
19
22
  const contextBuilder = require('./context-builder')
20
23
  const promptBuilder = require('./prompt-builder')
@@ -32,6 +35,9 @@ const groundTruth = require('./ground-truth')
32
35
  const thinkBlocks = require('./think-blocks')
33
36
  const parallelTools = require('./parallel-tools')
34
37
  const planMode = require('./plan-mode')
38
+
39
+ // Running file for status line integration
40
+ const RUNNING_FILE = path.join(os.homedir(), '.prjct-cli', '.running')
35
41
  // P3.5, P3.6, P3.7: DELEGATED TO CLAUDE CODE
36
42
  // - semantic-search → Claude Code has Grep/Glob with semantic understanding
37
43
  // - code-intelligence → Claude Code has native LSP integration
@@ -44,16 +50,48 @@ class CommandExecutor {
44
50
  this.contextEstimator = null
45
51
  }
46
52
 
53
+ /**
54
+ * Signal that a command is running (for status line)
55
+ */
56
+ signalStart(commandName) {
57
+ try {
58
+ const dir = path.dirname(RUNNING_FILE)
59
+ if (!fs.existsSync(dir)) {
60
+ fs.mkdirSync(dir, { recursive: true })
61
+ }
62
+ fs.writeFileSync(RUNNING_FILE, `/p:${commandName}`)
63
+ } catch {
64
+ // Silently ignore - status line is optional
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Signal that command has finished (for status line)
70
+ */
71
+ signalEnd() {
72
+ try {
73
+ if (fs.existsSync(RUNNING_FILE)) {
74
+ fs.unlinkSync(RUNNING_FILE)
75
+ }
76
+ } catch {
77
+ // Silently ignore - status line is optional
78
+ }
79
+ }
80
+
47
81
  /**
48
82
  * Execute command with MANDATORY agent assignment
49
83
  */
50
84
  async execute(commandName, params, projectPath) {
85
+ // Signal start for status line
86
+ this.signalStart(commandName)
87
+
51
88
  // Context for loop detection
52
89
  const loopContext = params.task || params.description || ''
53
90
 
54
91
  // Check if we're in a loop BEFORE attempting
55
92
  if (loopDetector.shouldEscalate(commandName, loopContext)) {
56
93
  const escalation = loopDetector.getEscalationInfo(commandName, loopContext)
94
+ this.signalEnd()
57
95
  return {
58
96
  success: false,
59
97
  error: escalation.message,
@@ -73,6 +111,7 @@ class CommandExecutor {
73
111
  // 2.5. VALIDATE: Pre-flight checks with specific errors
74
112
  const validation = await validate(commandName, metadataContext)
75
113
  if (!validation.valid) {
114
+ this.signalEnd()
76
115
  return {
77
116
  success: false,
78
117
  error: formatError(validation),
@@ -267,6 +306,9 @@ class CommandExecutor {
267
306
  // Record successful attempt
268
307
  loopDetector.recordSuccess(commandName, loopContext)
269
308
 
309
+ // Signal end for status line
310
+ this.signalEnd()
311
+
270
312
  return {
271
313
  success: true,
272
314
  template,
@@ -334,6 +376,9 @@ class CommandExecutor {
334
376
  // - Native LSP for code intelligence
335
377
  }
336
378
  } catch (error) {
379
+ // Signal end for status line
380
+ this.signalEnd()
381
+
337
382
  // Record failed attempt for loop detection
338
383
  const attemptInfo = loopDetector.recordAttempt(commandName, loopContext, {
339
384
  success: false,