aios-core 3.7.0 → 3.9.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.
Files changed (53) hide show
  1. package/.aios-core/core/session/context-detector.js +3 -0
  2. package/.aios-core/core/session/context-loader.js +154 -0
  3. package/.aios-core/data/learned-patterns.yaml +3 -0
  4. package/.aios-core/data/workflow-patterns.yaml +347 -3
  5. package/.aios-core/development/agents/dev.md +6 -0
  6. package/.aios-core/development/agents/squad-creator.md +30 -0
  7. package/.aios-core/development/scripts/squad/squad-analyzer.js +638 -0
  8. package/.aios-core/development/scripts/squad/squad-extender.js +871 -0
  9. package/.aios-core/development/scripts/squad/squad-generator.js +107 -19
  10. package/.aios-core/development/scripts/squad/squad-migrator.js +3 -5
  11. package/.aios-core/development/scripts/squad/squad-validator.js +98 -0
  12. package/.aios-core/development/tasks/next.md +294 -0
  13. package/.aios-core/development/tasks/patterns.md +334 -0
  14. package/.aios-core/development/tasks/squad-creator-analyze.md +315 -0
  15. package/.aios-core/development/tasks/squad-creator-create.md +26 -3
  16. package/.aios-core/development/tasks/squad-creator-extend.md +411 -0
  17. package/.aios-core/development/tasks/squad-creator-validate.md +9 -1
  18. package/.aios-core/development/tasks/waves.md +205 -0
  19. package/.aios-core/development/templates/squad/agent-template.md +69 -0
  20. package/.aios-core/development/templates/squad/checklist-template.md +82 -0
  21. package/.aios-core/development/templates/squad/data-template.yaml +105 -0
  22. package/.aios-core/development/templates/squad/script-template.js +179 -0
  23. package/.aios-core/development/templates/squad/task-template.md +125 -0
  24. package/.aios-core/development/templates/squad/template-template.md +97 -0
  25. package/.aios-core/development/templates/squad/tool-template.js +103 -0
  26. package/.aios-core/development/templates/squad/workflow-template.yaml +108 -0
  27. package/.aios-core/infrastructure/scripts/test-generator.js +8 -8
  28. package/.aios-core/infrastructure/scripts/test-quality-assessment.js +5 -5
  29. package/.aios-core/infrastructure/scripts/test-utilities.js +3 -3
  30. package/.aios-core/install-manifest.yaml +97 -33
  31. package/.aios-core/quality/metrics-collector.js +27 -0
  32. package/.aios-core/scripts/session-context-loader.js +13 -254
  33. package/.aios-core/scripts/test-template-system.js +6 -6
  34. package/.aios-core/utils/aios-validator.js +25 -0
  35. package/.aios-core/workflow-intelligence/__tests__/confidence-scorer.test.js +334 -0
  36. package/.aios-core/workflow-intelligence/__tests__/integration.test.js +337 -0
  37. package/.aios-core/workflow-intelligence/__tests__/suggestion-engine.test.js +431 -0
  38. package/.aios-core/workflow-intelligence/__tests__/wave-analyzer.test.js +458 -0
  39. package/.aios-core/workflow-intelligence/__tests__/workflow-registry.test.js +302 -0
  40. package/.aios-core/workflow-intelligence/engine/confidence-scorer.js +305 -0
  41. package/.aios-core/workflow-intelligence/engine/output-formatter.js +285 -0
  42. package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +603 -0
  43. package/.aios-core/workflow-intelligence/engine/wave-analyzer.js +676 -0
  44. package/.aios-core/workflow-intelligence/index.js +327 -0
  45. package/.aios-core/workflow-intelligence/learning/capture-hook.js +147 -0
  46. package/.aios-core/workflow-intelligence/learning/index.js +230 -0
  47. package/.aios-core/workflow-intelligence/learning/pattern-capture.js +340 -0
  48. package/.aios-core/workflow-intelligence/learning/pattern-store.js +498 -0
  49. package/.aios-core/workflow-intelligence/learning/pattern-validator.js +309 -0
  50. package/.aios-core/workflow-intelligence/registry/workflow-registry.js +358 -0
  51. package/package.json +1 -1
  52. package/src/installer/brownfield-upgrader.js +1 -1
  53. package/bin/aios-init.backup-v1.1.4.js +0 -352
