midas-mcp 2.3.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.
Files changed (47) hide show
  1. package/dist/analyzer.d.ts +14 -0
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +182 -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 +86 -0
  41. package/dist/tracker.d.ts.map +1 -1
  42. package/dist/tracker.js +493 -47
  43. package/dist/tracker.js.map +1 -1
  44. package/dist/tui.d.ts.map +1 -1
  45. package/dist/tui.js +118 -2
  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,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
- // 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)];
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
- // Scan for recently modified files
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; // Last hour by default
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
- if (!existsSync(join(projectPath, '.git')))
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: projectPath, encoding: 'utf-8' }).trim();
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: 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();
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: projectPath, encoding: 'utf-8' });
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); // All files
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
- // Get a summary of current activity for display
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