@equilateral_ai/mindmeld 3.3.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-start.js +635 -41
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +12 -8
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
package/hooks/session-start.js
CHANGED
|
@@ -23,7 +23,7 @@ function generateFingerprint(userId, companyId, tier) {
|
|
|
23
23
|
user_id: userId || 'anonymous',
|
|
24
24
|
company_id: companyId || 'unknown',
|
|
25
25
|
timestamp: new Date().toISOString(),
|
|
26
|
-
subscription_tier: tier || '
|
|
26
|
+
subscription_tier: tier || 'enterprise'
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const base64Fingerprint = Buffer.from(JSON.stringify(fingerprint)).toString('base64');
|
|
@@ -81,6 +81,274 @@ function filterStandardsByPreferences(standards, preferences) {
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Detect git remote URL from current directory
|
|
86
|
+
* @returns {Promise<string|null>} Git remote URL or null
|
|
87
|
+
*/
|
|
88
|
+
async function detectGitRemote() {
|
|
89
|
+
const { execSync } = require('child_process');
|
|
90
|
+
try {
|
|
91
|
+
const remote = execSync('git remote get-url origin', {
|
|
92
|
+
cwd: process.cwd(),
|
|
93
|
+
encoding: 'utf-8',
|
|
94
|
+
timeout: 2000,
|
|
95
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
96
|
+
}).trim();
|
|
97
|
+
return remote || null;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Not a git repo or no remote
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get project name from git remote URL or directory name
|
|
106
|
+
* @param {string|null} gitRemote - Git remote URL
|
|
107
|
+
* @returns {string} Project name
|
|
108
|
+
*/
|
|
109
|
+
function getProjectNameFromRemote(gitRemote) {
|
|
110
|
+
if (gitRemote) {
|
|
111
|
+
// Extract repo name from URL (handles both HTTPS and SSH)
|
|
112
|
+
// https://github.com/user/repo.git -> repo
|
|
113
|
+
// git@github.com:user/repo.git -> repo
|
|
114
|
+
const match = gitRemote.match(/\/([^\/]+?)(\.git)?$/);
|
|
115
|
+
if (match) {
|
|
116
|
+
return match[1];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Fallback to directory name
|
|
120
|
+
return path.basename(process.cwd());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Auto-create project if it doesn't exist
|
|
125
|
+
* Called when .mindmeld/config.json is missing but user is authenticated
|
|
126
|
+
* @param {string} authToken - Auth token for API calls
|
|
127
|
+
* @param {string} apiUrl - API base URL
|
|
128
|
+
* @returns {Promise<Object|null>} Created/existing project config or null
|
|
129
|
+
*/
|
|
130
|
+
async function autoCreateProject(authToken, apiUrl) {
|
|
131
|
+
if (!authToken) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const https = require('https');
|
|
136
|
+
const url = require('url');
|
|
137
|
+
|
|
138
|
+
// 1. Detect git remote
|
|
139
|
+
const gitRemote = await detectGitRemote();
|
|
140
|
+
const projectName = getProjectNameFromRemote(gitRemote);
|
|
141
|
+
|
|
142
|
+
// 2. Get user's entitlements to find company
|
|
143
|
+
let companyId = null;
|
|
144
|
+
let clientId = null;
|
|
145
|
+
let userEmail = null;
|
|
146
|
+
let subscriptionTier = 'enterprise';
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const entitlementsResponse = await makeHttpRequest(
|
|
150
|
+
`${apiUrl}/api/users/entitlements`,
|
|
151
|
+
'GET',
|
|
152
|
+
null,
|
|
153
|
+
authToken,
|
|
154
|
+
3000
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (entitlementsResponse && entitlementsResponse.Records && entitlementsResponse.Records.length > 0) {
|
|
158
|
+
// Use first client's first company (user's primary organization)
|
|
159
|
+
const primaryClient = entitlementsResponse.Records[0];
|
|
160
|
+
clientId = primaryClient.client_id;
|
|
161
|
+
subscriptionTier = primaryClient.subscription_tier || 'enterprise';
|
|
162
|
+
|
|
163
|
+
if (primaryClient.companies && primaryClient.companies.length > 0) {
|
|
164
|
+
// Prefer a company where user is admin
|
|
165
|
+
const adminCompany = primaryClient.companies.find(c => c.admin);
|
|
166
|
+
const targetCompany = adminCompany || primaryClient.companies[0];
|
|
167
|
+
companyId = targetCompany.company_id;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Also get user email from /api/users/me
|
|
172
|
+
const userResponse = await makeHttpRequest(
|
|
173
|
+
`${apiUrl}/api/users/me`,
|
|
174
|
+
'GET',
|
|
175
|
+
null,
|
|
176
|
+
authToken,
|
|
177
|
+
2000
|
|
178
|
+
);
|
|
179
|
+
if (userResponse && userResponse.data) {
|
|
180
|
+
userEmail = userResponse.data.email_address || userResponse.data.Email_Address;
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('[MindMeld] Could not fetch user info:', e.message);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!companyId) {
|
|
187
|
+
// User not set up with a company yet
|
|
188
|
+
console.error('[MindMeld] User has no company. Complete onboarding at app.mindmeld.dev');
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 3. Check if project already exists
|
|
193
|
+
try {
|
|
194
|
+
const projectsResponse = await makeHttpRequest(
|
|
195
|
+
`${apiUrl}/api/projects?Company_ID=${encodeURIComponent(companyId)}`,
|
|
196
|
+
'GET',
|
|
197
|
+
null,
|
|
198
|
+
authToken,
|
|
199
|
+
3000
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (projectsResponse && projectsResponse.Records) {
|
|
203
|
+
// Look for existing project by name or repo URL
|
|
204
|
+
const existing = projectsResponse.Records.find(p =>
|
|
205
|
+
p.project_name === projectName ||
|
|
206
|
+
(gitRemote && p.repo_url === gitRemote)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (existing) {
|
|
210
|
+
// Project exists - save config locally and return
|
|
211
|
+
const config = {
|
|
212
|
+
projectId: existing.project_id,
|
|
213
|
+
projectName: existing.project_name,
|
|
214
|
+
companyId: existing.company_id,
|
|
215
|
+
userEmail: userEmail,
|
|
216
|
+
subscriptionTier: subscriptionTier,
|
|
217
|
+
repoUrl: existing.repo_url || gitRemote,
|
|
218
|
+
autoCreated: false
|
|
219
|
+
};
|
|
220
|
+
await saveLocalConfig(config);
|
|
221
|
+
console.error(`[MindMeld] Linked to existing project: ${existing.project_name}`);
|
|
222
|
+
return config;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('[MindMeld] Could not check existing projects:', e.message);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 4. Create new project
|
|
230
|
+
try {
|
|
231
|
+
const createPayload = {
|
|
232
|
+
Company_ID: companyId,
|
|
233
|
+
project_name: projectName,
|
|
234
|
+
description: `Auto-created from ${gitRemote || process.cwd()}`,
|
|
235
|
+
private: true,
|
|
236
|
+
repo_url: gitRemote
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const createResponse = await makeHttpRequest(
|
|
240
|
+
`${apiUrl}/api/projects`,
|
|
241
|
+
'POST',
|
|
242
|
+
createPayload,
|
|
243
|
+
authToken,
|
|
244
|
+
5000
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (createResponse && createResponse.Records && createResponse.Records.length > 0) {
|
|
248
|
+
const created = createResponse.Records[0];
|
|
249
|
+
const config = {
|
|
250
|
+
projectId: created.project_id,
|
|
251
|
+
projectName: created.project_name,
|
|
252
|
+
companyId: created.company_id,
|
|
253
|
+
userEmail: userEmail,
|
|
254
|
+
subscriptionTier: subscriptionTier,
|
|
255
|
+
repoUrl: gitRemote,
|
|
256
|
+
autoCreated: true,
|
|
257
|
+
createdAt: new Date().toISOString()
|
|
258
|
+
};
|
|
259
|
+
await saveLocalConfig(config);
|
|
260
|
+
console.error(`[MindMeld] Auto-created project: ${created.project_name}`);
|
|
261
|
+
return config;
|
|
262
|
+
} else if (createResponse && createResponse.error) {
|
|
263
|
+
console.error('[MindMeld] Project creation failed:', createResponse.error);
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.error('[MindMeld] Could not create project:', e.message);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Save project config locally to .mindmeld/config.json
|
|
274
|
+
* @param {Object} config - Project configuration
|
|
275
|
+
*/
|
|
276
|
+
async function saveLocalConfig(config) {
|
|
277
|
+
const configDir = path.join(process.cwd(), '.mindmeld');
|
|
278
|
+
const configPath = path.join(configDir, 'config.json');
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
282
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('[MindMeld] Could not save config:', error.message);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Make HTTP request helper
|
|
290
|
+
* @param {string} urlStr - Full URL
|
|
291
|
+
* @param {string} method - HTTP method
|
|
292
|
+
* @param {Object|null} body - Request body
|
|
293
|
+
* @param {string|null} authToken - Bearer token
|
|
294
|
+
* @param {number} timeout - Timeout in ms
|
|
295
|
+
* @returns {Promise<Object>} Response object
|
|
296
|
+
*/
|
|
297
|
+
function makeHttpRequest(urlStr, method, body, authToken, timeout = 5000) {
|
|
298
|
+
const https = require('https');
|
|
299
|
+
const url = require('url');
|
|
300
|
+
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
const parsedUrl = url.parse(urlStr);
|
|
303
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
304
|
+
|
|
305
|
+
const options = {
|
|
306
|
+
hostname: parsedUrl.hostname,
|
|
307
|
+
port: parsedUrl.port || 443,
|
|
308
|
+
path: parsedUrl.path,
|
|
309
|
+
method: method,
|
|
310
|
+
headers: {
|
|
311
|
+
'Content-Type': 'application/json',
|
|
312
|
+
'Accept': 'application/json'
|
|
313
|
+
},
|
|
314
|
+
timeout: timeout
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (authToken) {
|
|
318
|
+
options.headers['Authorization'] = `Bearer ${authToken}`;
|
|
319
|
+
}
|
|
320
|
+
if (payload) {
|
|
321
|
+
options.headers['Content-Length'] = Buffer.byteLength(payload);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const req = https.request(options, (res) => {
|
|
325
|
+
let data = '';
|
|
326
|
+
res.on('data', chunk => { data += chunk; });
|
|
327
|
+
res.on('end', () => {
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(data);
|
|
330
|
+
if (res.statusCode >= 400) {
|
|
331
|
+
resolve({ error: parsed.message || `HTTP ${res.statusCode}`, status: res.statusCode });
|
|
332
|
+
} else {
|
|
333
|
+
resolve(parsed);
|
|
334
|
+
}
|
|
335
|
+
} catch (e) {
|
|
336
|
+
resolve({ error: 'Invalid JSON response' });
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
req.on('error', (err) => resolve({ error: err.message }));
|
|
342
|
+
req.on('timeout', () => {
|
|
343
|
+
req.destroy();
|
|
344
|
+
resolve({ error: 'Request timeout' });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (payload) req.write(payload);
|
|
348
|
+
req.end();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
84
352
|
/**
|
|
85
353
|
* Load user and company info from MindMeld config
|
|
86
354
|
* @returns {Promise<{userId: string, companyId: string, tier: string}>}
|
|
@@ -91,10 +359,11 @@ async function loadFingerprintConfig() {
|
|
|
91
359
|
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
92
360
|
const config = JSON.parse(configContent);
|
|
93
361
|
|
|
362
|
+
// Support both snake_case and camelCase keys for backwards compatibility
|
|
94
363
|
return {
|
|
95
|
-
userId: config.user_id || process.env.MINDMELD_USER_ID || 'anonymous',
|
|
96
|
-
companyId: config.company_id || process.env.MINDMELD_COMPANY_ID || 'unknown',
|
|
97
|
-
tier: config.subscription_tier || process.env.MINDMELD_TIER || '
|
|
364
|
+
userId: config.user_id || config.userId || config.userEmail || process.env.MINDMELD_USER_ID || 'anonymous',
|
|
365
|
+
companyId: config.company_id || config.companyId || process.env.MINDMELD_COMPANY_ID || 'unknown',
|
|
366
|
+
tier: config.subscription_tier || config.subscriptionTier || process.env.MINDMELD_TIER || 'enterprise'
|
|
98
367
|
};
|
|
99
368
|
} catch (error) {
|
|
100
369
|
// Expected: config doesn't exist or invalid JSON
|
|
@@ -105,7 +374,7 @@ async function loadFingerprintConfig() {
|
|
|
105
374
|
return {
|
|
106
375
|
userId: process.env.MINDMELD_USER_ID || 'anonymous',
|
|
107
376
|
companyId: process.env.MINDMELD_COMPANY_ID || 'unknown',
|
|
108
|
-
tier: process.env.MINDMELD_TIER || '
|
|
377
|
+
tier: process.env.MINDMELD_TIER || 'enterprise'
|
|
109
378
|
};
|
|
110
379
|
}
|
|
111
380
|
}
|
|
@@ -153,10 +422,27 @@ async function loadAuthToken() {
|
|
|
153
422
|
}
|
|
154
423
|
|
|
155
424
|
/**
|
|
156
|
-
* Load Cognito config
|
|
425
|
+
* Load Cognito config
|
|
426
|
+
* Priority: .mindmeld/config.json → .myworld.json → production defaults
|
|
157
427
|
* @returns {Promise<Object>} Cognito constructor options or empty object (uses prod defaults)
|
|
158
428
|
*/
|
|
159
429
|
async function loadCognitoConfig() {
|
|
430
|
+
// 1. Check .mindmeld/config.json first (always points to prod)
|
|
431
|
+
try {
|
|
432
|
+
const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
433
|
+
const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
|
|
434
|
+
const config = JSON.parse(content);
|
|
435
|
+
if (config.auth?.cognitoDomain && config.auth?.cognitoClientId) {
|
|
436
|
+
return {
|
|
437
|
+
cognitoDomain: config.auth.cognitoDomain,
|
|
438
|
+
cognitoClientId: config.auth.cognitoClientId
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
// No .mindmeld/config.json or no auth section
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 2. Fallback to .myworld.json (may point to dev — for backwards compatibility)
|
|
160
446
|
try {
|
|
161
447
|
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
162
448
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
@@ -198,20 +484,33 @@ function spawnBackgroundLogin() {
|
|
|
198
484
|
}
|
|
199
485
|
|
|
200
486
|
/**
|
|
201
|
-
* Load API
|
|
202
|
-
*
|
|
487
|
+
* Load API configuration
|
|
488
|
+
* Priority: .mindmeld/config.json → .myworld.json → env var → production default
|
|
489
|
+
* The hook should always talk to the production MindMeld API for standards/patterns,
|
|
490
|
+
* regardless of which environment the developer is building against.
|
|
491
|
+
* @returns {Promise<{apiUrl: string}>}
|
|
203
492
|
*/
|
|
204
493
|
async function loadApiConfig() {
|
|
494
|
+
// 1. Check .mindmeld/config.json first (always points to prod)
|
|
495
|
+
try {
|
|
496
|
+
const mindmeldConfigPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
497
|
+
const content = await fs.readFile(mindmeldConfigPath, 'utf-8');
|
|
498
|
+
const config = JSON.parse(content);
|
|
499
|
+
if (config.apiUrl) {
|
|
500
|
+
return { apiUrl: config.apiUrl };
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
// No .mindmeld/config.json or no apiUrl
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 2. Fallback to .myworld.json (may point to dev)
|
|
205
507
|
try {
|
|
206
508
|
const configPath = path.join(process.cwd(), '.myworld.json');
|
|
207
509
|
const content = await fs.readFile(configPath, 'utf-8');
|
|
208
510
|
const config = JSON.parse(content);
|
|
209
511
|
const backend = config.deployments?.backend;
|
|
210
|
-
const auth = backend?.auth;
|
|
211
512
|
return {
|
|
212
|
-
apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev'
|
|
213
|
-
cognitoDomain: auth?.domain ? `${auth.domain}.auth.us-east-2.amazoncognito.com` : undefined,
|
|
214
|
-
cognitoClientId: auth?.client_id || undefined
|
|
513
|
+
apiUrl: backend?.api?.base_url || 'https://api.mindmeld.dev'
|
|
215
514
|
};
|
|
216
515
|
} catch (error) {
|
|
217
516
|
return {
|
|
@@ -287,6 +586,79 @@ async function fetchRelevantStandardsFromAPI(apiUrl, authToken, characteristics,
|
|
|
287
586
|
});
|
|
288
587
|
}
|
|
289
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Fetch weekly splash data from API
|
|
591
|
+
* Returns splash summary with stats, or null if already acknowledged
|
|
592
|
+
*/
|
|
593
|
+
async function fetchSplashData(apiUrl, authToken) {
|
|
594
|
+
if (!authToken) return null;
|
|
595
|
+
|
|
596
|
+
const https = require('https');
|
|
597
|
+
const url = require('url');
|
|
598
|
+
const parsedUrl = url.parse(`${apiUrl}/api/user/splash`);
|
|
599
|
+
|
|
600
|
+
const options = {
|
|
601
|
+
hostname: parsedUrl.hostname,
|
|
602
|
+
port: parsedUrl.port || 443,
|
|
603
|
+
path: parsedUrl.path,
|
|
604
|
+
method: 'GET',
|
|
605
|
+
headers: {
|
|
606
|
+
'Authorization': `Bearer ${authToken}`
|
|
607
|
+
},
|
|
608
|
+
timeout: 3000
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
return new Promise((resolve) => {
|
|
612
|
+
const req = https.request(options, (res) => {
|
|
613
|
+
let data = '';
|
|
614
|
+
res.on('data', chunk => { data += chunk; });
|
|
615
|
+
res.on('end', () => {
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(data);
|
|
618
|
+
resolve(parsed.data || parsed);
|
|
619
|
+
} catch (e) {
|
|
620
|
+
resolve(null);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
req.on('error', () => resolve(null));
|
|
626
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
627
|
+
req.end();
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Acknowledge splash (fire-and-forget) to prevent showing again this week
|
|
633
|
+
*/
|
|
634
|
+
function acknowledgeSplash(apiUrl, authToken, weekStart) {
|
|
635
|
+
if (!authToken || !weekStart) return;
|
|
636
|
+
|
|
637
|
+
const https = require('https');
|
|
638
|
+
const url = require('url');
|
|
639
|
+
const payload = JSON.stringify({ week_start: weekStart });
|
|
640
|
+
const parsedUrl = url.parse(`${apiUrl}/api/user/splash/acknowledge`);
|
|
641
|
+
|
|
642
|
+
const options = {
|
|
643
|
+
hostname: parsedUrl.hostname,
|
|
644
|
+
port: parsedUrl.port || 443,
|
|
645
|
+
path: parsedUrl.path,
|
|
646
|
+
method: 'POST',
|
|
647
|
+
headers: {
|
|
648
|
+
'Content-Type': 'application/json',
|
|
649
|
+
'Authorization': `Bearer ${authToken}`,
|
|
650
|
+
'Content-Length': Buffer.byteLength(payload)
|
|
651
|
+
},
|
|
652
|
+
timeout: 3000
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const req = https.request(options, () => {}); // fire-and-forget
|
|
656
|
+
req.on('error', () => {}); // silently ignore
|
|
657
|
+
req.on('timeout', () => req.destroy());
|
|
658
|
+
req.write(payload);
|
|
659
|
+
req.end();
|
|
660
|
+
}
|
|
661
|
+
|
|
290
662
|
/**
|
|
291
663
|
* Main hook execution
|
|
292
664
|
*/
|
|
@@ -294,19 +666,39 @@ async function injectContext() {
|
|
|
294
666
|
const startTime = Date.now();
|
|
295
667
|
|
|
296
668
|
try {
|
|
297
|
-
//
|
|
298
|
-
|
|
669
|
+
// Check if MindMeld is configured for this project
|
|
670
|
+
let hasMindmeld = await checkMindmeldConfiguration();
|
|
671
|
+
|
|
672
|
+
// If not configured, try auto-create for authenticated users
|
|
299
673
|
if (!hasMindmeld) {
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
//
|
|
674
|
+
// Load auth token and API config first
|
|
675
|
+
const [authToken, apiConfig] = await Promise.all([
|
|
676
|
+
loadAuthToken(),
|
|
677
|
+
loadApiConfig()
|
|
678
|
+
]);
|
|
679
|
+
|
|
680
|
+
if (authToken) {
|
|
681
|
+
// User is authenticated - try to auto-create project
|
|
682
|
+
console.error('[MindMeld] No project config found. Auto-detecting...');
|
|
683
|
+
const autoCreated = await autoCreateProject(authToken, apiConfig.apiUrl);
|
|
684
|
+
if (autoCreated) {
|
|
685
|
+
hasMindmeld = true; // Config now exists
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
// Not authenticated - check if they have auth file but token expired
|
|
689
|
+
try {
|
|
690
|
+
const os = require('os');
|
|
691
|
+
const authPath = path.join(os.homedir(), '.mindmeld', 'auth.json');
|
|
692
|
+
await fs.access(authPath);
|
|
693
|
+
console.error('[MindMeld] Auth token expired. Run "mindmeld login" to refresh.');
|
|
694
|
+
} catch (e) {
|
|
695
|
+
// No auth file — not a subscriber, stay silent
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!hasMindmeld) {
|
|
700
|
+
return '';
|
|
308
701
|
}
|
|
309
|
-
return '';
|
|
310
702
|
}
|
|
311
703
|
|
|
312
704
|
// 1. Parallel local reads (no network, no DB)
|
|
@@ -334,27 +726,54 @@ async function injectContext() {
|
|
|
334
726
|
|
|
335
727
|
const sessionId = mindmeld.generateSessionId();
|
|
336
728
|
|
|
729
|
+
// 3b. Persist session context for pre-compact hook
|
|
730
|
+
// Pre-compact creates a new MindmeldClient and needs project context
|
|
731
|
+
try {
|
|
732
|
+
const sessionContext = {
|
|
733
|
+
sessionId: sessionId,
|
|
734
|
+
projectId: context.projectId,
|
|
735
|
+
projectName: context.projectName,
|
|
736
|
+
companyId: context.companyId,
|
|
737
|
+
userEmail: context.userEmail,
|
|
738
|
+
startedAt: new Date().toISOString()
|
|
739
|
+
};
|
|
740
|
+
const mindmeldDir = path.join(process.cwd(), '.mindmeld');
|
|
741
|
+
await fs.mkdir(mindmeldDir, { recursive: true });
|
|
742
|
+
await fs.writeFile(
|
|
743
|
+
path.join(mindmeldDir, 'current-session.json'),
|
|
744
|
+
JSON.stringify(sessionContext, null, 2)
|
|
745
|
+
);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error('[MindMeld] Could not persist session context (non-fatal):', err.message);
|
|
748
|
+
}
|
|
749
|
+
|
|
337
750
|
// 4. Detect project characteristics locally (file system only, no DB)
|
|
338
751
|
const characteristics = await mindmeld.relevanceDetector.detectProjectCharacteristics();
|
|
339
752
|
|
|
340
|
-
// 5. Parallel API calls: standards + team context
|
|
341
|
-
const [standardsResult, projectContextResult] = await Promise.allSettled([
|
|
753
|
+
// 5. Parallel API calls: standards + team context + splash
|
|
754
|
+
const [standardsResult, projectContextResult, splashResult] = await Promise.allSettled([
|
|
342
755
|
fetchRelevantStandardsFromAPI(apiConfig.apiUrl, authToken, characteristics, context.projectId, preferences),
|
|
343
|
-
mindmeld.loadProjectContext(context.projectId)
|
|
756
|
+
mindmeld.loadProjectContext(context.projectId),
|
|
757
|
+
fetchSplashData(apiConfig.apiUrl, authToken)
|
|
344
758
|
]);
|
|
345
759
|
|
|
346
|
-
// 6. Resolve standards: API → file-based fallback
|
|
760
|
+
// 6. Resolve standards: API → file-based fallback (only for network errors, not subscription blocks)
|
|
347
761
|
let relevantStandards = [];
|
|
348
762
|
if (standardsResult.status === 'fulfilled' && standardsResult.value.length > 0) {
|
|
349
763
|
relevantStandards = standardsResult.value;
|
|
350
764
|
console.error(`[MindMeld] ${relevantStandards.length} standards from API`);
|
|
765
|
+
} else if (standardsResult.status === 'rejected' &&
|
|
766
|
+
standardsResult.reason.message.includes('subscription required')) {
|
|
767
|
+
// Subscription enforcement — do NOT fall through to file-based injection
|
|
768
|
+
console.error('[MindMeld] Active subscription required. Subscribe at app.mindmeld.dev');
|
|
769
|
+
return '';
|
|
351
770
|
} else {
|
|
352
771
|
if (standardsResult.status === 'rejected') {
|
|
353
772
|
console.error(`[MindMeld] API fallback: ${standardsResult.reason.message}`);
|
|
354
773
|
}
|
|
355
774
|
const categories = mindmeld.relevanceDetector.mapCharacteristicsToCategories(characteristics);
|
|
356
|
-
relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories);
|
|
357
|
-
console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback`);
|
|
775
|
+
relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories, characteristics);
|
|
776
|
+
console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback (scored)`);
|
|
358
777
|
}
|
|
359
778
|
|
|
360
779
|
// 7. Filter by project preferences
|
|
@@ -367,7 +786,7 @@ async function injectContext() {
|
|
|
367
786
|
relevantStandards = relevantStandards.slice(0, 10);
|
|
368
787
|
|
|
369
788
|
// 8. Record standards shown (fire-and-forget, non-blocking)
|
|
370
|
-
mindmeld.recordStandardsShown(sessionId, relevantStandards);
|
|
789
|
+
mindmeld.recordStandardsShown(sessionId, relevantStandards, context.projectId, context.userEmail);
|
|
371
790
|
|
|
372
791
|
// 9. Resolve team context from parallel API call
|
|
373
792
|
const projectContext = projectContextResult.status === 'fulfilled' ? projectContextResult.value : null;
|
|
@@ -378,7 +797,15 @@ async function injectContext() {
|
|
|
378
797
|
? projectContext.recentLearning
|
|
379
798
|
: [];
|
|
380
799
|
|
|
381
|
-
// 10.
|
|
800
|
+
// 10. Resolve splash data
|
|
801
|
+
const splashData = splashResult.status === 'fulfilled' ? splashResult.value : null;
|
|
802
|
+
|
|
803
|
+
// Auto-acknowledge splash (fire-and-forget) so it doesn't show again this week
|
|
804
|
+
if (splashData && splashData.show_splash && splashData.summary) {
|
|
805
|
+
acknowledgeSplash(apiConfig.apiUrl, authToken, splashData.summary.week_start);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// 11. Build context injection with fingerprint
|
|
382
809
|
const injection = formatContextInjection({
|
|
383
810
|
project: context.projectName,
|
|
384
811
|
sessionId: sessionId,
|
|
@@ -386,9 +813,17 @@ async function injectContext() {
|
|
|
386
813
|
relevantStandards: relevantStandards,
|
|
387
814
|
teamPatterns: teamPatterns,
|
|
388
815
|
recentLearning: recentLearning,
|
|
389
|
-
fingerprint: fingerprintConfig
|
|
816
|
+
fingerprint: fingerprintConfig,
|
|
817
|
+
splash: splashData
|
|
390
818
|
});
|
|
391
819
|
|
|
820
|
+
// 12. Cache condensed context for subagent injection
|
|
821
|
+
try {
|
|
822
|
+
await cacheSubagentContext(relevantStandards, teamPatterns, context.projectName);
|
|
823
|
+
} catch (cacheError) {
|
|
824
|
+
console.error('[MindMeld] Subagent context cache failed (non-fatal):', cacheError.message);
|
|
825
|
+
}
|
|
826
|
+
|
|
392
827
|
const elapsed = Date.now() - startTime;
|
|
393
828
|
console.error(`[MindMeld] Context injected in ${elapsed}ms`);
|
|
394
829
|
|
|
@@ -418,6 +853,46 @@ async function checkMindmeldConfiguration() {
|
|
|
418
853
|
}
|
|
419
854
|
}
|
|
420
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Cache condensed context for subagent injection
|
|
858
|
+
* Subagents don't get session-start hooks, so we cache the essentials
|
|
859
|
+
* to a file that SubagentStart can read instantly (< 50ms)
|
|
860
|
+
*
|
|
861
|
+
* @param {Array} standards - Relevant standards from API
|
|
862
|
+
* @param {Array} teamPatterns - Team patterns
|
|
863
|
+
* @param {string} projectName - Current project name
|
|
864
|
+
*/
|
|
865
|
+
async function cacheSubagentContext(standards, teamPatterns, projectName) {
|
|
866
|
+
const mindmeldDir = path.join(process.cwd(), '.mindmeld');
|
|
867
|
+
const cachePath = path.join(mindmeldDir, 'subagent-context.md');
|
|
868
|
+
|
|
869
|
+
const sections = [];
|
|
870
|
+
sections.push(`# MindMeld Standards Context (${projectName})`);
|
|
871
|
+
sections.push('');
|
|
872
|
+
sections.push('Follow these standards when writing or modifying code:');
|
|
873
|
+
sections.push('');
|
|
874
|
+
|
|
875
|
+
// Include standards as compact rules (no examples/anti-patterns to save tokens)
|
|
876
|
+
if (standards && standards.length > 0) {
|
|
877
|
+
for (const s of standards) {
|
|
878
|
+
sections.push(`- **${s.element}** [${s.category}]: ${s.rule}`);
|
|
879
|
+
}
|
|
880
|
+
sections.push('');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Include team patterns as compact rules
|
|
884
|
+
if (teamPatterns && teamPatterns.length > 0) {
|
|
885
|
+
sections.push('## Team Patterns');
|
|
886
|
+
for (const p of teamPatterns) {
|
|
887
|
+
sections.push(`- **${p.element}** (${(p.correlation * 100).toFixed(0)}% correlation): ${p.rule}`);
|
|
888
|
+
}
|
|
889
|
+
sections.push('');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
await fs.writeFile(cachePath, sections.join('\n'));
|
|
893
|
+
console.error(`[MindMeld] Cached subagent context (${sections.length} lines)`);
|
|
894
|
+
}
|
|
895
|
+
|
|
421
896
|
/**
|
|
422
897
|
* Format context injection for Claude Code
|
|
423
898
|
* @param {object} data - Context data to inject
|
|
@@ -431,7 +906,8 @@ function formatContextInjection(data) {
|
|
|
431
906
|
relevantStandards,
|
|
432
907
|
teamPatterns,
|
|
433
908
|
recentLearning,
|
|
434
|
-
fingerprint
|
|
909
|
+
fingerprint,
|
|
910
|
+
splash
|
|
435
911
|
} = data;
|
|
436
912
|
|
|
437
913
|
const sections = [];
|
|
@@ -453,6 +929,34 @@ function formatContextInjection(data) {
|
|
|
453
929
|
sections.push('Licensed for use within MindMeld platform only. Redistribution prohibited.');
|
|
454
930
|
sections.push('');
|
|
455
931
|
|
|
932
|
+
// Weekly splash (shown once per week)
|
|
933
|
+
if (splash && splash.show_splash) {
|
|
934
|
+
if (splash.is_greenfield && splash.tips && splash.tips.length > 0) {
|
|
935
|
+
sections.push('## Getting Started with MindMeld');
|
|
936
|
+
for (const tip of splash.tips) {
|
|
937
|
+
sections.push(`- ${tip}`);
|
|
938
|
+
}
|
|
939
|
+
sections.push('');
|
|
940
|
+
} else if (splash.summary) {
|
|
941
|
+
const s = splash.summary;
|
|
942
|
+
const formatDate = (dateStr) => {
|
|
943
|
+
const d = new Date(dateStr + 'T00:00:00Z');
|
|
944
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
|
945
|
+
};
|
|
946
|
+
const hours = Math.floor(s.total_duration_minutes / 60);
|
|
947
|
+
const mins = s.total_duration_minutes % 60;
|
|
948
|
+
const duration = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
949
|
+
|
|
950
|
+
sections.push(`## Weekly Recap (${formatDate(s.week_start)} - ${formatDate(s.week_end)})`);
|
|
951
|
+
sections.push(`${s.sessions_count} sessions | ${duration} total | ${s.active_projects} project${s.active_projects !== 1 ? 's' : ''}`);
|
|
952
|
+
sections.push(`${s.standards_injected} standards injected, ${s.standards_followed} followed, ${s.violations_detected} violation${s.violations_detected !== 1 ? 's' : ''}`);
|
|
953
|
+
if (s.patterns_harvested > 0) {
|
|
954
|
+
sections.push(`${s.patterns_harvested} patterns harvested`);
|
|
955
|
+
}
|
|
956
|
+
sections.push('');
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
456
960
|
// Collaborators
|
|
457
961
|
if (collaborators && collaborators.length > 0) {
|
|
458
962
|
sections.push('## Team');
|
|
@@ -460,30 +964,120 @@ function formatContextInjection(data) {
|
|
|
460
964
|
sections.push('');
|
|
461
965
|
}
|
|
462
966
|
|
|
463
|
-
//
|
|
967
|
+
// Separate standards and workflows
|
|
968
|
+
const standards = [];
|
|
969
|
+
const workflows = [];
|
|
970
|
+
|
|
464
971
|
if (relevantStandards && relevantStandards.length > 0) {
|
|
972
|
+
for (const item of relevantStandards) {
|
|
973
|
+
// Detect workflows: check type flag, rule prefix, keywords, or structured examples
|
|
974
|
+
const isWorkflow = item.type === 'workflow' ||
|
|
975
|
+
(item.rule && item.rule.startsWith('WORKFLOW:')) ||
|
|
976
|
+
(Array.isArray(item.keywords) && item.keywords.includes('workflow') && item.examples && item.examples[0]?.type === 'workflow') ||
|
|
977
|
+
(item.tags && item.tags.includes('workflow') && item.examples && item.examples[0]?.type === 'workflow');
|
|
978
|
+
|
|
979
|
+
if (isWorkflow) {
|
|
980
|
+
workflows.push(item);
|
|
981
|
+
} else {
|
|
982
|
+
standards.push(item);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Relevant standards
|
|
988
|
+
if (standards.length > 0) {
|
|
465
989
|
sections.push('## Relevant Standards');
|
|
466
990
|
sections.push('');
|
|
467
991
|
|
|
468
|
-
for (const standard of
|
|
992
|
+
for (const standard of standards) {
|
|
469
993
|
sections.push(`### ${standard.element}`);
|
|
470
994
|
sections.push(`**Category**: ${standard.category}`);
|
|
471
995
|
// Add fingerprint to rule text
|
|
472
996
|
sections.push(`**Rule**: ${standard.rule} ${fingerprintStr}`);
|
|
473
997
|
|
|
474
998
|
if (standard.examples && standard.examples.length > 0) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
999
|
+
const example = standard.examples[0];
|
|
1000
|
+
const exampleCode = typeof example === 'string' ? example : (example?.code || example?.description || '');
|
|
1001
|
+
if (exampleCode) {
|
|
1002
|
+
sections.push('');
|
|
1003
|
+
sections.push('**Example**:');
|
|
1004
|
+
sections.push('```javascript');
|
|
1005
|
+
sections.push(exampleCode);
|
|
1006
|
+
sections.push('```');
|
|
1007
|
+
}
|
|
480
1008
|
}
|
|
481
1009
|
|
|
482
1010
|
if (standard.anti_patterns && standard.anti_patterns.length > 0) {
|
|
483
1011
|
sections.push('');
|
|
484
1012
|
sections.push('**Anti-patterns**:');
|
|
485
1013
|
for (const antiPattern of standard.anti_patterns) {
|
|
486
|
-
|
|
1014
|
+
const desc = typeof antiPattern === 'string' ? antiPattern : (antiPattern?.description || '');
|
|
1015
|
+
if (desc) {
|
|
1016
|
+
sections.push(`- ❌ ${desc}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
sections.push('');
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Workflows (rendered as ordered procedures)
|
|
1026
|
+
if (workflows.length > 0) {
|
|
1027
|
+
sections.push('## Workflows');
|
|
1028
|
+
sections.push('');
|
|
1029
|
+
|
|
1030
|
+
for (const workflow of workflows) {
|
|
1031
|
+
// Extract workflow data from examples[0]
|
|
1032
|
+
const workflowData = workflow.examples && workflow.examples[0]?.type === 'workflow'
|
|
1033
|
+
? workflow.examples[0]
|
|
1034
|
+
: null;
|
|
1035
|
+
|
|
1036
|
+
sections.push(`### ${workflow.element} ${fingerprintStr}`);
|
|
1037
|
+
sections.push(`**Category**: ${workflow.category}`);
|
|
1038
|
+
|
|
1039
|
+
if (workflowData) {
|
|
1040
|
+
sections.push(`**When**: ${workflowData.trigger}`);
|
|
1041
|
+
sections.push('');
|
|
1042
|
+
|
|
1043
|
+
// Preconditions
|
|
1044
|
+
if (workflowData.preconditions && workflowData.preconditions.length > 0) {
|
|
1045
|
+
sections.push('**Preconditions**:');
|
|
1046
|
+
for (const pre of workflowData.preconditions) {
|
|
1047
|
+
sections.push(`- ${pre}`);
|
|
1048
|
+
}
|
|
1049
|
+
sections.push('');
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Steps
|
|
1053
|
+
if (workflowData.steps && workflowData.steps.length > 0) {
|
|
1054
|
+
sections.push('**Steps**:');
|
|
1055
|
+
for (const step of workflowData.steps) {
|
|
1056
|
+
const gateMarker = step.gate ? ' (GATE)' : '';
|
|
1057
|
+
sections.push(`${step.index}. **${step.name}**${gateMarker} — ${step.description}`);
|
|
1058
|
+
|
|
1059
|
+
if (step.command) {
|
|
1060
|
+
// Clean up multiline commands for display
|
|
1061
|
+
const cmd = step.command.trim().split('\n')[0];
|
|
1062
|
+
sections.push(` \`${cmd}\``);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (step.validation) {
|
|
1066
|
+
sections.push(` Verify: ${step.validation}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
sections.push('');
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Anti-patterns
|
|
1074
|
+
if (workflow.anti_patterns && workflow.anti_patterns.length > 0) {
|
|
1075
|
+
sections.push('**Anti-patterns**:');
|
|
1076
|
+
for (const antiPattern of workflow.anti_patterns) {
|
|
1077
|
+
const desc = typeof antiPattern === 'string' ? antiPattern : (antiPattern?.description || '');
|
|
1078
|
+
if (desc) {
|
|
1079
|
+
sections.push(`- ❌ ${desc}`);
|
|
1080
|
+
}
|
|
487
1081
|
}
|
|
488
1082
|
}
|
|
489
1083
|
|