@@ -1,265 +1,24 @@
1
1
  /**
2
- * Session Context Loader - Multi-Agent Session Continuity
2
+ * Session Context Loader - Re-export from canonical location
3
3
  *
4
- * Provides session context when transitioning between agents.
5
- * Solves the problem: "@po approved story, @dev doesn't know about it"
4
+ * @deprecated Use require('../core/session/context-loader') directly
5
+ * @module scripts/session-context-loader
6
6
  *
7
- * Features:
8
- * - Detects previous agent in session
9
- * - Tracks last N commands
10
- * - Identifies active workflow
11
- * - Provides natural language summary for new agent
7
+ * This file re-exports SessionContextLoader from its canonical location
8
+ * in core/session/context-loader.js for backward compatibility.
12
9
  *
13
- * Part of Story 6.1.2.5 UX Improvements
10
+ * Migration note (WIS-3):
11
+ * - Canonical location: .aios-core/core/session/context-loader.js
12
+ * - This file exists for backward compatibility with existing imports
13
+ * - New code should import from core/session/context-loader directly
14
14
  */
15
15
 
16
- const fs = require('fs');
17
- const path = require('path');
18
- const ContextDetector = require('../core/session/context-detector');
16
+ 'use strict';
19
17
 
20
- const SESSION_STATE_PATH = path.join(process.cwd(), '.aios', 'session-state.json');
21
- const MAX_COMMANDS_HISTORY = 10;
18
+ // Re-export from canonical location
19
+ const SessionContextLoader = require('../core/session/context-loader');
22
20
 
