midas-mcp 2.2.0 → 2.5.0
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/dist/analyzer.d.ts +14 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +182 -67
- package/dist/analyzer.js.map +1 -1
- package/dist/cli.js +6 -6
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +55 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +336 -0
- package/dist/context.js.map +1 -0
- package/dist/security.d.ts +29 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +72 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -2
- package/dist/server.js.map +1 -1
- package/dist/tests/analyze.test.d.ts +2 -0
- package/dist/tests/analyze.test.d.ts.map +1 -0
- package/dist/tests/analyze.test.js +120 -0
- package/dist/tests/analyze.test.js.map +1 -0
- package/dist/tests/edge-cases.test.d.ts +2 -0
- package/dist/tests/edge-cases.test.d.ts.map +1 -0
- package/dist/tests/edge-cases.test.js +234 -0
- package/dist/tests/edge-cases.test.js.map +1 -0
- package/dist/tests/journal.test.d.ts +2 -0
- package/dist/tests/journal.test.d.ts.map +1 -0
- package/dist/tests/journal.test.js +184 -0
- package/dist/tests/journal.test.js.map +1 -0
- package/dist/tests/metrics.test.d.ts +2 -0
- package/dist/tests/metrics.test.d.ts.map +1 -0
- package/dist/tests/metrics.test.js +178 -0
- package/dist/tests/metrics.test.js.map +1 -0
- package/dist/tests/prompts.test.d.ts +2 -0
- package/dist/tests/prompts.test.d.ts.map +1 -0
- package/dist/tests/prompts.test.js +157 -0
- package/dist/tests/prompts.test.js.map +1 -0
- package/dist/tests/security.test.d.ts +2 -0
- package/dist/tests/security.test.d.ts.map +1 -0
- package/dist/tests/security.test.js +105 -0
- package/dist/tests/security.test.js.map +1 -0
- package/dist/tests/server.test.d.ts +2 -0
- package/dist/tests/server.test.d.ts.map +1 -0
- package/dist/tests/server.test.js +93 -0
- package/dist/tests/server.test.js.map +1 -0
- package/dist/tests/tracker.test.d.ts +2 -0
- package/dist/tests/tracker.test.d.ts.map +1 -0
- package/dist/tests/tracker.test.js +197 -0
- package/dist/tests/tracker.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/journal.d.ts.map +1 -1
- package/dist/tools/journal.js +15 -10
- package/dist/tools/journal.js.map +1 -1
- package/dist/tools/phase.d.ts +1 -0
- package/dist/tools/phase.d.ts.map +1 -1
- package/dist/tools/phase.js +55 -16
- package/dist/tools/phase.js.map +1 -1
- package/dist/tools/tornado.d.ts +2 -2
- package/dist/tools/verify.d.ts +137 -0
- package/dist/tools/verify.d.ts.map +1 -0
- package/dist/tools/verify.js +151 -0
- package/dist/tools/verify.js.map +1 -0
- package/dist/tracker.d.ts +86 -0
- package/dist/tracker.d.ts.map +1 -1
- package/dist/tracker.js +493 -47
- package/dist/tracker.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +131 -15
- package/dist/tui.js.map +1 -1
- package/package.json +2 -3
package/dist/tracker.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
2
2
|
import { join, relative } from 'path';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
+
import { loadState, saveState, getNextPhase } from './state/phase.js';
|
|
5
|
+
import { sanitizePath, isShellSafe } from './security.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
4
7
|
const MIDAS_DIR = '.midas';
|
|
5
8
|
const TRACKER_FILE = 'tracker.json';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// PERSISTENCE
|
|
11
|
+
// ============================================================================
|
|
6
12
|
function getTrackerPath(projectPath) {
|
|
7
13
|
return join(projectPath, MIDAS_DIR, TRACKER_FILE);
|
|
8
14
|
}
|
|
@@ -13,12 +19,17 @@ function ensureDir(projectPath) {
|
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
21
|
export function loadTracker(projectPath) {
|
|
16
|
-
const
|
|
22
|
+
const safePath = sanitizePath(projectPath);
|
|
23
|
+
const path = getTrackerPath(safePath);
|
|
17
24
|
if (existsSync(path)) {
|
|
18
25
|
try {
|
|
19
|
-
|
|
26
|
+
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
27
|
+
// Merge with defaults to handle schema evolution
|
|
28
|
+
return { ...getDefaultTracker(), ...data };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger.error('Failed to parse tracker state', error);
|
|
20
32
|
}
|
|
21
|
-
catch { }
|
|
22
33
|
}
|
|
23
34
|
return getDefaultTracker();
|
|
24
35
|
}
|
|
@@ -39,22 +50,463 @@ function getDefaultTracker() {
|
|
|
39
50
|
},
|
|
40
51
|
inferredPhase: { phase: 'IDLE' },
|
|
41
52
|
confidence: 0,
|
|
53
|
+
// NEW defaults
|
|
54
|
+
gates: {
|
|
55
|
+
compiles: null,
|
|
56
|
+
compiledAt: null,
|
|
57
|
+
testsPass: null,
|
|
58
|
+
testedAt: null,
|
|
59
|
+
lintsPass: null,
|
|
60
|
+
lintedAt: null,
|
|
61
|
+
},
|
|
62
|
+
errorMemory: [],
|
|
63
|
+
currentTask: null,
|
|
64
|
+
suggestionHistory: [],
|
|
65
|
+
fileSnapshot: [],
|
|
66
|
+
lastAnalysis: null,
|
|
42
67
|
};
|
|
43
68
|
}
|
|
44
|
-
//
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// TOOL CALL TRACKING
|
|
71
|
+
// ============================================================================
|
|
45
72
|
export function trackToolCall(projectPath, tool, args) {
|
|
46
|
-
const
|
|
73
|
+
const safePath = sanitizePath(projectPath);
|
|
74
|
+
const tracker = loadTracker(safePath);
|
|
47
75
|
tracker.recentToolCalls = [
|
|
48
76
|
{ tool, timestamp: Date.now(), args },
|
|
49
|
-
...tracker.recentToolCalls.slice(0, 49),
|
|
77
|
+
...tracker.recentToolCalls.slice(0, 49),
|
|
50
78
|
];
|
|
51
|
-
// Update phase based on tool calls
|
|
52
79
|
updatePhaseFromToolCalls(tracker);
|
|
80
|
+
saveTracker(safePath, tracker);
|
|
81
|
+
}
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// VERIFICATION GATES
|
|
84
|
+
// ============================================================================
|
|
85
|
+
export function runVerificationGates(projectPath) {
|
|
86
|
+
const safePath = sanitizePath(projectPath);
|
|
87
|
+
const gates = {
|
|
88
|
+
compiles: null,
|
|
89
|
+
compiledAt: null,
|
|
90
|
+
testsPass: null,
|
|
91
|
+
testedAt: null,
|
|
92
|
+
lintsPass: null,
|
|
93
|
+
lintedAt: null,
|
|
94
|
+
};
|
|
95
|
+
if (!isShellSafe(safePath)) {
|
|
96
|
+
logger.debug('Unsafe path for verification', { path: safePath });
|
|
97
|
+
return gates;
|
|
98
|
+
}
|
|
99
|
+
// Check if package.json exists
|
|
100
|
+
const pkgPath = join(safePath, 'package.json');
|
|
101
|
+
if (!existsSync(pkgPath)) {
|
|
102
|
+
return gates;
|
|
103
|
+
}
|
|
104
|
+
let pkg = {};
|
|
105
|
+
try {
|
|
106
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return gates;
|
|
110
|
+
}
|
|
111
|
+
// Run build if script exists
|
|
112
|
+
if (pkg.scripts?.build) {
|
|
113
|
+
try {
|
|
114
|
+
execSync('npm run build 2>&1', { cwd: safePath, encoding: 'utf-8', timeout: 60000 });
|
|
115
|
+
gates.compiles = true;
|
|
116
|
+
gates.compiledAt = Date.now();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
gates.compiles = false;
|
|
120
|
+
gates.compiledAt = Date.now();
|
|
121
|
+
gates.compileError = error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Run tests if script exists
|
|
125
|
+
if (pkg.scripts?.test) {
|
|
126
|
+
try {
|
|
127
|
+
execSync('npm test 2>&1', { cwd: safePath, encoding: 'utf-8', timeout: 120000 });
|
|
128
|
+
gates.testsPass = true;
|
|
129
|
+
gates.testedAt = Date.now();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
gates.testsPass = false;
|
|
133
|
+
gates.testedAt = Date.now();
|
|
134
|
+
const output = error instanceof Error ? error.message : String(error);
|
|
135
|
+
gates.testError = output.slice(0, 500);
|
|
136
|
+
// Try to extract failed test count
|
|
137
|
+
const failMatch = output.match(/(\d+) fail/i);
|
|
138
|
+
if (failMatch)
|
|
139
|
+
gates.failedTests = parseInt(failMatch[1]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Run lint if script exists
|
|
143
|
+
if (pkg.scripts?.lint) {
|
|
144
|
+
try {
|
|
145
|
+
execSync('npm run lint 2>&1', { cwd: safePath, encoding: 'utf-8', timeout: 30000 });
|
|
146
|
+
gates.lintsPass = true;
|
|
147
|
+
gates.lintedAt = Date.now();
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
gates.lintsPass = false;
|
|
151
|
+
gates.lintedAt = Date.now();
|
|
152
|
+
const output = error instanceof Error ? error.message : String(error);
|
|
153
|
+
// Try to extract error count
|
|
154
|
+
const errorMatch = output.match(/(\d+) error/i);
|
|
155
|
+
if (errorMatch)
|
|
156
|
+
gates.lintErrors = parseInt(errorMatch[1]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Update tracker with gates
|
|
160
|
+
const tracker = loadTracker(safePath);
|
|
161
|
+
tracker.gates = gates;
|
|
162
|
+
saveTracker(safePath, tracker);
|
|
163
|
+
return gates;
|
|
164
|
+
}
|
|
165
|
+
export function getGatesStatus(projectPath) {
|
|
166
|
+
const tracker = loadTracker(projectPath);
|
|
167
|
+
const gates = tracker.gates;
|
|
168
|
+
const failing = [];
|
|
169
|
+
if (gates.compiles === false)
|
|
170
|
+
failing.push('build');
|
|
171
|
+
if (gates.testsPass === false)
|
|
172
|
+
failing.push('tests');
|
|
173
|
+
if (gates.lintsPass === false)
|
|
174
|
+
failing.push('lint');
|
|
175
|
+
// Consider gates stale if older than 10 minutes or if files changed since
|
|
176
|
+
const oldestGate = Math.min(gates.compiledAt || Infinity, gates.testedAt || Infinity, gates.lintedAt || Infinity);
|
|
177
|
+
const stale = oldestGate === Infinity || (Date.now() - oldestGate > 600000);
|
|
178
|
+
return {
|
|
179
|
+
allPass: failing.length === 0 && gates.compiles === true,
|
|
180
|
+
failing,
|
|
181
|
+
stale,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// ERROR MEMORY
|
|
186
|
+
// ============================================================================
|
|
187
|
+
export function recordError(projectPath, error, file, line) {
|
|
188
|
+
const tracker = loadTracker(projectPath);
|
|
189
|
+
// Check if we've seen this error before
|
|
190
|
+
const existing = tracker.errorMemory.find(e => e.error === error && e.file === file && !e.resolved);
|
|
191
|
+
if (existing) {
|
|
192
|
+
existing.lastSeen = Date.now();
|
|
193
|
+
saveTracker(projectPath, tracker);
|
|
194
|
+
return existing;
|
|
195
|
+
}
|
|
196
|
+
// New error
|
|
197
|
+
const newError = {
|
|
198
|
+
id: `err-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
199
|
+
error,
|
|
200
|
+
file,
|
|
201
|
+
line,
|
|
202
|
+
firstSeen: Date.now(),
|
|
203
|
+
lastSeen: Date.now(),
|
|
204
|
+
fixAttempts: [],
|
|
205
|
+
resolved: false,
|
|
206
|
+
};
|
|
207
|
+
tracker.errorMemory = [newError, ...tracker.errorMemory.slice(0, 49)];
|
|
208
|
+
saveTracker(projectPath, tracker);
|
|
209
|
+
return newError;
|
|
210
|
+
}
|
|
211
|
+
export function recordFixAttempt(projectPath, errorId, approach, worked) {
|
|
212
|
+
const tracker = loadTracker(projectPath);
|
|
213
|
+
const error = tracker.errorMemory.find(e => e.id === errorId);
|
|
214
|
+
if (error) {
|
|
215
|
+
error.fixAttempts.push({
|
|
216
|
+
approach,
|
|
217
|
+
timestamp: Date.now(),
|
|
218
|
+
worked,
|
|
219
|
+
});
|
|
220
|
+
if (worked) {
|
|
221
|
+
error.resolved = true;
|
|
222
|
+
}
|
|
223
|
+
saveTracker(projectPath, tracker);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
export function getUnresolvedErrors(projectPath) {
|
|
227
|
+
const tracker = loadTracker(projectPath);
|
|
228
|
+
return tracker.errorMemory.filter(e => !e.resolved);
|
|
229
|
+
}
|
|
230
|
+
export function getStuckErrors(projectPath) {
|
|
231
|
+
const tracker = loadTracker(projectPath);
|
|
232
|
+
return tracker.errorMemory.filter(e => !e.resolved && e.fixAttempts.length >= 2);
|
|
233
|
+
}
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// TASK FOCUS
|
|
236
|
+
// ============================================================================
|
|
237
|
+
export function setTaskFocus(projectPath, description, relatedFiles = []) {
|
|
238
|
+
const tracker = loadTracker(projectPath);
|
|
239
|
+
const task = {
|
|
240
|
+
description,
|
|
241
|
+
startedAt: new Date().toISOString(),
|
|
242
|
+
relatedFiles,
|
|
243
|
+
phase: 'plan',
|
|
244
|
+
attempts: 0,
|
|
245
|
+
};
|
|
246
|
+
tracker.currentTask = task;
|
|
247
|
+
saveTracker(projectPath, tracker);
|
|
248
|
+
return task;
|
|
249
|
+
}
|
|
250
|
+
export function updateTaskPhase(projectPath, phase) {
|
|
251
|
+
const tracker = loadTracker(projectPath);
|
|
252
|
+
if (tracker.currentTask) {
|
|
253
|
+
tracker.currentTask.phase = phase;
|
|
254
|
+
if (phase === 'implement') {
|
|
255
|
+
tracker.currentTask.attempts++;
|
|
256
|
+
}
|
|
257
|
+
saveTracker(projectPath, tracker);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export function clearTaskFocus(projectPath) {
|
|
261
|
+
const tracker = loadTracker(projectPath);
|
|
262
|
+
tracker.currentTask = null;
|
|
53
263
|
saveTracker(projectPath, tracker);
|
|
54
264
|
}
|
|
55
|
-
//
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// SUGGESTION TRACKING
|
|
267
|
+
// ============================================================================
|
|
268
|
+
export function recordSuggestion(projectPath, suggestion) {
|
|
269
|
+
const tracker = loadTracker(projectPath);
|
|
270
|
+
tracker.suggestionHistory = [
|
|
271
|
+
{
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
suggestion,
|
|
274
|
+
accepted: false, // Will be updated when we see what user sends
|
|
275
|
+
},
|
|
276
|
+
...tracker.suggestionHistory.slice(0, 19),
|
|
277
|
+
];
|
|
278
|
+
saveTracker(projectPath, tracker);
|
|
279
|
+
}
|
|
280
|
+
export function recordSuggestionOutcome(projectPath, accepted, userPrompt, rejectionReason) {
|
|
281
|
+
const tracker = loadTracker(projectPath);
|
|
282
|
+
if (tracker.suggestionHistory.length > 0) {
|
|
283
|
+
const latest = tracker.suggestionHistory[0];
|
|
284
|
+
latest.accepted = accepted;
|
|
285
|
+
if (userPrompt)
|
|
286
|
+
latest.userPrompt = userPrompt;
|
|
287
|
+
if (rejectionReason)
|
|
288
|
+
latest.rejectionReason = rejectionReason;
|
|
289
|
+
saveTracker(projectPath, tracker);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export function getSuggestionAcceptanceRate(projectPath) {
|
|
293
|
+
const tracker = loadTracker(projectPath);
|
|
294
|
+
const recent = tracker.suggestionHistory.slice(0, 10);
|
|
295
|
+
if (recent.length === 0)
|
|
296
|
+
return 0;
|
|
297
|
+
const accepted = recent.filter(s => s.accepted).length;
|
|
298
|
+
return Math.round((accepted / recent.length) * 100);
|
|
299
|
+
}
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// FILE CHANGE DETECTION
|
|
302
|
+
// ============================================================================
|
|
303
|
+
export function takeFileSnapshot(projectPath) {
|
|
304
|
+
const files = scanRecentFiles(projectPath, 0);
|
|
305
|
+
return files.map(f => ({
|
|
306
|
+
path: f.path,
|
|
307
|
+
mtime: f.lastModified,
|
|
308
|
+
size: 0, // We don't need size for change detection
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
export function detectFileChanges(projectPath) {
|
|
312
|
+
const tracker = loadTracker(projectPath);
|
|
313
|
+
const oldSnapshot = tracker.fileSnapshot;
|
|
314
|
+
const newSnapshot = takeFileSnapshot(projectPath);
|
|
315
|
+
const oldMap = new Map(oldSnapshot.map(f => [f.path, f]));
|
|
316
|
+
const newMap = new Map(newSnapshot.map(f => [f.path, f]));
|
|
317
|
+
const changed = [];
|
|
318
|
+
const added = [];
|
|
319
|
+
const deleted = [];
|
|
320
|
+
// Find changed and added files
|
|
321
|
+
for (const [path, file] of newMap) {
|
|
322
|
+
const old = oldMap.get(path);
|
|
323
|
+
if (!old) {
|
|
324
|
+
added.push(path);
|
|
325
|
+
}
|
|
326
|
+
else if (old.mtime !== file.mtime) {
|
|
327
|
+
changed.push(path);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Find deleted files
|
|
331
|
+
for (const path of oldMap.keys()) {
|
|
332
|
+
if (!newMap.has(path)) {
|
|
333
|
+
deleted.push(path);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Update snapshot
|
|
337
|
+
tracker.fileSnapshot = newSnapshot;
|
|
338
|
+
saveTracker(projectPath, tracker);
|
|
339
|
+
return { changed, added, deleted };
|
|
340
|
+
}
|
|
341
|
+
export function hasFilesChangedSinceAnalysis(projectPath) {
|
|
342
|
+
const tracker = loadTracker(projectPath);
|
|
343
|
+
if (!tracker.lastAnalysis)
|
|
344
|
+
return true;
|
|
345
|
+
const recentFiles = scanRecentFiles(projectPath, tracker.lastAnalysis);
|
|
346
|
+
return recentFiles.length > 0;
|
|
347
|
+
}
|
|
348
|
+
export function markAnalysisComplete(projectPath) {
|
|
349
|
+
const tracker = loadTracker(projectPath);
|
|
350
|
+
tracker.lastAnalysis = Date.now();
|
|
351
|
+
saveTracker(projectPath, tracker);
|
|
352
|
+
}
|
|
353
|
+
// ============================================================================
|
|
354
|
+
// SMART PROMPT SUGGESTION
|
|
355
|
+
// ============================================================================
|
|
356
|
+
export function getSmartPromptSuggestion(projectPath) {
|
|
357
|
+
const tracker = loadTracker(projectPath);
|
|
358
|
+
const gates = tracker.gates;
|
|
359
|
+
const stuckErrors = getStuckErrors(projectPath);
|
|
360
|
+
const unresolvedErrors = getUnresolvedErrors(projectPath);
|
|
361
|
+
// Priority 1: CRITICAL - Build is broken
|
|
362
|
+
if (gates.compiles === false) {
|
|
363
|
+
return {
|
|
364
|
+
prompt: `Fix the TypeScript compilation errors:\n${gates.compileError || 'Run npm run build to see errors'}`,
|
|
365
|
+
reason: 'Build is failing - must fix before continuing',
|
|
366
|
+
priority: 'critical',
|
|
367
|
+
context: gates.compileError,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// Priority 2: HIGH - Tests are failing
|
|
371
|
+
if (gates.testsPass === false) {
|
|
372
|
+
return {
|
|
373
|
+
prompt: `Fix the failing tests (${gates.failedTests || 'some'} failures):\n${gates.testError || 'Run npm test to see failures'}`,
|
|
374
|
+
reason: 'Tests are failing',
|
|
375
|
+
priority: 'high',
|
|
376
|
+
context: gates.testError,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// Priority 3: HIGH - Stuck on same error
|
|
380
|
+
if (stuckErrors.length > 0) {
|
|
381
|
+
const stuck = stuckErrors[0];
|
|
382
|
+
const triedApproaches = stuck.fixAttempts.filter(a => !a.worked).map(a => a.approach);
|
|
383
|
+
return {
|
|
384
|
+
prompt: `Stuck on error (tried ${stuck.fixAttempts.length}x). Tornado time:\n1. Research: "${stuck.error.slice(0, 50)}"\n2. Add logging around the issue\n3. Write a minimal test case\n\nAlready tried: ${triedApproaches.join(', ')}`,
|
|
385
|
+
reason: `Same error seen ${stuck.fixAttempts.length} times`,
|
|
386
|
+
priority: 'high',
|
|
387
|
+
context: stuck.error,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// Priority 4: NORMAL - Lint errors
|
|
391
|
+
if (gates.lintsPass === false) {
|
|
392
|
+
return {
|
|
393
|
+
prompt: `Fix ${gates.lintErrors || 'the'} linter errors, then run lint again.`,
|
|
394
|
+
reason: 'Linter errors present',
|
|
395
|
+
priority: 'normal',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// Priority 5: NORMAL - Unresolved errors from recent session
|
|
399
|
+
if (unresolvedErrors.length > 0) {
|
|
400
|
+
const recent = unresolvedErrors[0];
|
|
401
|
+
return {
|
|
402
|
+
prompt: `Address this error${recent.file ? ` in ${recent.file}` : ''}:\n${recent.error}`,
|
|
403
|
+
reason: 'Unresolved error from earlier',
|
|
404
|
+
priority: 'normal',
|
|
405
|
+
context: recent.error,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
// Priority 6: Check if gates are stale
|
|
409
|
+
const gatesStatus = getGatesStatus(projectPath);
|
|
410
|
+
if (gatesStatus.stale && tracker.currentTask?.phase === 'implement') {
|
|
411
|
+
return {
|
|
412
|
+
prompt: 'Verify changes: run build and tests to check everything still works.',
|
|
413
|
+
reason: 'No verification run recently',
|
|
414
|
+
priority: 'normal',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
// Priority 7: All gates pass - suggest advancement
|
|
418
|
+
if (gatesStatus.allPass) {
|
|
419
|
+
return {
|
|
420
|
+
prompt: 'All gates pass. Ready to advance to the next step.',
|
|
421
|
+
reason: 'Build, tests, and lint all passing',
|
|
422
|
+
priority: 'low',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Default: Continue with current phase
|
|
426
|
+
return {
|
|
427
|
+
prompt: getPhaseBasedPrompt(tracker.inferredPhase, tracker.currentTask),
|
|
428
|
+
reason: 'Continuing current phase',
|
|
429
|
+
priority: 'normal',
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function getPhaseBasedPrompt(phase, task) {
|
|
433
|
+
if (phase.phase === 'IDLE') {
|
|
434
|
+
return 'Start a new project or set the phase with midas_set_phase.';
|
|
435
|
+
}
|
|
436
|
+
if (phase.phase === 'EAGLE_SIGHT') {
|
|
437
|
+
const stepPrompts = {
|
|
438
|
+
IDEA: 'Define the core idea: What problem? Who for? Why now?',
|
|
439
|
+
RESEARCH: 'Research the landscape: What exists? What works? What fails?',
|
|
440
|
+
BRAINLIFT: 'Document your unique insights in docs/brainlift.md',
|
|
441
|
+
PRD: 'Write requirements in docs/prd.md',
|
|
442
|
+
GAMEPLAN: 'Plan the build in docs/gameplan.md',
|
|
443
|
+
};
|
|
444
|
+
return stepPrompts[phase.step] || 'Continue planning.';
|
|
445
|
+
}
|
|
446
|
+
if (phase.phase === 'BUILD') {
|
|
447
|
+
const taskContext = task ? ` for: ${task.description}` : '';
|
|
448
|
+
const stepPrompts = {
|
|
449
|
+
RULES: `Load .cursorrules and understand project constraints${taskContext}`,
|
|
450
|
+
INDEX: `Index the codebase structure and architecture${taskContext}`,
|
|
451
|
+
READ: `Read the specific files needed${taskContext}`,
|
|
452
|
+
RESEARCH: `Research docs and APIs needed${taskContext}`,
|
|
453
|
+
IMPLEMENT: `Implement${taskContext} with tests`,
|
|
454
|
+
TEST: 'Run tests and fix any failures',
|
|
455
|
+
DEBUG: 'Debug using Tornado: Research + Logs + Tests',
|
|
456
|
+
};
|
|
457
|
+
return stepPrompts[phase.step] || 'Continue building.';
|
|
458
|
+
}
|
|
459
|
+
if (phase.phase === 'SHIP') {
|
|
460
|
+
const stepPrompts = {
|
|
461
|
+
REVIEW: 'Code review: Check security, performance, edge cases',
|
|
462
|
+
DEPLOY: 'Deploy to production: CI/CD, environment config',
|
|
463
|
+
MONITOR: 'Set up monitoring: logs, alerts, health checks',
|
|
464
|
+
};
|
|
465
|
+
return stepPrompts[phase.step] || 'Continue shipping.';
|
|
466
|
+
}
|
|
467
|
+
if (phase.phase === 'GROW') {
|
|
468
|
+
const stepPrompts = {
|
|
469
|
+
FEEDBACK: 'Collect user feedback: interviews, support tickets, reviews',
|
|
470
|
+
ANALYZE: 'Study the data: metrics, behavior patterns, retention',
|
|
471
|
+
ITERATE: 'Plan next cycle: prioritize and return to Eagle Sight',
|
|
472
|
+
};
|
|
473
|
+
return stepPrompts[phase.step] || 'Continue growing.';
|
|
474
|
+
}
|
|
475
|
+
return 'Continue with the current phase.';
|
|
476
|
+
}
|
|
477
|
+
// ============================================================================
|
|
478
|
+
// AUTO-ADVANCE PHASE
|
|
479
|
+
// ============================================================================
|
|
480
|
+
export function maybeAutoAdvance(projectPath) {
|
|
481
|
+
const tracker = loadTracker(projectPath);
|
|
482
|
+
const gatesStatus = getGatesStatus(projectPath);
|
|
483
|
+
const state = loadState(projectPath);
|
|
484
|
+
const currentPhase = state.current;
|
|
485
|
+
// Only auto-advance if all gates pass
|
|
486
|
+
if (!gatesStatus.allPass) {
|
|
487
|
+
return { advanced: false, from: currentPhase, to: currentPhase };
|
|
488
|
+
}
|
|
489
|
+
// Only auto-advance from BUILD:IMPLEMENT or BUILD:TEST
|
|
490
|
+
if (currentPhase.phase === 'BUILD') {
|
|
491
|
+
if (currentPhase.step === 'IMPLEMENT' || currentPhase.step === 'TEST') {
|
|
492
|
+
const nextPhase = getNextPhase(currentPhase);
|
|
493
|
+
// Update state
|
|
494
|
+
state.history.push(currentPhase);
|
|
495
|
+
state.current = nextPhase;
|
|
496
|
+
saveState(projectPath, state);
|
|
497
|
+
// Update tracker
|
|
498
|
+
tracker.inferredPhase = nextPhase;
|
|
499
|
+
saveTracker(projectPath, tracker);
|
|
500
|
+
return { advanced: true, from: currentPhase, to: nextPhase };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { advanced: false, from: currentPhase, to: currentPhase };
|
|
504
|
+
}
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// EXISTING FUNCTIONS (preserved with enhancements)
|
|
507
|
+
// ============================================================================
|
|
56
508
|
export function scanRecentFiles(projectPath, since) {
|
|
57
|
-
const cutoff = since || Date.now() - 3600000;
|
|
509
|
+
const cutoff = since || Date.now() - 3600000;
|
|
58
510
|
const files = [];
|
|
59
511
|
const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.midas'];
|
|
60
512
|
function scan(dir, depth = 0) {
|
|
@@ -87,45 +539,47 @@ export function scanRecentFiles(projectPath, since) {
|
|
|
87
539
|
scan(projectPath);
|
|
88
540
|
return files.sort((a, b) => b.lastModified - a.lastModified);
|
|
89
541
|
}
|
|
90
|
-
// Get git activity
|
|
91
542
|
export function getGitActivity(projectPath) {
|
|
92
|
-
|
|
543
|
+
const safePath = sanitizePath(projectPath);
|
|
544
|
+
if (!existsSync(join(safePath, '.git')))
|
|
545
|
+
return null;
|
|
546
|
+
if (!isShellSafe(safePath)) {
|
|
547
|
+
logger.debug('Unsafe path for git commands', { path: safePath });
|
|
93
548
|
return null;
|
|
549
|
+
}
|
|
94
550
|
try {
|
|
95
|
-
const branch = execSync('git branch --show-current', { cwd:
|
|
551
|
+
const branch = execSync('git branch --show-current', { cwd: safePath, encoding: 'utf-8' }).trim();
|
|
96
552
|
let lastCommit;
|
|
97
553
|
let lastCommitMessage;
|
|
98
554
|
let lastCommitTime;
|
|
99
555
|
try {
|
|
100
|
-
lastCommit = execSync('git log -1 --format=%H', { cwd:
|
|
101
|
-
lastCommitMessage = execSync('git log -1 --format=%s', { cwd:
|
|
102
|
-
const timeStr = execSync('git log -1 --format=%ct', { cwd:
|
|
556
|
+
lastCommit = execSync('git log -1 --format=%H', { cwd: safePath, encoding: 'utf-8' }).trim();
|
|
557
|
+
lastCommitMessage = execSync('git log -1 --format=%s', { cwd: safePath, encoding: 'utf-8' }).trim();
|
|
558
|
+
const timeStr = execSync('git log -1 --format=%ct', { cwd: safePath, encoding: 'utf-8' }).trim();
|
|
103
559
|
lastCommitTime = parseInt(timeStr) * 1000;
|
|
104
560
|
}
|
|
105
561
|
catch { }
|
|
106
562
|
let uncommittedChanges = 0;
|
|
107
563
|
try {
|
|
108
|
-
const status = execSync('git status --porcelain', { cwd:
|
|
564
|
+
const status = execSync('git status --porcelain', { cwd: safePath, encoding: 'utf-8' });
|
|
109
565
|
uncommittedChanges = status.split('\n').filter(Boolean).length;
|
|
110
566
|
}
|
|
111
567
|
catch { }
|
|
112
568
|
return { branch, lastCommit, lastCommitMessage, lastCommitTime, uncommittedChanges };
|
|
113
569
|
}
|
|
114
|
-
catch {
|
|
570
|
+
catch (error) {
|
|
571
|
+
logger.error('Failed to get git activity', error);
|
|
115
572
|
return null;
|
|
116
573
|
}
|
|
117
574
|
}
|
|
118
|
-
// Check completion signals
|
|
119
575
|
export function checkCompletionSignals(projectPath) {
|
|
120
576
|
const signals = {
|
|
121
577
|
testsExist: false,
|
|
122
578
|
docsComplete: false,
|
|
123
579
|
};
|
|
124
|
-
// Check for tests
|
|
125
580
|
const testPatterns = ['.test.', '.spec.', '__tests__', 'tests/'];
|
|
126
|
-
const files = scanRecentFiles(projectPath, 0);
|
|
581
|
+
const files = scanRecentFiles(projectPath, 0);
|
|
127
582
|
signals.testsExist = files.some(f => testPatterns.some(p => f.path.includes(p)));
|
|
128
|
-
// Check docs
|
|
129
583
|
const docsPath = join(projectPath, 'docs');
|
|
130
584
|
if (existsSync(docsPath)) {
|
|
131
585
|
const brainlift = existsSync(join(docsPath, 'brainlift.md'));
|
|
@@ -135,13 +589,11 @@ export function checkCompletionSignals(projectPath) {
|
|
|
135
589
|
}
|
|
136
590
|
return signals;
|
|
137
591
|
}
|
|
138
|
-
// Infer phase from tool calls
|
|
139
592
|
function updatePhaseFromToolCalls(tracker) {
|
|
140
593
|
const recent = tracker.recentToolCalls.slice(0, 10);
|
|
141
594
|
if (recent.length === 0)
|
|
142
595
|
return;
|
|
143
596
|
const lastTool = recent[0].tool;
|
|
144
|
-
// Tool -> Phase mapping
|
|
145
597
|
const toolPhaseMap = {
|
|
146
598
|
'midas_start_project': { phase: 'EAGLE_SIGHT', step: 'IDEA' },
|
|
147
599
|
'midas_check_docs': { phase: 'EAGLE_SIGHT', step: 'BRAINLIFT' },
|
|
@@ -149,32 +601,17 @@ function updatePhaseFromToolCalls(tracker) {
|
|
|
149
601
|
'midas_oneshot': { phase: 'BUILD', step: 'DEBUG' },
|
|
150
602
|
'midas_horizon': { phase: 'BUILD', step: 'IMPLEMENT' },
|
|
151
603
|
'midas_audit': { phase: 'SHIP', step: 'REVIEW' },
|
|
604
|
+
'midas_verify': { phase: 'BUILD', step: 'TEST' },
|
|
152
605
|
};
|
|
153
606
|
if (toolPhaseMap[lastTool]) {
|
|
154
607
|
tracker.inferredPhase = toolPhaseMap[lastTool];
|
|
155
608
|
tracker.confidence = 80;
|
|
156
609
|
}
|
|
157
610
|
}
|
|
158
|
-
// Full tracker update - call this periodically or on-demand
|
|
159
|
-
export function updateTracker(projectPath) {
|
|
160
|
-
const tracker = loadTracker(projectPath);
|
|
161
|
-
// Update file activity
|
|
162
|
-
tracker.recentFiles = scanRecentFiles(projectPath);
|
|
163
|
-
// Update git activity
|
|
164
|
-
tracker.gitActivity = getGitActivity(projectPath);
|
|
165
|
-
// Update completion signals
|
|
166
|
-
tracker.completionSignals = checkCompletionSignals(projectPath);
|
|
167
|
-
// Infer phase from signals
|
|
168
|
-
inferPhaseFromSignals(tracker);
|
|
169
|
-
saveTracker(projectPath, tracker);
|
|
170
|
-
return tracker;
|
|
171
|
-
}
|
|
172
|
-
// Use multiple signals to infer phase
|
|
173
611
|
function inferPhaseFromSignals(tracker) {
|
|
174
612
|
const signals = tracker.completionSignals;
|
|
175
613
|
const git = tracker.gitActivity;
|
|
176
614
|
const recentTools = tracker.recentToolCalls.slice(0, 5).map(t => t.tool);
|
|
177
|
-
// If docs don't exist yet, we're in EAGLE_SIGHT
|
|
178
615
|
if (!signals.docsComplete) {
|
|
179
616
|
if (!existsSync(join(process.cwd(), 'docs'))) {
|
|
180
617
|
tracker.inferredPhase = { phase: 'IDLE' };
|
|
@@ -185,9 +622,7 @@ function inferPhaseFromSignals(tracker) {
|
|
|
185
622
|
tracker.confidence = 70;
|
|
186
623
|
return;
|
|
187
624
|
}
|
|
188
|
-
// If we have uncommitted changes and recent file edits, we're building
|
|
189
625
|
if (git && git.uncommittedChanges > 0 && tracker.recentFiles.length > 0) {
|
|
190
|
-
// Check what type of files changed
|
|
191
626
|
const recentPaths = tracker.recentFiles.map(f => f.path);
|
|
192
627
|
const hasTestChanges = recentPaths.some(p => p.includes('.test.') || p.includes('.spec.'));
|
|
193
628
|
const hasSrcChanges = recentPaths.some(p => p.includes('src/') || p.includes('lib/'));
|
|
@@ -203,28 +638,32 @@ function inferPhaseFromSignals(tracker) {
|
|
|
203
638
|
tracker.confidence = 60;
|
|
204
639
|
return;
|
|
205
640
|
}
|
|
206
|
-
// If audit was recently called, we're shipping
|
|
207
641
|
if (recentTools.includes('midas_audit')) {
|
|
208
642
|
tracker.inferredPhase = { phase: 'SHIP', step: 'REVIEW' };
|
|
209
643
|
tracker.confidence = 75;
|
|
210
644
|
return;
|
|
211
645
|
}
|
|
212
|
-
// Default to BUILD:IMPLEMENT if we have code
|
|
213
646
|
if (tracker.recentFiles.length > 0) {
|
|
214
647
|
tracker.inferredPhase = { phase: 'BUILD', step: 'IMPLEMENT' };
|
|
215
648
|
tracker.confidence = 40;
|
|
216
649
|
}
|
|
217
650
|
}
|
|
218
|
-
|
|
651
|
+
export function updateTracker(projectPath) {
|
|
652
|
+
const tracker = loadTracker(projectPath);
|
|
653
|
+
tracker.recentFiles = scanRecentFiles(projectPath);
|
|
654
|
+
tracker.gitActivity = getGitActivity(projectPath);
|
|
655
|
+
tracker.completionSignals = checkCompletionSignals(projectPath);
|
|
656
|
+
inferPhaseFromSignals(tracker);
|
|
657
|
+
saveTracker(projectPath, tracker);
|
|
658
|
+
return tracker;
|
|
659
|
+
}
|
|
219
660
|
export function getActivitySummary(projectPath) {
|
|
220
661
|
const tracker = updateTracker(projectPath);
|
|
221
662
|
const lines = [];
|
|
222
|
-
// Recent files
|
|
223
663
|
if (tracker.recentFiles.length > 0) {
|
|
224
664
|
const topFiles = tracker.recentFiles.slice(0, 3);
|
|
225
665
|
lines.push(`Files: ${topFiles.map(f => f.path.split('/').pop()).join(', ')}`);
|
|
226
666
|
}
|
|
227
|
-
// Git status
|
|
228
667
|
if (tracker.gitActivity) {
|
|
229
668
|
if (tracker.gitActivity.uncommittedChanges > 0) {
|
|
230
669
|
lines.push(`${tracker.gitActivity.uncommittedChanges} uncommitted changes`);
|
|
@@ -233,11 +672,18 @@ export function getActivitySummary(projectPath) {
|
|
|
233
672
|
lines.push(`Last: "${tracker.gitActivity.lastCommitMessage.slice(0, 30)}..."`);
|
|
234
673
|
}
|
|
235
674
|
}
|
|
236
|
-
// Recent tools
|
|
237
675
|
if (tracker.recentToolCalls.length > 0) {
|
|
238
676
|
const lastTool = tracker.recentToolCalls[0].tool.replace('midas_', '');
|
|
239
677
|
lines.push(`Tool: ${lastTool}`);
|
|
240
678
|
}
|
|
679
|
+
// Add gate status
|
|
680
|
+
const gatesStatus = getGatesStatus(projectPath);
|
|
681
|
+
if (gatesStatus.failing.length > 0) {
|
|
682
|
+
lines.push(`Failing: ${gatesStatus.failing.join(', ')}`);
|
|
683
|
+
}
|
|
684
|
+
else if (gatesStatus.allPass) {
|
|
685
|
+
lines.push('Gates: all pass');
|
|
686
|
+
}
|
|
241
687
|
return lines.join(' | ') || 'No recent activity';
|
|
242
688
|
}
|
|
243
689
|
//# sourceMappingURL=tracker.js.map
|