@equilateral_ai/mindmeld 3.0.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 (86) hide show
  1. package/README.md +300 -0
  2. package/hooks/README.md +494 -0
  3. package/hooks/pre-compact.js +392 -0
  4. package/hooks/session-start.js +264 -0
  5. package/package.json +90 -0
  6. package/scripts/harvest.js +561 -0
  7. package/scripts/init-project.js +437 -0
  8. package/scripts/inject.js +388 -0
  9. package/src/collaboration/CollaborationPrompt.js +460 -0
  10. package/src/core/AlertEngine.js +813 -0
  11. package/src/core/AlertNotifier.js +363 -0
  12. package/src/core/CorrelationAnalyzer.js +774 -0
  13. package/src/core/CurationEngine.js +688 -0
  14. package/src/core/LLMPatternDetector.js +508 -0
  15. package/src/core/LoadBearingDetector.js +242 -0
  16. package/src/core/NotificationService.js +1032 -0
  17. package/src/core/PatternValidator.js +355 -0
  18. package/src/core/README.md +160 -0
  19. package/src/core/RapportOrchestrator.js +446 -0
  20. package/src/core/RelevanceDetector.js +577 -0
  21. package/src/core/StandardsIngestion.js +575 -0
  22. package/src/core/TeamLoadBearingDetector.js +431 -0
  23. package/src/database/dbOperations.js +105 -0
  24. package/src/handlers/activity/activityGetMe.js +98 -0
  25. package/src/handlers/activity/activityGetTeam.js +130 -0
  26. package/src/handlers/alerts/alertsAcknowledge.js +91 -0
  27. package/src/handlers/alerts/alertsGet.js +250 -0
  28. package/src/handlers/collaborators/collaboratorAdd.js +201 -0
  29. package/src/handlers/collaborators/collaboratorInvite.js +218 -0
  30. package/src/handlers/collaborators/collaboratorList.js +88 -0
  31. package/src/handlers/collaborators/collaboratorRemove.js +127 -0
  32. package/src/handlers/collaborators/inviteAccept.js +122 -0
  33. package/src/handlers/context/contextGet.js +57 -0
  34. package/src/handlers/context/invariantsGet.js +74 -0
  35. package/src/handlers/context/loopsGet.js +82 -0
  36. package/src/handlers/context/notesCreate.js +74 -0
  37. package/src/handlers/context/purposeGet.js +78 -0
  38. package/src/handlers/correlations/correlationsDeveloperGet.js +226 -0
  39. package/src/handlers/correlations/correlationsGet.js +93 -0
  40. package/src/handlers/correlations/correlationsProjectGet.js +161 -0
  41. package/src/handlers/github/githubConnectionStatus.js +49 -0
  42. package/src/handlers/github/githubDiscoverPatterns.js +364 -0
  43. package/src/handlers/github/githubOAuthCallback.js +166 -0
  44. package/src/handlers/github/githubOAuthStart.js +59 -0
  45. package/src/handlers/github/githubPatternsReview.js +109 -0
  46. package/src/handlers/github/githubReposList.js +105 -0
  47. package/src/handlers/helpers/checkSuperAdmin.js +85 -0
  48. package/src/handlers/helpers/dbOperations.js +53 -0
  49. package/src/handlers/helpers/errorHandler.js +49 -0
  50. package/src/handlers/helpers/index.js +106 -0
  51. package/src/handlers/helpers/lambdaWrapper.js +60 -0
  52. package/src/handlers/helpers/responseUtil.js +55 -0
  53. package/src/handlers/helpers/subscriptionTiers.js +1168 -0
  54. package/src/handlers/notifications/getPreferences.js +84 -0
  55. package/src/handlers/notifications/sendNotification.js +170 -0
  56. package/src/handlers/notifications/updatePreferences.js +316 -0
  57. package/src/handlers/patterns/patternUsagePost.js +182 -0
  58. package/src/handlers/patterns/patternViolationPost.js +185 -0
  59. package/src/handlers/projects/projectCreate.js +107 -0
  60. package/src/handlers/projects/projectDelete.js +82 -0
  61. package/src/handlers/projects/projectGet.js +95 -0
  62. package/src/handlers/projects/projectUpdate.js +118 -0
  63. package/src/handlers/reports/aiLeverage.js +206 -0
  64. package/src/handlers/reports/engineeringInvestment.js +132 -0
  65. package/src/handlers/reports/riskForecast.js +186 -0
  66. package/src/handlers/reports/standardsRoi.js +162 -0
  67. package/src/handlers/scheduled/analyzeCorrelations.js +178 -0
  68. package/src/handlers/scheduled/analyzeGitHistory.js +510 -0
  69. package/src/handlers/scheduled/generateAlerts.js +135 -0
  70. package/src/handlers/scheduled/refreshActivity.js +21 -0
  71. package/src/handlers/scheduled/scanCompliance.js +334 -0
  72. package/src/handlers/sessions/sessionEndPost.js +180 -0
  73. package/src/handlers/sessions/sessionStandardsPost.js +135 -0
  74. package/src/handlers/stripe/addonManagePost.js +240 -0
  75. package/src/handlers/stripe/billingPortalPost.js +93 -0
  76. package/src/handlers/stripe/enterpriseCheckoutPost.js +272 -0
  77. package/src/handlers/stripe/seatsUpdatePost.js +185 -0
  78. package/src/handlers/stripe/subscriptionCancelDelete.js +169 -0
  79. package/src/handlers/stripe/subscriptionCreatePost.js +221 -0
  80. package/src/handlers/stripe/subscriptionUpdatePut.js +163 -0
  81. package/src/handlers/stripe/webhookPost.js +454 -0
  82. package/src/handlers/users/cognitoPostConfirmation.js +150 -0
  83. package/src/handlers/users/userEntitlementsGet.js +89 -0
  84. package/src/handlers/users/userGet.js +114 -0
  85. package/src/handlers/webhooks/githubWebhook.js +223 -0
  86. package/src/index.js +969 -0
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@equilateral_ai/mindmeld",
3
+ "version": "3.0.0",
4
+ "description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "mindmeld": "./scripts/init-project.js"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "hooks/",
12
+ "scripts/init-project.js",
13
+ "scripts/inject.js",
14
+ "scripts/harvest.js",
15
+ "README.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "test": "jest",
22
+ "test:unit": "jest --selectProjects unit",
23
+ "test:integration": "jest --selectProjects integration",
24
+ "test:integration:hooks": "jest test/integration/hooks.integration.test.js",
25
+ "test:integration:database": "jest test/integration/database.integration.test.js",
26
+ "test:integration:multiagent": "jest test/integration/multiAgent.integration.test.js",
27
+ "test:integration:performance": "jest test/integration/performance.integration.test.js --testTimeout=120000",
28
+ "test:integration:all": "jest test/integration/ --runInBand",
29
+ "test:coverage": "jest --coverage",
30
+ "test:ci": "jest --ci --coverage --runInBand",
31
+ "build": "./scripts/build-handlers.sh",
32
+ "deploy:dev": "./scripts/deploy-dev.sh",
33
+ "deploy:prod": "./scripts/deploy-prod.sh",
34
+ "verify:dev": "./scripts/verify-deployment.sh dev",
35
+ "verify:prod": "./scripts/verify-deployment.sh prod",
36
+ "agents": "node scripts/rapport-agents-cli.js",
37
+ "test:hooks": "node scripts/test-claude-hooks.js",
38
+ "test:session-start": "node scripts/test-claude-hooks.js --session-start",
39
+ "test:pre-compact": "node scripts/test-claude-hooks.js --pre-compact",
40
+ "test:benchmark": "node scripts/test-claude-hooks.js --benchmark",
41
+ "test:relevance": "node scripts/test-relevance-detection.js",
42
+ "test:curation": "node scripts/test-curation-engine.js",
43
+ "test:validation": "node scripts/test-pattern-validation.js",
44
+ "test:orchestrator": "node scripts/test-orchestrator.js"
45
+ },
46
+ "claudeCode": {
47
+ "hooks": {
48
+ "sessionStart": "hooks/session-start.js",
49
+ "preCompact": "hooks/pre-compact.js"
50
+ },
51
+ "config": {
52
+ "standardsPath": ".equilateral-standards",
53
+ "maxContextLength": 50000,
54
+ "maxStandards": 10,
55
+ "sessionStartTimeout": 500
56
+ }
57
+ },
58
+ "keywords": [
59
+ "equilateral",
60
+ "mindmeld",
61
+ "collaboration",
62
+ "memory",
63
+ "context",
64
+ "team-learning",
65
+ "knowledge-management",
66
+ "ai-context",
67
+ "claude-code",
68
+ "standards",
69
+ "patterns"
70
+ ],
71
+ "author": "Jim Ford <jim@equilateral.ai>",
72
+ "license": "PROPRIETARY",
73
+ "repository": {
74
+ "type": "git",
75
+ "url": "git+https://github.com/jamesfordhq/rapport.git"
76
+ },
77
+ "homepage": "https://mindmeld.dev",
78
+ "dependencies": {
79
+ "@aws-sdk/client-bedrock-runtime": "^3.460.0",
80
+ "pg": "^8.11.3"
81
+ },
82
+ "devDependencies": {
83
+ "@aws-sdk/client-s3": "^3.460.0",
84
+ "@aws-sdk/client-ses": "^3.968.0",
85
+ "jest": "^29.7.0"
86
+ },
87
+ "engines": {
88
+ "node": ">=18.0.0"
89
+ }
90
+ }
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MindMeld CLI - Manually harvest patterns from recent git history
4
+ *
5
+ * For non-Claude-Code tools (Cursor, Aider, Ollama, etc.) that don't have
6
+ * a pre-compact hook. Analyzes recent commits to detect patterns.
7
+ *
8
+ * Usage:
9
+ * mindmeld harvest # Analyze last 10 commits
10
+ * mindmeld harvest --since 7d # Last 7 days
11
+ * mindmeld harvest --commits 20 # Last 20 commits
12
+ *
13
+ * @equilateral_ai/mindmeld v3.0.0
14
+ */
15
+
16
+ const path = require('path');
17
+ const fs = require('fs').promises;
18
+ const { exec } = require('child_process');
19
+ const { promisify } = require('util');
20
+
21
+ const execAsync = promisify(exec);
22
+
23
+ function showHarvestHelp() {
24
+ console.log(`
25
+ MindMeld harvest - Capture patterns from recent git history
26
+
27
+ Usage:
28
+ mindmeld harvest [options]
29
+
30
+ Options:
31
+ --since <period> Time period: 1d, 7d, 30d (default: 7d)
32
+ --commits <n> Number of commits to analyze (default: 10)
33
+ --path <dir> Project path (default: current directory)
34
+ --dry-run Show what would be captured without saving
35
+ --help, -h Show this help
36
+
37
+ Examples:
38
+ mindmeld harvest # Analyze last 7 days
39
+ mindmeld harvest --since 1d # Just today's commits
40
+ mindmeld harvest --commits 20 # Last 20 commits
41
+ mindmeld harvest --dry-run # Preview without saving
42
+
43
+ What it does:
44
+ 1. Reads recent git diffs and commit messages
45
+ 2. Detects repeated patterns (file types, naming, structure)
46
+ 3. Identifies correction patterns (reverts, fixes, amendments)
47
+ 4. Records detected patterns in .mindmeld/patterns/
48
+ 5. Patterns with enough repetition get promoted to standards
49
+ `);
50
+ }
51
+
52
+ /**
53
+ * Convert shorthand period (7d, 30d, 2w) to git-compatible format
54
+ */
55
+ function parseGitSince(since) {
56
+ const match = since.match(/^(\d+)(d|w|m)$/);
57
+ if (!match) return since; // pass through if already in git format
58
+ const [, num, unit] = match;
59
+ const units = { d: 'days', w: 'weeks', m: 'months' };
60
+ return `${num} ${units[unit] || 'days'} ago`;
61
+ }
62
+
63
+ /**
64
+ * Get recent git activity
65
+ */
66
+ async function getGitHistory(projectPath, options = {}) {
67
+ const since = parseGitSince(options.since || '7d');
68
+ const maxCommits = options.commits || 10;
69
+
70
+ // Get commit log with diffs
71
+ try {
72
+ const { stdout: log } = await execAsync(
73
+ `git log --since="${since}" -n ${maxCommits} --format="%H|%s|%an|%ai" --stat`,
74
+ { cwd: projectPath, maxBuffer: 1024 * 1024 }
75
+ );
76
+
77
+ const { stdout: diff } = await execAsync(
78
+ `git log --since="${since}" -n ${maxCommits} -p --diff-filter=M`,
79
+ { cwd: projectPath, maxBuffer: 5 * 1024 * 1024 }
80
+ );
81
+
82
+ return { log, diff };
83
+ } catch (error) {
84
+ if (error.message.includes('not a git repository')) {
85
+ throw new Error('not a git repository');
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Detect patterns from git history using regex analysis
93
+ */
94
+ function detectPatterns(gitHistory) {
95
+ const patterns = [];
96
+ const { log, diff } = gitHistory;
97
+
98
+ // 1. Detect fix/correction patterns (commits that fix previous work)
99
+ const fixPatterns = log.match(/fix[:\s]|correct[:\s]|revert[:\s]|wrong|mistake|should be|instead of/gi) || [];
100
+ if (fixPatterns.length >= 2) {
101
+ patterns.push({
102
+ type: 'correction',
103
+ element: 'Repeated corrections detected',
104
+ evidence: `${fixPatterns.length} fix/correction commits in recent history`,
105
+ confidence: Math.min(0.9, fixPatterns.length * 0.15),
106
+ category: 'workflow'
107
+ });
108
+ }
109
+
110
+ // 2. Detect file type patterns (what's being edited most)
111
+ const fileExtensions = {};
112
+ const fileMatches = log.match(/\S+\.\w+/g) || [];
113
+ for (const file of fileMatches) {
114
+ const ext = path.extname(file);
115
+ if (ext && ext.length < 6) {
116
+ fileExtensions[ext] = (fileExtensions[ext] || 0) + 1;
117
+ }
118
+ }
119
+
120
+ // 3. Detect code patterns from diffs
121
+ const codePatterns = [];
122
+
123
+ // Connection pool vs single client
124
+ if (diff.includes('createPool') || diff.includes('Pool(')) {
125
+ codePatterns.push({
126
+ type: 'anti-pattern',
127
+ element: 'Connection pool usage detected',
128
+ evidence: 'createPool or Pool() found in diffs',
129
+ confidence: 0.7,
130
+ category: 'database'
131
+ });
132
+ }
133
+
134
+ // Console.log removal (cleanup pattern)
135
+ const consoleAdds = (diff.match(/^\+.*console\.log/gm) || []).length;
136
+ const consoleRemoves = (diff.match(/^-.*console\.log/gm) || []).length;
137
+ if (consoleRemoves > consoleAdds && consoleRemoves >= 3) {
138
+ codePatterns.push({
139
+ type: 'pattern',
140
+ element: 'Console.log cleanup pattern',
141
+ evidence: `${consoleRemoves} console.log removals vs ${consoleAdds} additions`,
142
+ confidence: 0.6,
143
+ category: 'code-quality'
144
+ });
145
+ }
146
+
147
+ // Error handling patterns
148
+ const tryCatchAdds = (diff.match(/^\+.*try\s*\{/gm) || []).length;
149
+ if (tryCatchAdds >= 3) {
150
+ codePatterns.push({
151
+ type: 'pattern',
152
+ element: 'Error handling additions',
153
+ evidence: `${tryCatchAdds} try/catch blocks added`,
154
+ confidence: 0.5,
155
+ category: 'error-handling'
156
+ });
157
+ }
158
+
159
+ // Type annotation additions (TypeScript patterns)
160
+ const typeAnnotations = (diff.match(/^\+.*:\s*(string|number|boolean|any|void|Promise|Array)/gm) || []).length;
161
+ if (typeAnnotations >= 5) {
162
+ codePatterns.push({
163
+ type: 'pattern',
164
+ element: 'Type annotations being added',
165
+ evidence: `${typeAnnotations} type annotations in recent diffs`,
166
+ confidence: 0.6,
167
+ category: 'typescript'
168
+ });
169
+ }
170
+
171
+ // Import organization patterns
172
+ const importChanges = (diff.match(/^\+\s*import\s/gm) || []).length;
173
+ if (importChanges >= 5) {
174
+ codePatterns.push({
175
+ type: 'pattern',
176
+ element: 'Import reorganization',
177
+ evidence: `${importChanges} import statements modified`,
178
+ confidence: 0.4,
179
+ category: 'code-organization'
180
+ });
181
+ }
182
+
183
+ patterns.push(...codePatterns);
184
+
185
+ return patterns;
186
+ }
187
+
188
+ /**
189
+ * Save detected patterns to .mindmeld/patterns/
190
+ */
191
+ async function savePatterns(projectPath, patterns) {
192
+ const patternsDir = path.join(projectPath, '.mindmeld', 'patterns');
193
+ await fs.mkdir(patternsDir, { recursive: true });
194
+
195
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
196
+ const harvestFile = path.join(patternsDir, `harvest-${timestamp}.json`);
197
+
198
+ const harvest = {
199
+ timestamp: new Date().toISOString(),
200
+ source: 'manual-harvest',
201
+ patterns: patterns,
202
+ projectPath: projectPath,
203
+ projectName: path.basename(projectPath)
204
+ };
205
+
206
+ await fs.writeFile(harvestFile, JSON.stringify(harvest, null, 2));
207
+ return harvestFile;
208
+ }
209
+
210
+ /**
211
+ * Main harvest execution
212
+ */
213
+ async function harvest(options = {}) {
214
+ const projectPath = options.path || process.cwd();
215
+ const dryRun = options.dryRun || false;
216
+
217
+ console.error(`[MindMeld] Harvesting patterns from: ${projectPath}`);
218
+ console.error(`[MindMeld] Period: last ${options.since || '7d'}, max ${options.commits || 10} commits`);
219
+ console.error('');
220
+
221
+ // 1. Get git history
222
+ let gitHistory;
223
+ try {
224
+ gitHistory = await getGitHistory(projectPath, {
225
+ since: options.since || '7d',
226
+ commits: options.commits || 10
227
+ });
228
+ } catch (error) {
229
+ if (error.message === 'not a git repository') {
230
+ console.error('[MindMeld] Not a git repository. Harvest requires git history.');
231
+ process.exit(1);
232
+ }
233
+ throw error;
234
+ }
235
+
236
+ // 2. Detect patterns
237
+ const patterns = detectPatterns(gitHistory);
238
+
239
+ if (patterns.length === 0) {
240
+ console.error('[MindMeld] No patterns detected in recent history.');
241
+ console.error('[MindMeld] Try a longer period: mindmeld harvest --since 30d');
242
+ return;
243
+ }
244
+
245
+ // 3. Display results
246
+ console.error(`[MindMeld] Detected ${patterns.length} pattern(s):\n`);
247
+ for (const pattern of patterns) {
248
+ const confidence = (pattern.confidence * 100).toFixed(0);
249
+ console.error(` [${pattern.type}] ${pattern.element}`);
250
+ console.error(` Category: ${pattern.category} | Confidence: ${confidence}%`);
251
+ console.error(` Evidence: ${pattern.evidence}`);
252
+ console.error('');
253
+ }
254
+
255
+ // 4. Save (unless dry-run)
256
+ if (!dryRun) {
257
+ const savedPath = await savePatterns(projectPath, patterns);
258
+ console.error(`[MindMeld] Patterns saved to: ${path.relative(projectPath, savedPath)}`);
259
+ console.error(`[MindMeld] Patterns with 5+ detections will be promoted to provisional standards.`);
260
+ } else {
261
+ console.error('[MindMeld] Dry run - no patterns saved.');
262
+ }
263
+
264
+ // 5. Try LLM-powered detection if available
265
+ try {
266
+ const llmModule = require('../src/core/LLMPatternDetector');
267
+ if (llmModule && process.env.MINDMELD_USE_LLM !== 'false') {
268
+ console.error('\n[MindMeld] LLM-powered detection available for deeper analysis.');
269
+ console.error('[MindMeld] Set MINDMELD_USE_LLM=true for semantic pattern detection.');
270
+ }
271
+ } catch {
272
+ // LLM module not available
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Promote high-confidence patterns to local staging
278
+ * Writes to .mindmeld/patterns/{category}/ (local, never directly to community repo)
279
+ * Community promotion happens via API → PR to EquilateralAgents-Community-Standards
280
+ */
281
+ async function promotePatterns(projectPath, patterns, options = {}) {
282
+ const threshold = options.threshold || 0.5;
283
+ const standardsDir = path.join(projectPath, '.mindmeld', 'patterns');
284
+
285
+ const eligible = patterns.filter(p => p.confidence >= threshold);
286
+ if (eligible.length === 0) return [];
287
+
288
+ await fs.mkdir(standardsDir, { recursive: true });
289
+
290
+ const promoted = [];
291
+
292
+ for (const pattern of eligible) {
293
+ const categoryDir = path.join(standardsDir, pattern.category);
294
+ await fs.mkdir(categoryDir, { recursive: true });
295
+
296
+ const filename = pattern.element
297
+ .toLowerCase()
298
+ .replace(/[^a-z0-9]+/g, '_')
299
+ .replace(/^_|_$/g, '') + '.md';
300
+
301
+ const filePath = path.join(categoryDir, filename);
302
+
303
+ // Don't overwrite existing standards
304
+ try {
305
+ await fs.access(filePath);
306
+ continue;
307
+ } catch {
308
+ // File doesn't exist, safe to create
309
+ }
310
+
311
+ const content = [
312
+ `# ${pattern.element}`,
313
+ '',
314
+ `**Status**: provisional`,
315
+ `**Category**: ${pattern.category}`,
316
+ `**Confidence**: ${(pattern.confidence * 100).toFixed(0)}%`,
317
+ `**Source**: git-harvest`,
318
+ `**Detected**: ${new Date().toISOString().split('T')[0]}`,
319
+ '',
320
+ '## Evidence',
321
+ '',
322
+ `- ${pattern.evidence}`,
323
+ '',
324
+ '## Guidance',
325
+ '',
326
+ pattern.type === 'anti-pattern'
327
+ ? `- Avoid: ${pattern.element}`
328
+ : `- Follow: ${pattern.element}`,
329
+ '',
330
+ '---',
331
+ '*Provisional standard - will be promoted to solidified after 5+ consistent detections.*'
332
+ ].join('\n');
333
+
334
+ await fs.writeFile(filePath, content);
335
+ promoted.push({ pattern, path: filePath });
336
+ }
337
+
338
+ return promoted;
339
+ }
340
+
341
+ /**
342
+ * Harvest decisions from Claude Code plan files
343
+ * Matches plans to project by correlating session timestamps with plan file mtimes
344
+ */
345
+ async function harvestPlans(projectPath) {
346
+ const os = require('os');
347
+ const homeDir = os.homedir();
348
+ const plansDir = path.join(homeDir, '.claude', 'plans');
349
+ const projectsDir = path.join(homeDir, '.claude', 'projects');
350
+
351
+ // Encode project path the same way Claude Code does
352
+ const encodedPath = projectPath.replace(/\//g, '-');
353
+ const sessionIndexPath = path.join(projectsDir, encodedPath, 'sessions-index.json');
354
+
355
+ // 1. Load sessions for this project
356
+ let sessions;
357
+ try {
358
+ const indexContent = await fs.readFile(sessionIndexPath, 'utf-8');
359
+ const index = JSON.parse(indexContent);
360
+ sessions = index.entries || [];
361
+ } catch {
362
+ return []; // No sessions found for this project
363
+ }
364
+
365
+ if (sessions.length === 0) return [];
366
+
367
+ // 2. Get time window from sessions (earliest created → now, since current session may not be indexed)
368
+ const sessionTimes = sessions.map(s => new Date(s.created).getTime());
369
+ const windowStart = Math.min(...sessionTimes);
370
+ const windowEnd = Date.now();
371
+
372
+ // 3. Find plan files within the session time window
373
+ let planFiles;
374
+ try {
375
+ const entries = await fs.readdir(plansDir);
376
+ planFiles = [];
377
+ for (const entry of entries) {
378
+ if (!entry.endsWith('.md')) continue;
379
+ const filePath = path.join(plansDir, entry);
380
+ const stat = await fs.stat(filePath);
381
+ const mtime = stat.mtimeMs;
382
+ if (mtime >= windowStart && mtime <= windowEnd) {
383
+ planFiles.push({ path: filePath, name: entry, mtime });
384
+ }
385
+ }
386
+ } catch {
387
+ return []; // No plans directory
388
+ }
389
+
390
+ // 4. Content-match: only keep plans that reference files/terms from this project
391
+ const projectName = path.basename(projectPath).toLowerCase();
392
+ const relevantPlans = [];
393
+
394
+ for (const planFile of planFiles) {
395
+ try {
396
+ const content = await fs.readFile(planFile.path, 'utf-8');
397
+ const contentLower = content.toLowerCase();
398
+
399
+ // Score relevance by checking project indicators
400
+ let relevance = 0;
401
+
402
+ // Project name match (strongest signal)
403
+ if (contentLower.includes(projectName)) relevance += 5;
404
+
405
+ // Check if plan mentions files that exist in this project
406
+ const fileRefs = content.match(/`([^`]+\.(js|ts|json|yaml|yml|py|sql))`/g) || [];
407
+ for (const ref of fileRefs.slice(0, 10)) {
408
+ const cleanRef = ref.replace(/`/g, '');
409
+ try {
410
+ await fs.access(path.join(projectPath, cleanRef));
411
+ relevance += 3; // File exists in this project
412
+ } catch {
413
+ // File doesn't exist here
414
+ }
415
+ }
416
+
417
+ // Check for project-specific paths
418
+ if (content.includes(projectPath)) relevance += 10;
419
+
420
+ if (relevance >= 3) {
421
+ planFile.relevance = relevance;
422
+ planFile.content = content;
423
+ relevantPlans.push(planFile);
424
+ }
425
+ } catch {
426
+ // Skip unreadable
427
+ }
428
+ }
429
+
430
+ planFiles = relevantPlans;
431
+
432
+ if (planFiles.length === 0) return [];
433
+
434
+ // 5. Extract decisions from matched plan files
435
+ const decisions = [];
436
+
437
+ for (const planFile of planFiles) {
438
+ try {
439
+ const content = planFile.content || await fs.readFile(planFile.path, 'utf-8');
440
+
441
+ // Extract "Decisions" sections
442
+ const decisionPatterns = [
443
+ /## Decisions?\s*(?:Confirmed|Made)?\s*\n([\s\S]*?)(?=\n## |\n---|\Z)/gi,
444
+ /\| Decision \|[\s\S]*?\n((?:\|[^\n]+\n)+)/gi
445
+ ];
446
+
447
+ for (const pattern of decisionPatterns) {
448
+ let match;
449
+ while ((match = pattern.exec(content)) !== null) {
450
+ const section = match[1] || match[0];
451
+ // Parse table rows or bullet points
452
+ const lines = section.split('\n').filter(l =>
453
+ l.includes('|') && !l.includes('---') && !l.includes('Decision') && l.trim().length > 5
454
+ );
455
+
456
+ for (const line of lines) {
457
+ const cells = line.split('|').map(c => c.trim()).filter(c => c.length > 0);
458
+ if (cells.length >= 2) {
459
+ decisions.push({
460
+ type: 'decision',
461
+ element: cells[0].replace(/\*\*/g, ''),
462
+ evidence: cells[1].replace(/\*\*/g, ''),
463
+ confidence: 0.8,
464
+ category: 'architecture',
465
+ source: planFile.name
466
+ });
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ // Extract "Recommendation:" lines
473
+ const recommendations = content.match(/\*\*Recommendation:?\*\*:?\s*(.+)/gi) || [];
474
+ for (const rec of recommendations) {
475
+ const cleaned = rec.replace(/\*\*Recommendation:?\*\*:?\s*/i, '').trim();
476
+ if (cleaned.length > 10) {
477
+ decisions.push({
478
+ type: 'decision',
479
+ element: cleaned,
480
+ evidence: `From plan: ${planFile.name}`,
481
+ confidence: 0.7,
482
+ category: 'architecture',
483
+ source: planFile.name
484
+ });
485
+ }
486
+ }
487
+ } catch {
488
+ // Skip unreadable plans
489
+ }
490
+ }
491
+
492
+ // Deduplicate by element name (keep longest evidence)
493
+ const unique = {};
494
+ for (const d of decisions) {
495
+ if (!unique[d.element] || d.evidence.length > unique[d.element].evidence.length) {
496
+ unique[d.element] = d;
497
+ }
498
+ }
499
+
500
+ return Object.values(unique);
501
+ }
502
+
503
+ /**
504
+ * Promote plan decisions to standards
505
+ * Always writes to .mindmeld/decisions/ (local staging)
506
+ * Community promotion happens via API → PR to EquilateralAgents-Community-Standards
507
+ */
508
+ async function promoteDecisions(projectPath, decisions, options = {}) {
509
+ if (decisions.length === 0) return [];
510
+
511
+ const decisionsDir = path.join(projectPath, '.mindmeld', 'decisions');
512
+ await fs.mkdir(decisionsDir, { recursive: true });
513
+
514
+ const promoted = [];
515
+
516
+ for (const decision of decisions) {
517
+ const filename = decision.element
518
+ .toLowerCase()
519
+ .replace(/[^a-z0-9]+/g, '_')
520
+ .replace(/^_|_$/g, '')
521
+ .substring(0, 50) + '.md';
522
+
523
+ const filePath = path.join(decisionsDir, filename);
524
+
525
+ // Don't overwrite existing
526
+ try {
527
+ await fs.access(filePath);
528
+ continue;
529
+ } catch {
530
+ // Safe to create
531
+ }
532
+
533
+ const content = [
534
+ `# ${decision.element}`,
535
+ '',
536
+ `**Status**: solidified`,
537
+ `**Category**: ${decision.category}`,
538
+ `**Confidence**: ${(decision.confidence * 100).toFixed(0)}%`,
539
+ `**Source**: plan-harvest`,
540
+ `**Detected**: ${new Date().toISOString().split('T')[0]}`,
541
+ '',
542
+ '## Decision',
543
+ '',
544
+ `- ${decision.evidence}`,
545
+ '',
546
+ '## Context',
547
+ '',
548
+ `Extracted from: ${decision.source}`,
549
+ '',
550
+ '---',
551
+ '*Architectural decision - do not revisit without explicit discussion.*'
552
+ ].join('\n');
553
+
554
+ await fs.writeFile(filePath, content);
555
+ promoted.push({ decision, path: filePath });
556
+ }
557
+
558
+ return promoted;
559
+ }
560
+
561
+ module.exports = { harvest, promotePatterns, promoteDecisions, harvestPlans, getGitHistory, detectPatterns, savePatterns, showHarvestHelp };