23
- class SessionContextLoader {
24
- constructor() {
25
- this.detector = new ContextDetector();
26
- this.sessionStatePath = SESSION_STATE_PATH;
27
- }
28
-
29
- /**
30
- * Load session context for current agent activation
31
- *
32
- * @param {string} currentAgentId - ID of agent being activated
33
- * @returns {Object} Session context
34
- */
35
- loadContext(currentAgentId) {
36
- // Pass sessionStatePath to detector so it uses the correct file (important for testing)
37
- const sessionType = this.detector.detectSessionType([], this.sessionStatePath);
38
- const sessionState = this.loadSessionState();
39
-
40
- if (sessionType === 'new') {
41
- // Fresh session - no context
42
- return {
43
- sessionType: 'new',
44
- message: null,
45
- previousAgent: null,
46
- lastCommands: [],
47
- workflowActive: null,
48
- };
49
- }
50
-
51
- // Extract context information
52
- const previousAgent = this.getPreviousAgent(sessionState, currentAgentId);
53
- const lastCommands = sessionState.lastCommands || [];
54
- const workflowActive = sessionState.workflowActive || null;
55
-
56
- // Generate natural language summary
57
- const message = this.generateContextMessage({
58
- sessionType,
59
- previousAgent,
60
- lastCommands,
61
- workflowActive,
62
- currentAgentId,
63
- });
64
-
65
- return {
66
- sessionType,
67
- message,
68
- previousAgent,
69
- lastCommands,
70
- workflowActive,
71
- currentStory: sessionState.currentStory || null,
72
- sessionId: sessionState.sessionId,
73
- sessionStartTime: sessionState.startTime,
74
- };
75
- }
76
-
77
- /**
78
- * Load session state from file
79
- *
80
- * @returns {Object} Session state
81
- */
82
- loadSessionState() {
83
- try {
84
- if (!fs.existsSync(this.sessionStatePath)) {
85
- return {};
86
- }
87
-
88
- const content = fs.readFileSync(this.sessionStatePath, 'utf8');
89
- return JSON.parse(content);
90
- } catch (error) {
91
- console.warn('[SessionContext] Failed to load session state:', error.message);
92
- return {};
93
- }
94
- }
95
-
96
- /**
97
- * Get previous agent from session
98
- *
99
- * @param {Object} sessionState - Session state
100
- * @param {string} currentAgentId - Current agent ID
101
- * @returns {Object|null} Previous agent info
102
- */
103
- getPreviousAgent(sessionState, currentAgentId) {
104
- const agentSequence = sessionState.agentSequence || [];
105
-
106
- if (agentSequence.length === 0) {
107
- return null;
108
- }
109
-
110
- // Get last agent that's different from current
111
- for (let i = agentSequence.length - 1; i >= 0; i--) {
112
- const agent = agentSequence[i];
113
- if (agent.agentId !== currentAgentId) {
114
- return {
115
- agentId: agent.agentId,
116
- agentName: agent.agentName,
117
- activatedAt: agent.activatedAt,
118
- lastCommand: agent.lastCommand,
119
- };
120
- }
121
- }
122
-
123
- return null;
124
- }
125
-
126
- /**
127
- * Generate natural language context message
128
- *
129
- * @param {Object} context - Context data
130
- * @returns {string|null} Context message
131
- */
132
- generateContextMessage(context) {
133
- const { sessionType, previousAgent, lastCommands, workflowActive } = context;
134
-
135
- if (sessionType === 'new') {
136
- return null;
137
- }
138
-
139
- const parts = [];
140
-
141
- // Previous agent context
142
- if (previousAgent) {
143
- const agentName = previousAgent.agentName || previousAgent.agentId;
144
- const minutesAgo = Math.floor((Date.now() - previousAgent.activatedAt) / 60000);
145
- const timeAgo = minutesAgo < 1 ? 'just now' : minutesAgo === 1 ? '1 minute ago' : `${minutesAgo} minutes ago`;
146
-
147
- parts.push(`📍 **Session Context**: Continuing from @${previousAgent.agentId} (${agentName}) activated ${timeAgo}`);
148
-
149
- if (previousAgent.lastCommand) {
150
- parts.push(` Last action: *${previousAgent.lastCommand}`);
151
- }
152
- }
153
-
154
- // Recent commands
155
- if (lastCommands.length > 0) {
156
- const recentCmds = lastCommands.slice(-5).join(', *');
157
- parts.push(` Recent commands: *${recentCmds}`);
158
- }
159
-
160
- // Active workflow
161
- if (workflowActive) {
162
- parts.push(` ⚡ Active Workflow: ${workflowActive}`);
163
- }
164
-
165
- return parts.length > 0 ? parts.join('\n') : null;
166
- }
167
-
168
- /**
169
- * Update session state with current agent
170
- *
171
- * @param {string} agentId - Agent ID
172
- * @param {string} agentName - Agent name
173
- * @param {string} lastCommand - Last command executed
174
- * @param {Object} options - Update options
175
- */
176
- updateSession(agentId, agentName, lastCommand = null, options = {}) {
177
- try {
178
- const sessionState = this.loadSessionState();
179
-
180
- // Initialize if new session
181
- if (!sessionState.sessionId) {
182
- sessionState.sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
183
- sessionState.startTime = Date.now();
184
- }
185
-
186
- // Update activity timestamp
187
- sessionState.lastActivity = Date.now();
188
-
189
- // Update agent sequence
190
- if (!sessionState.agentSequence) {
191
- sessionState.agentSequence = [];
192
- }
193
-
194
- sessionState.agentSequence.push({
195
- agentId,
196
- agentName,
197
- activatedAt: Date.now(),
198
- lastCommand,
199
- });
200
-
201
- // Keep last 20 agent activations
202
- if (sessionState.agentSequence.length > 20) {
203
- sessionState.agentSequence = sessionState.agentSequence.slice(-20);
204
- }
205
-
206
- // Update command history
207
- if (lastCommand) {
208
- if (!sessionState.lastCommands) {
209
- sessionState.lastCommands = [];
210
- }
211
-
212
- sessionState.lastCommands.push(lastCommand);
213
-
214
- // Keep last N commands
215
- if (sessionState.lastCommands.length > MAX_COMMANDS_HISTORY) {
216
- sessionState.lastCommands = sessionState.lastCommands.slice(-MAX_COMMANDS_HISTORY);
217
- }
218
- }
219
-
220
- // Update workflow status
221
- if (options.workflowActive !== undefined) {
222
- sessionState.workflowActive = options.workflowActive;
223
- }
224
-
225
- // Save to file
226
- this.detector.updateSessionState(sessionState, this.sessionStatePath);
227
- } catch (error) {
228
- console.warn('[SessionContext] Failed to update session:', error.message);
229
- }
230
- }
231
-
232
- /**
233
- * Clear session (start fresh)
234
- */
235
- clearSession() {
236
- try {
237
- if (fs.existsSync(this.sessionStatePath)) {
238
- fs.unlinkSync(this.sessionStatePath);
239
- }
240
- } catch (error) {
241
- console.warn('[SessionContext] Failed to clear session:', error.message);
242
- }
243
- }
244
-
245
- /**
246
- * Format context for display in agent greeting
247
- *
248
- * @param {string} currentAgentId - Current agent ID
249
- * @returns {string} Formatted context message
250
- */
251
- formatForGreeting(currentAgentId) {
252
- const context = this.loadContext(currentAgentId);
253
-
254
- if (!context.message) {
255
- return '';
256
- }
257
-
258
- return `\n${context.message}\n`;
259
- }
260
- }
261
-
262
- // CLI Interface
21
+ // CLI Interface (preserved for backward compatibility)
263
22
  if (require.main === module) {
264
23
  const loader = new SessionContextLoader();
265
24
  const command = process.argv[2];
@@ -34,7 +34,7 @@ class TestTemplateSystem {
34
34
  console.log(chalk.green('✅ Test template system initialized'));
35
35
  return true;
36
36
 
37
- } catch (_error) {
37
+ } catch (error) {
38
38
  console.error(chalk.red(`Failed to initialize test template system: ${error.message}`));
39
39
  throw error;
40
40
  }
@@ -121,7 +121,7 @@ class TestTemplateSystem {
121
121
  console.log(chalk.green(`✅ Custom template created: ${templateName}`));
122
122
  return templateWrapper;
123
123
 
124
- } catch (_error) {
124
+ } catch (error) {
125
125
  console.error(chalk.red(`Failed to create custom template: ${error.message}`));
126
126
  throw error;
127
127
  }
@@ -136,7 +136,7 @@ class TestTemplateSystem {
136
136
  try {
137
137
  const content = await fs.readFile(templatePath, 'utf-8');
138
138
  return JSON.parse(content);
139
- } catch (_error) {
139
+ } catch {
140
140
  // Template file doesn't exist
141
141
  return null;
142
142
  }
@@ -837,14 +837,14 @@ afterAll(async () => {
837
837
  const templateKey = path.basename(templateFile, '.template.js');
838
838
 
839
839
  this.templateCache.set(templateKey, template);
840
- } catch (_error) {
840
+ } catch (error) {
841
841
  console.warn(chalk.yellow(`Failed to load template ${templateFile}: ${error.message}`));
842
842
  }
843
843
  }
844
844
 
845
845
  console.log(chalk.gray(`Loaded ${this.templateCache.size} template(s)`));
846
846
 
847
- } catch (_error) {
847
+ } catch (error) {
848
848
  console.warn(chalk.yellow(`Failed to load templates: ${error.message}`));
849
849
  }
850
850
  }
