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.
- package/.aios-core/core/session/context-detector.js +3 -0
- package/.aios-core/core/session/context-loader.js +154 -0
- package/.aios-core/data/learned-patterns.yaml +3 -0
- package/.aios-core/data/workflow-patterns.yaml +347 -3
- package/.aios-core/development/agents/dev.md +6 -0
- package/.aios-core/development/agents/squad-creator.md +30 -0
- package/.aios-core/development/scripts/squad/squad-analyzer.js +638 -0
- package/.aios-core/development/scripts/squad/squad-extender.js +871 -0
- package/.aios-core/development/scripts/squad/squad-generator.js +107 -19
- package/.aios-core/development/scripts/squad/squad-migrator.js +3 -5
- package/.aios-core/development/scripts/squad/squad-validator.js +98 -0
- package/.aios-core/development/tasks/next.md +294 -0
- package/.aios-core/development/tasks/patterns.md +334 -0
- package/.aios-core/development/tasks/squad-creator-analyze.md +315 -0
- package/.aios-core/development/tasks/squad-creator-create.md +26 -3
- package/.aios-core/development/tasks/squad-creator-extend.md +411 -0
- package/.aios-core/development/tasks/squad-creator-validate.md +9 -1
- package/.aios-core/development/tasks/waves.md +205 -0
- package/.aios-core/development/templates/squad/agent-template.md +69 -0
- package/.aios-core/development/templates/squad/checklist-template.md +82 -0
- package/.aios-core/development/templates/squad/data-template.yaml +105 -0
- package/.aios-core/development/templates/squad/script-template.js +179 -0
- package/.aios-core/development/templates/squad/task-template.md +125 -0
- package/.aios-core/development/templates/squad/template-template.md +97 -0
- package/.aios-core/development/templates/squad/tool-template.js +103 -0
- package/.aios-core/development/templates/squad/workflow-template.yaml +108 -0
- package/.aios-core/infrastructure/scripts/test-generator.js +8 -8
- package/.aios-core/infrastructure/scripts/test-quality-assessment.js +5 -5
- package/.aios-core/infrastructure/scripts/test-utilities.js +3 -3
- package/.aios-core/install-manifest.yaml +97 -33
- package/.aios-core/quality/metrics-collector.js +27 -0
- package/.aios-core/scripts/session-context-loader.js +13 -254
- package/.aios-core/scripts/test-template-system.js +6 -6
- package/.aios-core/utils/aios-validator.js +25 -0
- package/.aios-core/workflow-intelligence/__tests__/confidence-scorer.test.js +334 -0
- package/.aios-core/workflow-intelligence/__tests__/integration.test.js +337 -0
- package/.aios-core/workflow-intelligence/__tests__/suggestion-engine.test.js +431 -0
- package/.aios-core/workflow-intelligence/__tests__/wave-analyzer.test.js +458 -0
- package/.aios-core/workflow-intelligence/__tests__/workflow-registry.test.js +302 -0
- package/.aios-core/workflow-intelligence/engine/confidence-scorer.js +305 -0
- package/.aios-core/workflow-intelligence/engine/output-formatter.js +285 -0
- package/.aios-core/workflow-intelligence/engine/suggestion-engine.js +603 -0
- package/.aios-core/workflow-intelligence/engine/wave-analyzer.js +676 -0
- package/.aios-core/workflow-intelligence/index.js +327 -0
- package/.aios-core/workflow-intelligence/learning/capture-hook.js +147 -0
- package/.aios-core/workflow-intelligence/learning/index.js +230 -0
- package/.aios-core/workflow-intelligence/learning/pattern-capture.js +340 -0
- package/.aios-core/workflow-intelligence/learning/pattern-store.js +498 -0
- package/.aios-core/workflow-intelligence/learning/pattern-validator.js +309 -0
- package/.aios-core/workflow-intelligence/registry/workflow-registry.js +358 -0
- package/package.json +1 -1
- package/src/installer/brownfield-upgrader.js +1 -1
- package/bin/aios-init.backup-v1.1.4.js +0 -352
|
@@ -1,265 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session Context Loader -
|
|
2
|
+
* Session Context Loader - Re-export from canonical location
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* @deprecated Use require('../core/session/context-loader') directly
|
|
5
|
+
* @module scripts/session-context-loader
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
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
|
-
*
|
|
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
|
-
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const ContextDetector = require('../core/session/context-detector');
|
|
16
|
+
'use strict';
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
const
|
|
18
|
+
// Re-export from canonical location
|
|
19
|
+
const SessionContextLoader = require('../core/session/context-loader');
|
|
22
20
|
|
|
23
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
+
});
|