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
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module SuggestionEngine
|
|
3
|
+
* @description High-level suggestion engine for the *next task
|
|
4
|
+
* @story WIS-3 - *next Task Implementation
|
|
5
|
+
* @story WIS-5 - Pattern Capture Integration
|
|
6
|
+
* @version 1.1.0
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const { SuggestionEngine } = require('./suggestion-engine');
|
|
10
|
+
* const engine = new SuggestionEngine();
|
|
11
|
+
*
|
|
12
|
+
* const context = await engine.buildContext({ storyOverride: 'path/to/story.md' });
|
|
13
|
+
* const result = await engine.suggestNext(context);
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
// Lazy-loaded dependencies for performance
|
|
22
|
+
let wis = null;
|
|
23
|
+
let SessionContextLoader = null;
|
|
24
|
+
let learning = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default cache TTL for suggestions (5 minutes)
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
const SUGGESTION_CACHE_TTL = 5 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Low confidence threshold for marking suggestions as "uncertain"
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
const LOW_CONFIDENCE_THRESHOLD = 0.50;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SuggestionEngine class for generating context-aware command suggestions
|
|
40
|
+
*/
|
|
41
|
+
class SuggestionEngine {
|
|
42
|
+
/**
|
|
43
|
+
* Create a SuggestionEngine instance
|
|
44
|
+
* @param {Object} options - Configuration options
|
|
45
|
+
* @param {number} options.cacheTTL - Cache time-to-live in milliseconds
|
|
46
|
+
* @param {boolean} options.lazyLoad - Whether to lazy-load dependencies
|
|
47
|
+
* @param {boolean} options.useLearnedPatterns - Whether to use learned patterns (default: true)
|
|
48
|
+
* @param {number} options.learnedPatternBoost - Confidence boost for learned patterns (default: 0.15)
|
|
49
|
+
*/
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
this.cacheTTL = options.cacheTTL || SUGGESTION_CACHE_TTL;
|
|
52
|
+
this.lazyLoad = options.lazyLoad !== false;
|
|
53
|
+
this.useLearnedPatterns = options.useLearnedPatterns !== false;
|
|
54
|
+
this.learnedPatternBoost = options.learnedPatternBoost || 0.15;
|
|
55
|
+
this.suggestionCache = null;
|
|
56
|
+
this.cacheTimestamp = null;
|
|
57
|
+
this.cacheKey = null;
|
|
58
|
+
|
|
59
|
+
// Load dependencies immediately if not lazy loading
|
|
60
|
+
if (!this.lazyLoad) {
|
|
61
|
+
this._loadDependencies();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Lazy-load WIS and session dependencies
|
|
67
|
+
* @private
|
|
68
|
+
*/
|
|
69
|
+
_loadDependencies() {
|
|
70
|
+
if (!wis) {
|
|
71
|
+
try {
|
|
72
|
+
wis = require('../index');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn('[SuggestionEngine] Failed to load WIS module:', error.message);
|
|
75
|
+
wis = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!SessionContextLoader) {
|
|
80
|
+
try {
|
|
81
|
+
SessionContextLoader = require('../../core/session/context-loader');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn('[SuggestionEngine] Failed to load SessionContextLoader:', error.message);
|
|
84
|
+
SessionContextLoader = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!learning && this.useLearnedPatterns) {
|
|
89
|
+
try {
|
|
90
|
+
learning = require('../learning');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn('[SuggestionEngine] Failed to load learning module:', error.message);
|
|
93
|
+
learning = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build context from multiple sources
|
|
100
|
+
* @param {Object} options - Context building options
|
|
101
|
+
* @param {string} options.storyOverride - Explicit story path (optional)
|
|
102
|
+
* @param {boolean} options.autoDetect - Whether to auto-detect context (default: true)
|
|
103
|
+
* @param {string} options.agentId - Current agent ID (optional)
|
|
104
|
+
* @returns {Promise<Object>} Built context object
|
|
105
|
+
*/
|
|
106
|
+
async buildContext(options = {}) {
|
|
107
|
+
this._loadDependencies();
|
|
108
|
+
|
|
109
|
+
const context = {
|
|
110
|
+
agentId: options.agentId || this._detectCurrentAgent(),
|
|
111
|
+
lastCommand: null,
|
|
112
|
+
lastCommands: [],
|
|
113
|
+
storyPath: null,
|
|
114
|
+
branch: null,
|
|
115
|
+
projectState: {}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// 1. Load session context if available
|
|
119
|
+
if (options.autoDetect !== false && SessionContextLoader) {
|
|
120
|
+
try {
|
|
121
|
+
const loader = new SessionContextLoader();
|
|
122
|
+
const sessionContext = loader.loadContext(context.agentId);
|
|
123
|
+
|
|
124
|
+
context.lastCommands = sessionContext.lastCommands || [];
|
|
125
|
+
context.lastCommand = context.lastCommands[context.lastCommands.length - 1] || null;
|
|
126
|
+
context.storyPath = sessionContext.currentStory || null;
|
|
127
|
+
context.workflowActive = sessionContext.workflowActive || null;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn('[SuggestionEngine] Failed to load session context:', error.message);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Story override takes precedence
|
|
134
|
+
if (options.storyOverride) {
|
|
135
|
+
const resolvedPath = this._resolveStoryPath(options.storyOverride);
|
|
136
|
+
if (resolvedPath) {
|
|
137
|
+
context.storyPath = resolvedPath;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 3. Detect git branch
|
|
142
|
+
context.branch = this._detectGitBranch();
|
|
143
|
+
|
|
144
|
+
// 4. Build project state
|
|
145
|
+
context.projectState = await this._buildProjectState(context);
|
|
146
|
+
|
|
147
|
+
return context;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get suggestions for next commands based on context
|
|
152
|
+
* @param {Object} context - Current session context
|
|
153
|
+
* @returns {Promise<Object>} Suggestion result
|
|
154
|
+
*/
|
|
155
|
+
async suggestNext(context) {
|
|
156
|
+
this._loadDependencies();
|
|
157
|
+
|
|
158
|
+
// Check cache first
|
|
159
|
+
const cacheKey = this._generateCacheKey(context);
|
|
160
|
+
if (this._isCacheValid(cacheKey)) {
|
|
161
|
+
return this.suggestionCache;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Default result for when WIS is not available
|
|
165
|
+
const defaultResult = {
|
|
166
|
+
workflow: null,
|
|
167
|
+
currentState: null,
|
|
168
|
+
confidence: 0,
|
|
169
|
+
suggestions: [],
|
|
170
|
+
isUncertain: true,
|
|
171
|
+
message: 'Unable to determine workflow context'
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (!wis) {
|
|
175
|
+
return defaultResult;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// Get suggestions from WIS
|
|
180
|
+
const suggestions = wis.getSuggestions(context);
|
|
181
|
+
|
|
182
|
+
if (!suggestions || suggestions.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
...defaultResult,
|
|
185
|
+
message: 'No matching workflow found for current context'
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get workflow match info
|
|
190
|
+
const commands = context.lastCommands || (context.lastCommand ? [context.lastCommand] : []);
|
|
191
|
+
const match = wis.matchWorkflow(commands);
|
|
192
|
+
|
|
193
|
+
// Calculate overall confidence
|
|
194
|
+
const avgConfidence = suggestions.length > 0
|
|
195
|
+
? suggestions.reduce((sum, s) => sum + (s.confidence || 0), 0) / suggestions.length
|
|
196
|
+
: 0;
|
|
197
|
+
|
|
198
|
+
// Format base suggestions
|
|
199
|
+
let formattedSuggestions = suggestions.map((s, index) => ({
|
|
200
|
+
command: `*${s.command}`,
|
|
201
|
+
args: this._interpolateArgs(s.args_template, context),
|
|
202
|
+
description: s.description || '',
|
|
203
|
+
confidence: Math.round((s.confidence || 0) * 100) / 100,
|
|
204
|
+
priority: s.priority || index + 1,
|
|
205
|
+
source: 'workflow'
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
// Apply learned pattern boost (WIS-5)
|
|
209
|
+
if (this.useLearnedPatterns && learning) {
|
|
210
|
+
formattedSuggestions = this._applyLearnedPatternBoost(
|
|
211
|
+
formattedSuggestions,
|
|
212
|
+
context
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Re-sort after boost
|
|
217
|
+
formattedSuggestions.sort((a, b) => b.confidence - a.confidence);
|
|
218
|
+
|
|
219
|
+
// Recalculate average confidence
|
|
220
|
+
const finalAvgConfidence = formattedSuggestions.length > 0
|
|
221
|
+
? formattedSuggestions.reduce((sum, s) => sum + s.confidence, 0) / formattedSuggestions.length
|
|
222
|
+
: 0;
|
|
223
|
+
|
|
224
|
+
// Build result
|
|
225
|
+
const result = {
|
|
226
|
+
workflow: match?.name || suggestions[0]?.workflow || null,
|
|
227
|
+
currentState: suggestions[0]?.state || null,
|
|
228
|
+
confidence: Math.round(finalAvgConfidence * 100) / 100,
|
|
229
|
+
suggestions: formattedSuggestions,
|
|
230
|
+
isUncertain: finalAvgConfidence < LOW_CONFIDENCE_THRESHOLD,
|
|
231
|
+
message: null
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Cache the result
|
|
235
|
+
this._cacheResult(cacheKey, result);
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('[SuggestionEngine] Error getting suggestions:', error.message);
|
|
240
|
+
return {
|
|
241
|
+
...defaultResult,
|
|
242
|
+
message: `Error: ${error.message}`
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Detect current active agent from environment or session
|
|
249
|
+
* @returns {string} Agent ID
|
|
250
|
+
* @private
|
|
251
|
+
*/
|
|
252
|
+
_detectCurrentAgent() {
|
|
253
|
+
// Check environment variable first
|
|
254
|
+
if (process.env.AIOS_CURRENT_AGENT) {
|
|
255
|
+
return process.env.AIOS_CURRENT_AGENT.replace('@', '');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Default to 'dev' if unknown
|
|
259
|
+
return 'dev';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Resolve and validate story path
|
|
264
|
+
* @param {string} storyPath - Story path to resolve
|
|
265
|
+
* @returns {string|null} Resolved path or null if invalid
|
|
266
|
+
* @private
|
|
267
|
+
*/
|
|
268
|
+
_resolveStoryPath(storyPath) {
|
|
269
|
+
if (!storyPath) return null;
|
|
270
|
+
|
|
271
|
+
// Handle relative paths
|
|
272
|
+
const resolved = path.isAbsolute(storyPath)
|
|
273
|
+
? storyPath
|
|
274
|
+
: path.resolve(process.cwd(), storyPath);
|
|
275
|
+
|
|
276
|
+
// Validate file exists
|
|
277
|
+
try {
|
|
278
|
+
if (fs.existsSync(resolved)) {
|
|
279
|
+
return resolved;
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// File doesn't exist
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.warn(`[SuggestionEngine] Story path not found: ${storyPath}`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Detect current git branch
|
|
291
|
+
* @returns {string|null} Branch name or null
|
|
292
|
+
* @private
|
|
293
|
+
*/
|
|
294
|
+
_detectGitBranch() {
|
|
295
|
+
try {
|
|
296
|
+
const gitHeadPath = path.join(process.cwd(), '.git', 'HEAD');
|
|
297
|
+
if (fs.existsSync(gitHeadPath)) {
|
|
298
|
+
const content = fs.readFileSync(gitHeadPath, 'utf8').trim();
|
|
299
|
+
if (content.startsWith('ref: refs/heads/')) {
|
|
300
|
+
return content.replace('ref: refs/heads/', '');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// Git not available or not a git repo
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Build project state object
|
|
311
|
+
* @param {Object} context - Current context
|
|
312
|
+
* @returns {Promise<Object>} Project state
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
async _buildProjectState(context) {
|
|
316
|
+
const state = {
|
|
317
|
+
activeStory: !!context.storyPath,
|
|
318
|
+
hasUncommittedChanges: false,
|
|
319
|
+
failingTests: false,
|
|
320
|
+
workflowPhase: null
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Check for uncommitted changes
|
|
324
|
+
try {
|
|
325
|
+
const gitStatusPath = path.join(process.cwd(), '.git', 'index');
|
|
326
|
+
if (fs.existsSync(gitStatusPath)) {
|
|
327
|
+
// Simple check - if there are modified files in git status
|
|
328
|
+
// For production, would use git status command
|
|
329
|
+
state.hasUncommittedChanges = true; // Assume true for now
|
|
330
|
+
}
|
|
331
|
+
} catch (error) {
|
|
332
|
+
// Ignore
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Infer workflow phase from last command
|
|
336
|
+
if (context.lastCommand) {
|
|
337
|
+
const cmd = context.lastCommand.toLowerCase();
|
|
338
|
+
if (cmd.includes('develop') || cmd.includes('implement')) {
|
|
339
|
+
state.workflowPhase = 'development';
|
|
340
|
+
} else if (cmd.includes('review') || cmd.includes('qa')) {
|
|
341
|
+
state.workflowPhase = 'review';
|
|
342
|
+
} else if (cmd.includes('push') || cmd.includes('pr') || cmd.includes('deploy')) {
|
|
343
|
+
state.workflowPhase = 'deployment';
|
|
344
|
+
} else if (cmd.includes('create') || cmd.includes('story') || cmd.includes('epic')) {
|
|
345
|
+
state.workflowPhase = 'planning';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return state;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Interpolate argument templates with context values
|
|
354
|
+
* @param {string} argsTemplate - Template string with ${var} placeholders
|
|
355
|
+
* @param {Object} context - Context for interpolation
|
|
356
|
+
* @returns {string} Interpolated arguments
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
_interpolateArgs(argsTemplate, context) {
|
|
360
|
+
if (!argsTemplate) return '';
|
|
361
|
+
|
|
362
|
+
return argsTemplate
|
|
363
|
+
.replace(/\$\{story_path\}/g, context.storyPath || '')
|
|
364
|
+
.replace(/\$\{epic_path\}/g, context.epicPath || '')
|
|
365
|
+
.replace(/\$\{doc_path\}/g, context.docPath || '')
|
|
366
|
+
.replace(/\$\{file_path\}/g, context.filePath || '')
|
|
367
|
+
.replace(/\$\{feature_name\}/g, context.featureName || '')
|
|
368
|
+
.replace(/\$\{topic\}/g, context.topic || '')
|
|
369
|
+
.replace(/\$\{branch\}/g, context.branch || '')
|
|
370
|
+
.trim();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Generate cache key from context
|
|
375
|
+
* @param {Object} context - Context object
|
|
376
|
+
* @returns {string} Cache key
|
|
377
|
+
* @private
|
|
378
|
+
*/
|
|
379
|
+
_generateCacheKey(context) {
|
|
380
|
+
const keyParts = [
|
|
381
|
+
context.agentId || '',
|
|
382
|
+
context.lastCommand || '',
|
|
383
|
+
(context.lastCommands || []).slice(-3).join(','),
|
|
384
|
+
context.storyPath || '',
|
|
385
|
+
context.branch || ''
|
|
386
|
+
];
|
|
387
|
+
return keyParts.join('|');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if cache is valid for given key
|
|
392
|
+
* @param {string} key - Cache key
|
|
393
|
+
* @returns {boolean} True if cache is valid
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
396
|
+
_isCacheValid(key) {
|
|
397
|
+
if (!this.suggestionCache || !this.cacheTimestamp || !this.cacheKey) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (this.cacheKey !== key) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return (Date.now() - this.cacheTimestamp) < this.cacheTTL;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Cache suggestion result
|
|
408
|
+
* @param {string} key - Cache key
|
|
409
|
+
* @param {Object} result - Result to cache
|
|
410
|
+
* @private
|
|
411
|
+
*/
|
|
412
|
+
_cacheResult(key, result) {
|
|
413
|
+
this.suggestionCache = result;
|
|
414
|
+
this.cacheTimestamp = Date.now();
|
|
415
|
+
this.cacheKey = key;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Apply learned pattern boost to suggestions
|
|
420
|
+
* @param {Object[]} suggestions - Base suggestions
|
|
421
|
+
* @param {Object} context - Session context
|
|
422
|
+
* @returns {Object[]} Boosted suggestions
|
|
423
|
+
* @private
|
|
424
|
+
*/
|
|
425
|
+
_applyLearnedPatternBoost(suggestions, context) {
|
|
426
|
+
if (!learning) {
|
|
427
|
+
return suggestions;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
// Get commands to match against
|
|
432
|
+
const lastCommands = context.lastCommands || [];
|
|
433
|
+
if (lastCommands.length === 0 && context.lastCommand) {
|
|
434
|
+
lastCommands.push(context.lastCommand);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (lastCommands.length === 0) {
|
|
438
|
+
return suggestions;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Find matching learned patterns
|
|
442
|
+
const matchingPatterns = learning.findMatchingPatterns(lastCommands);
|
|
443
|
+
|
|
444
|
+
if (!matchingPatterns || matchingPatterns.length === 0) {
|
|
445
|
+
return suggestions;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Build a map of command -> boost based on learned patterns
|
|
449
|
+
const boostMap = new Map();
|
|
450
|
+
|
|
451
|
+
for (const pattern of matchingPatterns) {
|
|
452
|
+
// Find the next command in the pattern after the current position
|
|
453
|
+
const patternSeq = pattern.sequence || [];
|
|
454
|
+
const matchIndex = this._findSequencePosition(lastCommands, patternSeq);
|
|
455
|
+
|
|
456
|
+
if (matchIndex >= 0 && matchIndex < patternSeq.length - 1) {
|
|
457
|
+
const nextCommand = patternSeq[matchIndex + 1];
|
|
458
|
+
const currentBoost = boostMap.get(nextCommand) || 0;
|
|
459
|
+
|
|
460
|
+
// Calculate boost based on pattern quality
|
|
461
|
+
const occurrenceBoost = Math.min(pattern.occurrences * 0.02, 0.1);
|
|
462
|
+
const successBoost = (pattern.successRate || 1) * 0.05;
|
|
463
|
+
const similarityBoost = (pattern.similarity || 0.5) * 0.05;
|
|
464
|
+
|
|
465
|
+
const totalBoost = this.learnedPatternBoost + occurrenceBoost + successBoost + similarityBoost;
|
|
466
|
+
boostMap.set(nextCommand, Math.max(currentBoost, totalBoost));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Apply boosts to suggestions
|
|
471
|
+
return suggestions.map(suggestion => {
|
|
472
|
+
const cmdNormalized = suggestion.command.replace(/^\*/, '').toLowerCase();
|
|
473
|
+
const boost = boostMap.get(cmdNormalized) || 0;
|
|
474
|
+
|
|
475
|
+
if (boost > 0) {
|
|
476
|
+
return {
|
|
477
|
+
...suggestion,
|
|
478
|
+
confidence: Math.min(1.0, suggestion.confidence + boost),
|
|
479
|
+
source: 'learned_pattern',
|
|
480
|
+
learnedBoost: Math.round(boost * 100) / 100
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return suggestion;
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.warn('[SuggestionEngine] Failed to apply learned pattern boost:', error.message);
|
|
488
|
+
return suggestions;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Find position of a subsequence in a pattern sequence
|
|
494
|
+
* @param {string[]} subseq - Subsequence to find
|
|
495
|
+
* @param {string[]} pattern - Full pattern sequence
|
|
496
|
+
* @returns {number} End index of match, or -1 if not found
|
|
497
|
+
* @private
|
|
498
|
+
*/
|
|
499
|
+
_findSequencePosition(subseq, pattern) {
|
|
500
|
+
if (!subseq || !pattern || subseq.length === 0) {
|
|
501
|
+
return -1;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Normalize for comparison
|
|
505
|
+
const normalizedSubseq = subseq.map(c => c.toLowerCase().replace(/^\*/, ''));
|
|
506
|
+
const normalizedPattern = pattern.map(c => c.toLowerCase().replace(/^\*/, ''));
|
|
507
|
+
|
|
508
|
+
// Find where the subsequence ends in the pattern
|
|
509
|
+
for (let i = 0; i <= normalizedPattern.length - normalizedSubseq.length; i++) {
|
|
510
|
+
let matches = true;
|
|
511
|
+
for (let j = 0; j < normalizedSubseq.length; j++) {
|
|
512
|
+
if (normalizedPattern[i + j] !== normalizedSubseq[j]) {
|
|
513
|
+
matches = false;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (matches) {
|
|
518
|
+
return i + normalizedSubseq.length - 1;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Try partial match (last command matches)
|
|
523
|
+
const lastCmd = normalizedSubseq[normalizedSubseq.length - 1];
|
|
524
|
+
for (let i = 0; i < normalizedPattern.length; i++) {
|
|
525
|
+
if (normalizedPattern[i] === lastCmd) {
|
|
526
|
+
return i;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return -1;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Invalidate the cache
|
|
535
|
+
*/
|
|
536
|
+
invalidateCache() {
|
|
537
|
+
this.suggestionCache = null;
|
|
538
|
+
this.cacheTimestamp = null;
|
|
539
|
+
this.cacheKey = null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get fallback suggestions when WIS is unavailable
|
|
544
|
+
* @param {Object} context - Context object
|
|
545
|
+
* @returns {Object} Fallback suggestions
|
|
546
|
+
*/
|
|
547
|
+
getFallbackSuggestions(context) {
|
|
548
|
+
const agent = context.agentId || 'dev';
|
|
549
|
+
|
|
550
|
+
// Agent-specific fallback suggestions
|
|
551
|
+
const fallbacks = {
|
|
552
|
+
dev: [
|
|
553
|
+
{ command: '*help', args: '', description: 'Show available commands', confidence: 0.30, priority: 1 },
|
|
554
|
+
{ command: '*run-tests', args: '', description: 'Run test suite', confidence: 0.25, priority: 2 },
|
|
555
|
+
{ command: '*develop', args: '', description: 'Start development mode', confidence: 0.20, priority: 3 }
|
|
556
|
+
],
|
|
557
|
+
po: [
|
|
558
|
+
{ command: '*help', args: '', description: 'Show available commands', confidence: 0.30, priority: 1 },
|
|
559
|
+
{ command: '*backlog-review', args: '', description: 'Review backlog', confidence: 0.25, priority: 2 },
|
|
560
|
+
{ command: '*create-story', args: '', description: 'Create new story', confidence: 0.20, priority: 3 }
|
|
561
|
+
],
|
|
562
|
+
qa: [
|
|
563
|
+
{ command: '*help', args: '', description: 'Show available commands', confidence: 0.30, priority: 1 },
|
|
564
|
+
{ command: '*run-tests', args: '', description: 'Run test suite', confidence: 0.25, priority: 2 },
|
|
565
|
+
{ command: '*review-qa', args: '', description: 'Start QA review', confidence: 0.20, priority: 3 }
|
|
566
|
+
],
|
|
567
|
+
sm: [
|
|
568
|
+
{ command: '*help', args: '', description: 'Show available commands', confidence: 0.30, priority: 1 },
|
|
569
|
+
{ command: '*create-next-story', args: '', description: 'Create next story', confidence: 0.25, priority: 2 },
|
|
570
|
+
{ command: '*validate-story-draft', args: '', description: 'Validate story', confidence: 0.20, priority: 3 }
|
|
571
|
+
],
|
|
572
|
+
default: [
|
|
573
|
+
{ command: '*help', args: '', description: 'Show available commands', confidence: 0.30, priority: 1 },
|
|
574
|
+
{ command: '*status', args: '', description: 'Show project status', confidence: 0.20, priority: 2 }
|
|
575
|
+
]
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
workflow: null,
|
|
580
|
+
currentState: null,
|
|
581
|
+
confidence: 0.25,
|
|
582
|
+
suggestions: fallbacks[agent] || fallbacks.default,
|
|
583
|
+
isUncertain: true,
|
|
584
|
+
message: 'Using fallback suggestions - context unclear'
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Create a new SuggestionEngine instance
|
|
591
|
+
* @param {Object} options - Configuration options
|
|
592
|
+
* @returns {SuggestionEngine} New engine instance
|
|
593
|
+
*/
|
|
594
|
+
function createSuggestionEngine(options = {}) {
|
|
595
|
+
return new SuggestionEngine(options);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
module.exports = {
|
|
599
|
+
SuggestionEngine,
|
|
600
|
+
createSuggestionEngine,
|
|
601
|
+
SUGGESTION_CACHE_TTL,
|
|
602
|
+
LOW_CONFIDENCE_THRESHOLD
|
|
603
|
+
};
|