@@ -872,7 +872,7 @@ afterAll(async () => {
872
872
  templateFiles.push(path.join(this.templatesDir, entry.name));
873
873
  }
874
874
  }
875
- } catch (_error) {
875
+ } catch {
876
876
  // Templates directory doesn't exist yet
877
877
  }
878
878
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * AIOS Validator - Re-export from canonical location
3
+ *
4
+ * @deprecated Use require('../infrastructure/scripts/aios-validator') directly
5
+ * @module utils/aios-validator
6
+ *
7
+ * This file re-exports from the canonical location in infrastructure/scripts/
8
+ * for backward compatibility with CI workflows.
9
+ *
10
+ * Migration note:
11
+ * - Canonical location: .aios-core/infrastructure/scripts/aios-validator.js
12
+ * - This file exists for backward compatibility with existing CI workflows
13
+ * - New code should import from infrastructure/scripts/aios-validator directly
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ // Re-export from canonical location
19
+ module.exports = require('../infrastructure/scripts/aios-validator');
20
+
21
+ // CLI Interface - delegate to canonical location
22
+ if (require.main === module) {
23
+ // Pass through to the original script
24
+ require('../infrastructure/scripts/aios-validator');
25
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * @fileoverview Unit tests for ConfidenceScorer
3
+ * @story WIS-2 - Workflow Registry Enhancement
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const {
9
+ ConfidenceScorer,
10
+ createConfidenceScorer,
11
+ SCORING_WEIGHTS
12
+ } = require('../engine/confidence-scorer');
13
+
14
+ describe('ConfidenceScorer', () => {
15
+ let scorer;
16
+
17
+ beforeEach(() => {
18
+ scorer = createConfidenceScorer();
19
+ });
20
+
21
+ describe('constructor', () => {
22
+ it('should create scorer with default weights', () => {
23
+ const weights = scorer.getWeights();
24
+ expect(weights.COMMAND_MATCH).toBe(0.40);
25
+ expect(weights.AGENT_MATCH).toBe(0.25);
26
+ expect(weights.HISTORY_DEPTH).toBe(0.20);
27
+ expect(weights.PROJECT_STATE).toBe(0.15);
28
+ });
29
+
30
+ it('should accept custom weights', () => {
31
+ const customScorer = createConfidenceScorer({
32
+ weights: {
33
+ COMMAND_MATCH: 0.50,
34
+ AGENT_MATCH: 0.20,
35
+ HISTORY_DEPTH: 0.15,
36
+ PROJECT_STATE: 0.15
37
+ }
38
+ });
39
+ const weights = customScorer.getWeights();
40
+ expect(weights.COMMAND_MATCH).toBe(0.50);
41
+ });
42
+
43
+ it('should throw error if weights do not sum to 1.0', () => {
44
+ expect(() => {
45
+ createConfidenceScorer({
46
+ weights: {
47
+ COMMAND_MATCH: 0.50,
48
+ AGENT_MATCH: 0.50,
49
+ HISTORY_DEPTH: 0.20,
50
+ PROJECT_STATE: 0.15
51
+ }
52
+ });
53
+ }).toThrow('Scoring weights must sum to 1.0');
54
+ });
55
+ });
56
+
57
+ describe('score()', () => {
58
+ it('should return 0 for null suggestion', () => {
59
+ const result = scorer.score(null, { lastCommand: 'test' });
60
+ expect(result).toBe(0);
61
+ });
62
+
63
+ it('should return 0 for null context', () => {
64
+ const result = scorer.score({ trigger: 'test' }, null);
65
+ expect(result).toBe(0);
66
+ });
67
+
68
+ it('should return high score for exact command match', () => {
69
+ const suggestion = {
70
+ trigger: 'create-epic',
71
+ agentSequence: ['po', 'sm'],
72
+ keyCommands: ['create-epic', 'create-story']
73
+ };
74
+ const context = {
75
+ lastCommand: 'create-epic',
76
+ lastCommands: ['create-epic'],
77
+ agentId: '@po',
78
+ projectState: {}
79
+ };
80
+
81
+ const result = scorer.score(suggestion, context);
82
+ // Score is composed of: command(40%) + agent(25%) + history(20%) + state(15%)
83
+ expect(result).toBeGreaterThanOrEqual(0.70);
84
+ });
85
+
86
+ it('should return medium score for partial command match', () => {
87
+ const suggestion = {
88
+ trigger: 'create-epic',
89
+ agentSequence: ['po', 'sm'],
90
+ keyCommands: ['create-epic', 'create-story']
91
+ };
92
+ const context = {
93
+ lastCommand: 'create-story',
94
+ lastCommands: ['create-story'],
95
+ agentId: '@sm',
96
+ projectState: {}
97
+ };
98
+
99
+ const result = scorer.score(suggestion, context);
100
+ expect(result).toBeGreaterThan(0.40);
101
+ expect(result).toBeLessThan(0.80);
102
+ });
103
+
104
+ it('should return low score for no command match', () => {
105
+ const suggestion = {
106
+ trigger: 'create-epic',
107
+ agentSequence: ['po', 'sm'],
108
+ keyCommands: ['create-epic', 'create-story']
109
+ };
110
+ const context = {
111
+ lastCommand: 'push',
112
+ lastCommands: ['push'],
113
+ agentId: '@devops',
114
+ projectState: {}
115
+ };
116
+
117
+ const result = scorer.score(suggestion, context);
118
+ expect(result).toBeLessThanOrEqual(0.30);
119
+ });
120
+
121
+ it('should return normalized score between 0 and 1', () => {
122
+ const suggestion = { trigger: 'any', agentSequence: [], keyCommands: [] };
123
+ const context = { lastCommand: 'any', lastCommands: [], agentId: '' };
124
+
125
+ const result = scorer.score(suggestion, context);
126
+ expect(result).toBeGreaterThanOrEqual(0);
127
+ expect(result).toBeLessThanOrEqual(1);
128
+ });
129
+ });
130
+
131
+ describe('matchCommand()', () => {
132
+ it('should return 1.0 for exact match', () => {
133
+ const result = scorer.matchCommand('create-epic', 'create-epic');
134
+ expect(result).toBe(1.0);
135
+ });
136
+
137
+ it('should return 1.0 when normalized trigger equals command', () => {
138
+ // "create-epic completed" normalizes to "create-epic"
139
+ const result = scorer.matchCommand('create-epic completed', 'create-epic');
140
+ expect(result).toBe(1.0);
141
+ });
142
+
143
+ it('should return partial score for keyword match', () => {
144
+ const result = scorer.matchCommand('create-epic', 'create-story');
145
+ // Both share "create" word, so partial match
146
+ expect(result).toBeGreaterThan(0);
147
+ expect(result).toBeLessThan(1);
148
+ });
149
+
150
+ it('should return 0 for no match', () => {
151
+ const result = scorer.matchCommand('create-epic', 'push');
152
+ expect(result).toBe(0);
153
+ });
154
+
155
+ it('should handle null trigger', () => {
156
+ const result = scorer.matchCommand(null, 'test');
157
+ expect(result).toBe(0);
158
+ });
159
+
160
+ it('should handle null command', () => {
161
+ const result = scorer.matchCommand('test', null);
162
+ expect(result).toBe(0);
163
+ });
164
+ });
165
+
166
+ describe('matchAgent()', () => {
167
+ it('should return high score for first agent in sequence', () => {
168
+ const result = scorer.matchAgent(['po', 'sm', 'dev'], '@po');
169
+ expect(result).toBeGreaterThan(0.6);
170
+ });
171
+
172
+ it('should return higher score for later agents', () => {
173
+ const result1 = scorer.matchAgent(['po', 'sm', 'dev'], '@po');
174
+ const result2 = scorer.matchAgent(['po', 'sm', 'dev'], '@dev');
175
+ expect(result2).toBeGreaterThan(result1);
176
+ });
177
+
178
+ it('should return 0 for agent not in sequence', () => {
179
+ const result = scorer.matchAgent(['po', 'sm'], '@devops');
180
+ expect(result).toBe(0);
181
+ });
182
+
183
+ it('should handle agent with @ prefix', () => {
184
+ const result = scorer.matchAgent(['po'], '@po');
185
+ expect(result).toBeGreaterThan(0);
186
+ });
187
+
188
+ it('should handle null agent sequence', () => {
189
+ const result = scorer.matchAgent(null, '@po');
190
+ expect(result).toBe(0);
191
+ });
192
+ });
193
+
194
+ describe('matchHistory()', () => {
195
+ it('should return high score when history matches key commands', () => {
196
+ const keyCommands = ['create-epic', 'create-story'];
197
+ const history = ['create-epic', 'create-story', 'validate'];
198
+
199
+ const result = scorer.matchHistory(keyCommands, history);
200
+ expect(result).toBeGreaterThan(0.8);
201
+ });
202
+
203
+ it('should give recency bonus for recent commands', () => {
204
+ const keyCommands = ['create-epic'];
205
+ const historyRecent = ['create-epic', 'other', 'another'];
206
+ const historyOld = ['other', 'another', 'create-epic'];
207
+
208
+ const resultRecent = scorer.matchHistory(keyCommands, historyRecent);
209
+ const resultOld = scorer.matchHistory(keyCommands, historyOld);
210
+
211
+ // Recent should get higher score due to recency bonus
212
+ expect(resultRecent).toBeGreaterThanOrEqual(resultOld);
213
+ });
214
+
215
+ it('should return 0 for no matching commands', () => {
216
+ const keyCommands = ['create-epic'];
217
+ const history = ['push', 'commit'];
218
+
219
+ const result = scorer.matchHistory(keyCommands, history);
220
+ expect(result).toBe(0);
221
+ });
222
+
223
+ it('should handle empty history', () => {
224
+ const result = scorer.matchHistory(['test'], []);
225
+ expect(result).toBe(0);
226
+ });
227
+
228
+ it('should handle empty key commands', () => {
229
+ const result = scorer.matchHistory([], ['test']);
230
+ expect(result).toBe(0);
231
+ });
232
+ });
233
+
234
+ describe('matchProjectState()', () => {
235
+ it('should return neutral score for empty state', () => {
236
+ const result = scorer.matchProjectState({}, {});
237
+ expect(result).toBe(0.5);
238
+ });
239
+
240
+ it('should boost score for git-related suggestions when uncommitted changes', () => {
241
+ const suggestion = { trigger: 'git commit' };
242
+ const state = { hasUncommittedChanges: true };
243
+
244
+ const result = scorer.matchProjectState(suggestion, state);
245
+ expect(result).toBeGreaterThan(0.5);
246
+ });
247
+
248
+ it('should boost score for test suggestions when tests failing', () => {
249
+ const suggestion = { trigger: 'run tests' };
250
+ const state = { failingTests: true };
251
+
252
+ const result = scorer.matchProjectState(suggestion, state);
253
+ expect(result).toBeGreaterThan(0.5);
254
+ });
255
+
256
+ it('should return 0.5 for null project state', () => {
257
+ const result = scorer.matchProjectState({}, null);
258
+ expect(result).toBe(0.5);
259
+ });
260
+ });
261
+
262
+ describe('normalizeCommand()', () => {
263
+ it('should convert to lowercase', () => {
264
+ const result = scorer.normalizeCommand('CREATE-EPIC');
265
+ expect(result).toBe('create-epic');
266
+ });
267
+
268
+ it('should remove "completed" suffix', () => {
269
+ const result = scorer.normalizeCommand('create-epic completed');
270
+ expect(result).toBe('create-epic');
271
+ });
272
+
273
+ it('should remove "successfully" suffix', () => {
274
+ const result = scorer.normalizeCommand('create-epic successfully');
275
+ expect(result).toBe('create-epic');
276
+ });
277
+
278
+ it('should remove * prefix', () => {
279
+ const result = scorer.normalizeCommand('*create-epic');
280
+ expect(result).toBe('create-epic');
281
+ });
282
+
283
+ it('should handle null', () => {
284
+ const result = scorer.normalizeCommand(null);
285
+ expect(result).toBe('');
286
+ });
287
+ });
288
+
289
+ describe('rankSuggestions()', () => {
290
+ it('should return empty array for empty suggestions', () => {
291
+ const result = scorer.rankSuggestions([], {});
292
+ expect(result).toEqual([]);
293
+ });
294
+
295
+ it('should rank suggestions by score descending', () => {
296
+ const suggestions = [
297
+ { trigger: 'push', agentSequence: ['devops'] },
298
+ { trigger: 'create-epic', agentSequence: ['po'] }
299
+ ];
300
+ const context = {
301
+ lastCommand: 'create-epic',
302
+ lastCommands: ['create-epic'],
303
+ agentId: '@po'
304
+ };
305
+
306
+ const result = scorer.rankSuggestions(suggestions, context);
307
+ expect(result[0].trigger).toBe('create-epic');
308
+ expect(result[0].score).toBeGreaterThan(result[1].score);
309
+ });
310
+
311
+ it('should add score property to each suggestion', () => {
312
+ const suggestions = [{ trigger: 'test' }];
313
+ const context = { lastCommand: 'test' };
314
+
315
+ const result = scorer.rankSuggestions(suggestions, context);
316
+ expect(result[0]).toHaveProperty('score');
317
+ expect(typeof result[0].score).toBe('number');
318
+ });
319
+ });
320
+
321
+ describe('SCORING_WEIGHTS constant', () => {
322
+ it('should have correct default weights', () => {
323
+ expect(SCORING_WEIGHTS.COMMAND_MATCH).toBe(0.40);
324
+ expect(SCORING_WEIGHTS.AGENT_MATCH).toBe(0.25);
325
+ expect(SCORING_WEIGHTS.HISTORY_DEPTH).toBe(0.20);
326
+ expect(SCORING_WEIGHTS.PROJECT_STATE).toBe(0.15);
327
+ });
328
+
329
+ it('should sum to 1.0', () => {
330
+ const sum = Object.values(SCORING_WEIGHTS).reduce((a, b) => a + b, 0);
331
+ expect(sum).toBe(1.0);
332
+ });
333
+ });
334
+ });