midas-mcp 2.3.0 → 2.6.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.
Files changed (47) hide show
  1. package/dist/analyzer.d.ts +15 -0
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +183 -67
  4. package/dist/analyzer.js.map +1 -1
  5. package/dist/context.d.ts +55 -0
  6. package/dist/context.d.ts.map +1 -0
  7. package/dist/context.js +336 -0
  8. package/dist/context.js.map +1 -0
  9. package/dist/security.d.ts +29 -0
  10. package/dist/security.d.ts.map +1 -0
  11. package/dist/security.js +72 -0
  12. package/dist/security.js.map +1 -0
  13. package/dist/server.d.ts.map +1 -1
  14. package/dist/server.js +13 -2
  15. package/dist/server.js.map +1 -1
  16. package/dist/tests/edge-cases.test.js +4 -2
  17. package/dist/tests/edge-cases.test.js.map +1 -1
  18. package/dist/tests/journal.test.js +4 -1
  19. package/dist/tests/journal.test.js.map +1 -1
  20. package/dist/tests/security.test.d.ts +2 -0
  21. package/dist/tests/security.test.d.ts.map +1 -0
  22. package/dist/tests/security.test.js +105 -0
  23. package/dist/tests/security.test.js.map +1 -0
  24. package/dist/tools/index.d.ts +1 -0
  25. package/dist/tools/index.d.ts.map +1 -1
  26. package/dist/tools/index.js +2 -0
  27. package/dist/tools/index.js.map +1 -1
  28. package/dist/tools/journal.d.ts.map +1 -1
  29. package/dist/tools/journal.js +15 -10
  30. package/dist/tools/journal.js.map +1 -1
  31. package/dist/tools/phase.d.ts +1 -0
  32. package/dist/tools/phase.d.ts.map +1 -1
  33. package/dist/tools/phase.js +55 -16
  34. package/dist/tools/phase.js.map +1 -1
  35. package/dist/tools/tornado.d.ts +2 -2
  36. package/dist/tools/verify.d.ts +137 -0
  37. package/dist/tools/verify.d.ts.map +1 -0
  38. package/dist/tools/verify.js +151 -0
  39. package/dist/tools/verify.js.map +1 -0
  40. package/dist/tracker.d.ts +87 -0
  41. package/dist/tracker.d.ts.map +1 -1
  42. package/dist/tracker.js +608 -47
  43. package/dist/tracker.js.map +1 -1
  44. package/dist/tui.d.ts.map +1 -1
  45. package/dist/tui.js +184 -44
  46. package/dist/tui.js.map +1 -1
  47. 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 path = getTrackerPath(projectPath);
22
+ const safePath = sanitizePath(projectPath);
23
+ const path = getTrackerPath(safePath);
17
24
  if (existsSync(path)) {
18
25
  try {
19
- return JSON.parse(readFileSync(path, 'utf-8'));
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,578 @@ 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
- // Track when an MCP tool is called
69
+ // ============================================================================
70
+ // TOOL CALL TRACKING
71
+ // ============================================================================
45
72
  export function trackToolCall(projectPath, tool, args) {
46
- const tracker = loadTracker(projectPath);
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), // Keep last 50
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)];
53
208
  saveTracker(projectPath, tracker);
209
+ return newError;
54
210
  }
55
- // Scan for recently modified files
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;
263
+ saveTracker(projectPath, tracker);
264
+ }
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
+ // ============================================================================
357
+ // COACHING EXPLANATIONS
358
+ // ============================================================================
359
+ /**
360
+ * Educational explanations for each suggestion type
361
+ * These teach the user WHY this is the right next step
362
+ */
363
+ const COACHING = {
364
+ buildFailing: {
365
+ short: 'Build is failing - must fix before continuing',
366
+ explain: `Your code won't compile. In Golden Code, we never proceed with broken builds because:
367
+ 1. You can't run tests on code that doesn't compile
368
+ 2. Errors compound - fixing later is harder
369
+ 3. Every minute coding on a broken base is wasted
370
+ Fix compilation errors first, always.`,
371
+ },
372
+ testsFailing: {
373
+ short: 'Tests are failing - fix before new features',
374
+ explain: `Failing tests mean your safety net has holes. The Golden Code rule:
375
+ 1. Never add features with failing tests
376
+ 2. A test failure is a gift - it caught a bug before users did
377
+ 3. Fix tests immediately while context is fresh
378
+ The longer you wait, the harder it gets to remember what broke.`,
379
+ },
380
+ stuckOnError: {
381
+ short: 'Same error multiple times - time for Tornado',
382
+ explain: `You've tried fixing this ${'{attempts}'} times without success. Random fixes won't work.
383
+ The Tornado cycle breaks the loop:
384
+ 1. RESEARCH - Search docs, StackOverflow, GitHub issues for this exact error
385
+ 2. LOGS - Add console.log/debugger around the problem to see actual values
386
+ 3. TESTS - Write a minimal test case that reproduces the bug
387
+ This systematic approach works when guessing doesn't.`,
388
+ },
389
+ lintErrors: {
390
+ short: 'Linter errors present',
391
+ explain: `Linting catches bugs before they become runtime errors:
392
+ - Unused variables often indicate logic mistakes
393
+ - Type errors prevent crashes
394
+ - Style consistency makes code readable for future you
395
+ Fix these now - they take seconds but prevent hours of debugging later.`,
396
+ },
397
+ unresolvedError: {
398
+ short: 'Unresolved error from earlier session',
399
+ explain: `You left off with an error. Continuing without fixing it means:
400
+ - The bug is still there
401
+ - You'll hit it again (probably at a worse time)
402
+ - Context you had is fading
403
+ Address it now while it's still fresh in the codebase.`,
404
+ },
405
+ verifyChanges: {
406
+ short: 'No verification run recently',
407
+ explain: `You've made changes but haven't verified them. Golden Code principle:
408
+ - Verify early, verify often
409
+ - The longer between checks, the harder to find what broke
410
+ - A passing build gives confidence to continue
411
+ Run build + tests now to catch issues while changes are small.`,
412
+ },
413
+ allGatesPass: {
414
+ short: 'All gates pass - ready to advance',
415
+ explain: `Build passes, tests pass, lint passes. This is the green light.
416
+ - Your code is verified working
417
+ - It's safe to commit and move forward
418
+ - You've earned the right to add new features
419
+ Consider committing this checkpoint before starting the next task.`,
420
+ },
421
+ phaseDefault: (phase, step) => ({
422
+ short: `Continuing ${phase}:${step}`,
423
+ explain: `You're in the ${phase} phase, ${step} step.
424
+ ${getPhaseExplanation(phase, step)}
425
+ Focus on completing this step before moving to the next.`,
426
+ }),
427
+ };
428
+ function getPhaseExplanation(phase, step) {
429
+ const explanations = {
430
+ EAGLE_SIGHT: {
431
+ IDEA: 'Define the core problem, who it affects, and why now is the right time to solve it.',
432
+ RESEARCH: 'Study what exists. What works? What fails? Where are the gaps?',
433
+ BRAINLIFT: 'Document your unique insights - what do you know that others don\'t?',
434
+ PRD: 'Write clear requirements. Vague requirements lead to vague implementations.',
435
+ GAMEPLAN: 'Break the build into ordered tasks. Each task should be completable in one session.',
436
+ },
437
+ BUILD: {
438
+ RULES: 'Read project constraints first. Building without knowing the rules wastes time.',
439
+ INDEX: 'Understand the codebase structure before diving in. Where does what live?',
440
+ READ: 'Read the specific files you\'ll touch. Understand before you modify.',
441
+ RESEARCH: 'Look up docs for APIs you\'ll use. Don\'t guess at library behavior.',
442
+ IMPLEMENT: 'Write code with tests. Test-first catches bugs before they compound.',
443
+ TEST: 'Run all tests. Green means safe. Red means stop and fix.',
444
+ DEBUG: 'Use the Tornado cycle: Research + Logs + Tests when stuck.',
445
+ },
446
+ SHIP: {
447
+ REVIEW: 'Review for security, performance, and edge cases before shipping.',
448
+ DEPLOY: 'Deploy with proper CI/CD. Manual deploys are error-prone.',
449
+ MONITOR: 'Set up logs and alerts. You can\'t fix what you can\'t see.',
450
+ },
451
+ GROW: {
452
+ FEEDBACK: 'Collect real user feedback. Your assumptions need validation.',
453
+ ANALYZE: 'Study the data. Where do users struggle? What do they love?',
454
+ ITERATE: 'Plan the next cycle based on evidence, not guesses. Back to Eagle Sight.',
455
+ },
456
+ };
457
+ return explanations[phase]?.[step] || 'Continue with the current step.';
458
+ }
459
+ export function getSmartPromptSuggestion(projectPath) {
460
+ const tracker = loadTracker(projectPath);
461
+ const gates = tracker.gates;
462
+ const stuckErrors = getStuckErrors(projectPath);
463
+ const unresolvedErrors = getUnresolvedErrors(projectPath);
464
+ // Priority 1: CRITICAL - Build is broken
465
+ if (gates.compiles === false) {
466
+ return {
467
+ prompt: `Fix the TypeScript compilation errors:\n${gates.compileError || 'Run npm run build to see errors'}`,
468
+ reason: COACHING.buildFailing.short,
469
+ explanation: COACHING.buildFailing.explain,
470
+ priority: 'critical',
471
+ context: gates.compileError,
472
+ };
473
+ }
474
+ // Priority 2: HIGH - Tests are failing
475
+ if (gates.testsPass === false) {
476
+ return {
477
+ prompt: `Fix the failing tests (${gates.failedTests || 'some'} failures):\n${gates.testError || 'Run npm test to see failures'}`,
478
+ reason: COACHING.testsFailing.short,
479
+ explanation: COACHING.testsFailing.explain,
480
+ priority: 'high',
481
+ context: gates.testError,
482
+ };
483
+ }
484
+ // Priority 3: HIGH - Stuck on same error
485
+ if (stuckErrors.length > 0) {
486
+ const stuck = stuckErrors[0];
487
+ const triedApproaches = stuck.fixAttempts.filter(a => !a.worked).map(a => a.approach);
488
+ return {
489
+ 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(', ')}`,
490
+ reason: COACHING.stuckOnError.short,
491
+ explanation: COACHING.stuckOnError.explain.replace('{attempts}', String(stuck.fixAttempts.length)),
492
+ priority: 'high',
493
+ context: stuck.error,
494
+ };
495
+ }
496
+ // Priority 4: NORMAL - Lint errors
497
+ if (gates.lintsPass === false) {
498
+ return {
499
+ prompt: `Fix ${gates.lintErrors || 'the'} linter errors, then run lint again.`,
500
+ reason: COACHING.lintErrors.short,
501
+ explanation: COACHING.lintErrors.explain,
502
+ priority: 'normal',
503
+ };
504
+ }
505
+ // Priority 5: NORMAL - Unresolved errors from recent session
506
+ if (unresolvedErrors.length > 0) {
507
+ const recent = unresolvedErrors[0];
508
+ return {
509
+ prompt: `Address this error${recent.file ? ` in ${recent.file}` : ''}:\n${recent.error}`,
510
+ reason: COACHING.unresolvedError.short,
511
+ explanation: COACHING.unresolvedError.explain,
512
+ priority: 'normal',
513
+ context: recent.error,
514
+ };
515
+ }
516
+ // Priority 6: Check if gates are stale
517
+ const gatesStatus = getGatesStatus(projectPath);
518
+ if (gatesStatus.stale && tracker.currentTask?.phase === 'implement') {
519
+ return {
520
+ prompt: 'Verify changes: run build and tests to check everything still works.',
521
+ reason: COACHING.verifyChanges.short,
522
+ explanation: COACHING.verifyChanges.explain,
523
+ priority: 'normal',
524
+ };
525
+ }
526
+ // Priority 7: All gates pass - suggest advancement
527
+ if (gatesStatus.allPass) {
528
+ return {
529
+ prompt: 'All gates pass. Ready to advance to the next step.',
530
+ reason: COACHING.allGatesPass.short,
531
+ explanation: COACHING.allGatesPass.explain,
532
+ priority: 'low',
533
+ };
534
+ }
535
+ // Default: Continue with current phase
536
+ const phase = tracker.inferredPhase;
537
+ const phaseStr = phase.phase;
538
+ const stepStr = 'step' in phase ? phase.step : 'IDLE';
539
+ const coaching = COACHING.phaseDefault(phaseStr, stepStr);
540
+ return {
541
+ prompt: getPhaseBasedPrompt(tracker.inferredPhase, tracker.currentTask),
542
+ reason: coaching.short,
543
+ explanation: coaching.explain,
544
+ priority: 'normal',
545
+ };
546
+ }
547
+ function getPhaseBasedPrompt(phase, task) {
548
+ if (phase.phase === 'IDLE') {
549
+ return 'Start a new project or set the phase with midas_set_phase.';
550
+ }
551
+ if (phase.phase === 'EAGLE_SIGHT') {
552
+ const stepPrompts = {
553
+ IDEA: 'Define the core idea: What problem? Who for? Why now?',
554
+ RESEARCH: 'Research the landscape: What exists? What works? What fails?',
555
+ BRAINLIFT: 'Document your unique insights in docs/brainlift.md',
556
+ PRD: 'Write requirements in docs/prd.md',
557
+ GAMEPLAN: 'Plan the build in docs/gameplan.md',
558
+ };
559
+ return stepPrompts[phase.step] || 'Continue planning.';
560
+ }
561
+ if (phase.phase === 'BUILD') {
562
+ const taskContext = task ? ` for: ${task.description}` : '';
563
+ const stepPrompts = {
564
+ RULES: `Load .cursorrules and understand project constraints${taskContext}`,
565
+ INDEX: `Index the codebase structure and architecture${taskContext}`,
566
+ READ: `Read the specific files needed${taskContext}`,
567
+ RESEARCH: `Research docs and APIs needed${taskContext}`,
568
+ IMPLEMENT: `Implement${taskContext} with tests`,
569
+ TEST: 'Run tests and fix any failures',
570
+ DEBUG: 'Debug using Tornado: Research + Logs + Tests',
571
+ };
572
+ return stepPrompts[phase.step] || 'Continue building.';
573
+ }
574
+ if (phase.phase === 'SHIP') {
575
+ const stepPrompts = {
576
+ REVIEW: 'Code review: Check security, performance, edge cases',
577
+ DEPLOY: 'Deploy to production: CI/CD, environment config',
578
+ MONITOR: 'Set up monitoring: logs, alerts, health checks',
579
+ };
580
+ return stepPrompts[phase.step] || 'Continue shipping.';
581
+ }
582
+ if (phase.phase === 'GROW') {
583
+ const stepPrompts = {
584
+ FEEDBACK: 'Collect user feedback: interviews, support tickets, reviews',
585
+ ANALYZE: 'Study the data: metrics, behavior patterns, retention',
586
+ ITERATE: 'Plan next cycle: prioritize and return to Eagle Sight',
587
+ };
588
+ return stepPrompts[phase.step] || 'Continue growing.';
589
+ }
590
+ return 'Continue with the current phase.';
591
+ }
592
+ // ============================================================================
593
+ // AUTO-ADVANCE PHASE
594
+ // ============================================================================
595
+ export function maybeAutoAdvance(projectPath) {
596
+ const tracker = loadTracker(projectPath);
597
+ const gatesStatus = getGatesStatus(projectPath);
598
+ const state = loadState(projectPath);
599
+ const currentPhase = state.current;
600
+ // Only auto-advance if all gates pass
601
+ if (!gatesStatus.allPass) {
602
+ return { advanced: false, from: currentPhase, to: currentPhase };
603
+ }
604
+ // Only auto-advance from BUILD:IMPLEMENT or BUILD:TEST
605
+ if (currentPhase.phase === 'BUILD') {
606
+ if (currentPhase.step === 'IMPLEMENT' || currentPhase.step === 'TEST') {
607
+ const nextPhase = getNextPhase(currentPhase);
608
+ // Update state
609
+ state.history.push(currentPhase);
610
+ state.current = nextPhase;
611
+ saveState(projectPath, state);
612
+ // Update tracker
613
+ tracker.inferredPhase = nextPhase;
614
+ saveTracker(projectPath, tracker);
615
+ return { advanced: true, from: currentPhase, to: nextPhase };
616
+ }
617
+ }
618
+ return { advanced: false, from: currentPhase, to: currentPhase };
619
+ }
620
+ // ============================================================================
621
+ // EXISTING FUNCTIONS (preserved with enhancements)
622
+ // ============================================================================
56
623
  export function scanRecentFiles(projectPath, since) {
57
- const cutoff = since || Date.now() - 3600000; // Last hour by default
624
+ const cutoff = since || Date.now() - 3600000;
58
625
  const files = [];
59
626
  const ignore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.midas'];
60
627
  function scan(dir, depth = 0) {
@@ -87,45 +654,47 @@ export function scanRecentFiles(projectPath, since) {
87
654
  scan(projectPath);
88
655
  return files.sort((a, b) => b.lastModified - a.lastModified);
89
656
  }
90
- // Get git activity
91
657
  export function getGitActivity(projectPath) {
92
- if (!existsSync(join(projectPath, '.git')))
658
+ const safePath = sanitizePath(projectPath);
659
+ if (!existsSync(join(safePath, '.git')))
93
660
  return null;
661
+ if (!isShellSafe(safePath)) {
662
+ logger.debug('Unsafe path for git commands', { path: safePath });
663
+ return null;
664
+ }
94
665
  try {
95
- const branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim();
666
+ const branch = execSync('git branch --show-current', { cwd: safePath, encoding: 'utf-8' }).trim();
96
667
  let lastCommit;
97
668
  let lastCommitMessage;
98
669
  let lastCommitTime;
99
670
  try {
100
- lastCommit = execSync('git log -1 --format=%H', { cwd: projectPath, encoding: 'utf-8' }).trim();
101
- lastCommitMessage = execSync('git log -1 --format=%s', { cwd: projectPath, encoding: 'utf-8' }).trim();
102
- const timeStr = execSync('git log -1 --format=%ct', { cwd: projectPath, encoding: 'utf-8' }).trim();
671
+ lastCommit = execSync('git log -1 --format=%H', { cwd: safePath, encoding: 'utf-8' }).trim();
672
+ lastCommitMessage = execSync('git log -1 --format=%s', { cwd: safePath, encoding: 'utf-8' }).trim();
673
+ const timeStr = execSync('git log -1 --format=%ct', { cwd: safePath, encoding: 'utf-8' }).trim();
103
674
  lastCommitTime = parseInt(timeStr) * 1000;
104
675
  }
105
676
  catch { }
106
677
  let uncommittedChanges = 0;
107
678
  try {
108
- const status = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' });
679
+ const status = execSync('git status --porcelain', { cwd: safePath, encoding: 'utf-8' });
109
680
  uncommittedChanges = status.split('\n').filter(Boolean).length;
110
681
  }
111
682
  catch { }
112
683
  return { branch, lastCommit, lastCommitMessage, lastCommitTime, uncommittedChanges };
113
684
  }
114
- catch {
685
+ catch (error) {
686
+ logger.error('Failed to get git activity', error);
115
687
  return null;
116
688
  }
117
689
  }
118
- // Check completion signals
119
690
  export function checkCompletionSignals(projectPath) {
120
691
  const signals = {
121
692
  testsExist: false,
122
693
  docsComplete: false,
123
694
  };
124
- // Check for tests
125
695
  const testPatterns = ['.test.', '.spec.', '__tests__', 'tests/'];
126
- const files = scanRecentFiles(projectPath, 0); // All files
696
+ const files = scanRecentFiles(projectPath, 0);
127
697
  signals.testsExist = files.some(f => testPatterns.some(p => f.path.includes(p)));
128
- // Check docs
129
698
  const docsPath = join(projectPath, 'docs');
130
699
  if (existsSync(docsPath)) {
131
700
  const brainlift = existsSync(join(docsPath, 'brainlift.md'));
@@ -135,13 +704,11 @@ export function checkCompletionSignals(projectPath) {
135
704
  }
136
705
  return signals;
137
706
  }
138
- // Infer phase from tool calls
139
707
  function updatePhaseFromToolCalls(tracker) {
140
708
  const recent = tracker.recentToolCalls.slice(0, 10);
141
709
  if (recent.length === 0)
142
710
  return;
143
711
  const lastTool = recent[0].tool;
144
- // Tool -> Phase mapping
145
712
  const toolPhaseMap = {
146
713
  'midas_start_project': { phase: 'EAGLE_SIGHT', step: 'IDEA' },
147
714
  'midas_check_docs': { phase: 'EAGLE_SIGHT', step: 'BRAINLIFT' },
@@ -149,32 +716,17 @@ function updatePhaseFromToolCalls(tracker) {
149
716
  'midas_oneshot': { phase: 'BUILD', step: 'DEBUG' },
150
717
  'midas_horizon': { phase: 'BUILD', step: 'IMPLEMENT' },
151
718
  'midas_audit': { phase: 'SHIP', step: 'REVIEW' },
719
+ 'midas_verify': { phase: 'BUILD', step: 'TEST' },
152
720
  };
153
721
  if (toolPhaseMap[lastTool]) {
154
722
  tracker.inferredPhase = toolPhaseMap[lastTool];
155
723
  tracker.confidence = 80;
156
724
  }
157
725
  }
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
726
  function inferPhaseFromSignals(tracker) {
174
727
  const signals = tracker.completionSignals;
175
728
  const git = tracker.gitActivity;
176
729
  const recentTools = tracker.recentToolCalls.slice(0, 5).map(t => t.tool);
177
- // If docs don't exist yet, we're in EAGLE_SIGHT
178
730
  if (!signals.docsComplete) {
179
731
  if (!existsSync(join(process.cwd(), 'docs'))) {
180
732
  tracker.inferredPhase = { phase: 'IDLE' };
@@ -185,9 +737,7 @@ function inferPhaseFromSignals(tracker) {
185
737
  tracker.confidence = 70;
186
738
  return;
187
739
  }
188
- // If we have uncommitted changes and recent file edits, we're building
189
740
  if (git && git.uncommittedChanges > 0 && tracker.recentFiles.length > 0) {
190
- // Check what type of files changed
191
741
  const recentPaths = tracker.recentFiles.map(f => f.path);
192
742
  const hasTestChanges = recentPaths.some(p => p.includes('.test.') || p.includes('.spec.'));
193
743
  const hasSrcChanges = recentPaths.some(p => p.includes('src/') || p.includes('lib/'));
@@ -203,28 +753,32 @@ function inferPhaseFromSignals(tracker) {
203
753
  tracker.confidence = 60;
204
754
  return;
205
755
  }
206
- // If audit was recently called, we're shipping
207
756
  if (recentTools.includes('midas_audit')) {
208
757
  tracker.inferredPhase = { phase: 'SHIP', step: 'REVIEW' };
209
758
  tracker.confidence = 75;
210
759
  return;
211
760
  }
212
- // Default to BUILD:IMPLEMENT if we have code
213
761
  if (tracker.recentFiles.length > 0) {
214
762
  tracker.inferredPhase = { phase: 'BUILD', step: 'IMPLEMENT' };
215
763
  tracker.confidence = 40;
216
764
  }
217
765
  }
218
- // Get a summary of current activity for display
766
+ export function updateTracker(projectPath) {
767
+ const tracker = loadTracker(projectPath);
768
+ tracker.recentFiles = scanRecentFiles(projectPath);
769
+ tracker.gitActivity = getGitActivity(projectPath);
770
+ tracker.completionSignals = checkCompletionSignals(projectPath);
771
+ inferPhaseFromSignals(tracker);
772
+ saveTracker(projectPath, tracker);
773
+ return tracker;
774
+ }
219
775
  export function getActivitySummary(projectPath) {
220
776
  const tracker = updateTracker(projectPath);
221
777
  const lines = [];
222
- // Recent files
223
778
  if (tracker.recentFiles.length > 0) {
224
779
  const topFiles = tracker.recentFiles.slice(0, 3);
225
780
  lines.push(`Files: ${topFiles.map(f => f.path.split('/').pop()).join(', ')}`);
226
781
  }
227
- // Git status
228
782
  if (tracker.gitActivity) {
229
783
  if (tracker.gitActivity.uncommittedChanges > 0) {
230
784
  lines.push(`${tracker.gitActivity.uncommittedChanges} uncommitted changes`);
@@ -233,11 +787,18 @@ export function getActivitySummary(projectPath) {
233
787
  lines.push(`Last: "${tracker.gitActivity.lastCommitMessage.slice(0, 30)}..."`);
234
788
  }
235
789
  }
236
- // Recent tools
237
790
  if (tracker.recentToolCalls.length > 0) {
238
791
  const lastTool = tracker.recentToolCalls[0].tool.replace('midas_', '');
239
792
  lines.push(`Tool: ${lastTool}`);
240
793
  }
794
+ // Add gate status
795
+ const gatesStatus = getGatesStatus(projectPath);
796
+ if (gatesStatus.failing.length > 0) {
797
+ lines.push(`Failing: ${gatesStatus.failing.join(', ')}`);
798
+ }
799
+ else if (gatesStatus.allPass) {
800
+ lines.push('Gates: all pass');
801
+ }
241
802
  return lines.join(' | ') || 'No recent activity';
242
803
  }
243
804
  //# sourceMappingURL=tracker.js.map