@equilateral_ai/mindmeld 3.5.2 → 4.0.1
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/hooks/session-end.js +25 -0
- package/hooks/session-start.js +363 -83
- package/hooks/session-watcher.js +585 -0
- package/package.json +19 -13
- package/scripts/init-project.js +9 -23
- package/src/client/dbShim.js +16 -0
- package/src/core/AuthManager.js +3 -2
- package/src/handlers/helpers/dbOperations.js +9 -46
- package/src/index.js +2 -217
- package/src/utils/piiMask.js +16 -0
- package/scripts/harvest.js +0 -601
- package/scripts/inject.js +0 -409
- package/scripts/mcp-bridge.js +0 -220
- package/scripts/repo-analyzer.js +0 -870
- package/src/collaboration/CollaborationPrompt.js +0 -460
- package/src/core/AlertEngine.js +0 -813
- package/src/core/AlertNotifier.js +0 -363
- package/src/core/CorrelationAnalyzer.js +0 -931
- package/src/core/CrossReferenceEngine.js +0 -624
- package/src/core/CurationEngine.js +0 -688
- package/src/core/DeprecationScheduler.js +0 -183
- package/src/core/LoadBearingDetector.js +0 -242
- package/src/core/NotificationService.js +0 -1032
- package/src/core/RapportOrchestrator.js +0 -632
- package/src/core/RelevanceDetector.js +0 -694
- package/src/core/StandardLifecycle.js +0 -244
- package/src/core/StandardsIngestion.js +0 -991
- package/src/core/TeamLoadBearingDetector.js +0 -431
- package/src/core/parsers/adrParser.js +0 -479
- package/src/core/parsers/cursorRulesParser.js +0 -564
- package/src/core/parsers/eslintParser.js +0 -439
- package/src/database/dbOperations.js +0 -105
- package/src/handlers/activity/activityGetMe.js +0 -98
- package/src/handlers/activity/activityGetTeam.js +0 -175
- package/src/handlers/admin/adminSetup.js +0 -216
- package/src/handlers/alerts/alertsAcknowledge.js +0 -92
- package/src/handlers/alerts/alertsGet.js +0 -250
- package/src/handlers/analytics/activitySummaryGet.js +0 -234
- package/src/handlers/analytics/coachingGet.js +0 -361
- package/src/handlers/analytics/convergenceGet.js +0 -236
- package/src/handlers/analytics/developerScoreGet.js +0 -137
- package/src/handlers/collaborators/collaboratorAdd.js +0 -200
- package/src/handlers/collaborators/collaboratorInvite.js +0 -219
- package/src/handlers/collaborators/collaboratorList.js +0 -82
- package/src/handlers/collaborators/collaboratorRemove.js +0 -128
- package/src/handlers/collaborators/inviteAccept.js +0 -122
- package/src/handlers/company/companyUsersDelete.js +0 -141
- package/src/handlers/company/companyUsersGet.js +0 -90
- package/src/handlers/company/companyUsersPost.js +0 -267
- package/src/handlers/company/companyUsersPut.js +0 -76
- package/src/handlers/context/contextGet.js +0 -57
- package/src/handlers/context/invariantsGet.js +0 -74
- package/src/handlers/context/loopsGet.js +0 -82
- package/src/handlers/context/notesCreate.js +0 -74
- package/src/handlers/context/purposeGet.js +0 -78
- package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
- package/src/handlers/correlations/correlationsGet.js +0 -93
- package/src/handlers/correlations/correlationsProjectGet.js +0 -153
- package/src/handlers/enterprise/controlTowerGet.js +0 -224
- package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
- package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
- package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
- package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
- package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
- package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
- package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
- package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
- package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
- package/src/handlers/github/githubConnectionStatus.js +0 -49
- package/src/handlers/github/githubDiscoverPatterns.js +0 -621
- package/src/handlers/github/githubOAuthCallback.js +0 -178
- package/src/handlers/github/githubOAuthStart.js +0 -59
- package/src/handlers/github/githubPatternsReview.js +0 -76
- package/src/handlers/github/githubReposList.js +0 -105
- package/src/handlers/health/healthGet.js +0 -55
- package/src/handlers/helpers/auditLogger.js +0 -201
- package/src/handlers/helpers/checkSuperAdmin.js +0 -84
- package/src/handlers/helpers/decisionFrames.js +0 -29
- package/src/handlers/helpers/errorHandler.js +0 -49
- package/src/handlers/helpers/index.js +0 -138
- package/src/handlers/helpers/lambdaWrapper.js +0 -60
- package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
- package/src/handlers/helpers/predictiveCache.js +0 -51
- package/src/handlers/helpers/projectAccess.js +0 -88
- package/src/handlers/helpers/responseUtil.js +0 -55
- package/src/handlers/helpers/subscriptionTiers.js +0 -1168
- package/src/handlers/mcp/mcpHandler.js +0 -569
- package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
- package/src/handlers/notifications/getPreferences.js +0 -84
- package/src/handlers/notifications/sendNotification.js +0 -170
- package/src/handlers/notifications/updatePreferences.js +0 -316
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
- package/src/handlers/patterns/patternUsagePost.js +0 -182
- package/src/handlers/patterns/patternViolationPost.js +0 -185
- package/src/handlers/projects/projectCreate.js +0 -248
- package/src/handlers/projects/projectDelete.js +0 -82
- package/src/handlers/projects/projectGet.js +0 -95
- package/src/handlers/projects/projectUpdate.js +0 -117
- package/src/handlers/reports/aiLeverage.js +0 -210
- package/src/handlers/reports/engineeringInvestment.js +0 -132
- package/src/handlers/reports/riskForecast.js +0 -206
- package/src/handlers/reports/standardsRoi.js +0 -254
- package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
- package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
- package/src/handlers/scheduled/generateAlerts.js +0 -135
- package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
- package/src/handlers/scheduled/refreshActivity.js +0 -21
- package/src/handlers/scheduled/scanCompliance.js +0 -334
- package/src/handlers/sessions/sessionEndPost.js +0 -180
- package/src/handlers/sessions/sessionStandardsPost.js +0 -171
- package/src/handlers/standards/catalogGet.js +0 -185
- package/src/handlers/standards/catalogSync.js +0 -120
- package/src/handlers/standards/discoveriesGet.js +0 -89
- package/src/handlers/standards/projectStandardsGet.js +0 -129
- package/src/handlers/standards/projectStandardsPut.js +0 -151
- package/src/handlers/standards/standardsAuditGet.js +0 -65
- package/src/handlers/standards/standardsParseUpload.js +0 -149
- package/src/handlers/standards/standardsRelevantPost.js +0 -405
- package/src/handlers/standards/standardsTransition.js +0 -161
- package/src/handlers/stripe/addonManagePost.js +0 -240
- package/src/handlers/stripe/billingPortalPost.js +0 -93
- package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
- package/src/handlers/stripe/seatsUpdatePost.js +0 -185
- package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
- package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
- package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
- package/src/handlers/stripe/webhookPost.js +0 -482
- package/src/handlers/user/apiTokenCreate.js +0 -71
- package/src/handlers/user/apiTokenList.js +0 -64
- package/src/handlers/user/userSplashAck.js +0 -91
- package/src/handlers/user/userSplashGet.js +0 -211
- package/src/handlers/users/cognitoPostConfirmation.js +0 -186
- package/src/handlers/users/cognitoPreSignUp.js +0 -114
- package/src/handlers/users/userEntitlementsGet.js +0 -89
- package/src/handlers/users/userGet.js +0 -118
- package/src/handlers/users/userProfilePut.js +0 -77
- package/src/handlers/webhooks/githubWebhook.js +0 -215
package/hooks/session-end.js
CHANGED
|
@@ -291,6 +291,31 @@ async function recordSessionEnd() {
|
|
|
291
291
|
return { skipped: true, reason: 'No session_id' };
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Stop session watcher — write stop file (primary) + SIGTERM (secondary)
|
|
295
|
+
try {
|
|
296
|
+
// Write stop file — watcher checks this on its liveness interval
|
|
297
|
+
await fs.writeFile(path.join(cwd, '.mindmeld', 'watcher.stop'), '').catch(() => {});
|
|
298
|
+
|
|
299
|
+
const pidPath = path.join(cwd, '.mindmeld', 'watcher.pid');
|
|
300
|
+
const pidContent = await fs.readFile(pidPath, 'utf-8');
|
|
301
|
+
const watcherPid = parseInt(pidContent.trim(), 10);
|
|
302
|
+
if (watcherPid && !isNaN(watcherPid)) {
|
|
303
|
+
try {
|
|
304
|
+
process.kill(watcherPid, 'SIGTERM');
|
|
305
|
+
console.error(`[MindMeld] Stopped session watcher (PID: ${watcherPid})`);
|
|
306
|
+
} catch (killErr) {
|
|
307
|
+
if (killErr.code !== 'ESRCH') {
|
|
308
|
+
console.error(`[MindMeld] Watcher kill failed: ${killErr.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
await fs.unlink(pidPath).catch(() => {});
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err.code !== 'ENOENT') {
|
|
315
|
+
console.error(`[MindMeld] Watcher cleanup error (non-fatal): ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
// Load auth and config in parallel
|
|
295
320
|
const [authToken, apiConfig, projectId, duration, gitBranch] = await Promise.all([
|
|
296
321
|
loadAuthToken(),
|
package/hooks/session-start.js
CHANGED
|
@@ -101,6 +101,80 @@ async function detectGitRemote() {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Detect git HEAD SHA and branch for project identity (Spec §6.5)
|
|
106
|
+
* @returns {Promise<{commitSha: string|null, gitBranch: string|null}>}
|
|
107
|
+
*/
|
|
108
|
+
async function detectGitMetadata() {
|
|
109
|
+
const { execSync } = require('child_process');
|
|
110
|
+
const opts = { cwd: process.cwd(), encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] };
|
|
111
|
+
let commitSha = null;
|
|
112
|
+
let gitBranch = null;
|
|
113
|
+
try {
|
|
114
|
+
commitSha = execSync('git rev-parse HEAD', opts).trim() || null;
|
|
115
|
+
} catch (_) { /* not a git repo */ }
|
|
116
|
+
try {
|
|
117
|
+
gitBranch = execSync('git branch --show-current', opts).trim() || null;
|
|
118
|
+
} catch (_) { /* detached HEAD or not a git repo */ }
|
|
119
|
+
return { commitSha, gitBranch };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read Claude Code memory files for drift detection (Spec §5.2)
|
|
124
|
+
* Reads MEMORY.md index + all referenced .md files from the project memory dir
|
|
125
|
+
* @returns {Promise<string|null>} Concatenated memory content or null
|
|
126
|
+
*/
|
|
127
|
+
async function readClaudeMemory() {
|
|
128
|
+
const os = require('os');
|
|
129
|
+
const encodedCwd = process.cwd().replace(/\//g, '-');
|
|
130
|
+
const memoryDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd, 'memory');
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await fs.access(memoryDir);
|
|
134
|
+
} catch (_) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const files = await fs.readdir(memoryDir);
|
|
140
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
141
|
+
if (mdFiles.length === 0) return null;
|
|
142
|
+
|
|
143
|
+
const contents = [];
|
|
144
|
+
// Read MEMORY.md first if it exists (it's the index)
|
|
145
|
+
if (mdFiles.includes('MEMORY.md')) {
|
|
146
|
+
const content = await fs.readFile(path.join(memoryDir, 'MEMORY.md'), 'utf-8');
|
|
147
|
+
contents.push(`--- MEMORY.md ---\n${content}`);
|
|
148
|
+
}
|
|
149
|
+
for (const file of mdFiles) {
|
|
150
|
+
if (file === 'MEMORY.md') continue;
|
|
151
|
+
try {
|
|
152
|
+
const content = await fs.readFile(path.join(memoryDir, file), 'utf-8');
|
|
153
|
+
contents.push(`--- ${file} ---\n${content}`);
|
|
154
|
+
} catch (_) { /* skip unreadable files */ }
|
|
155
|
+
}
|
|
156
|
+
return contents.join('\n\n');
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('[MindMeld] Memory read failed (non-fatal):', err.message);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Read CLAUDE.md files for drift detection
|
|
165
|
+
* Reads project-level CLAUDE.md and any parent CLAUDE.md files
|
|
166
|
+
* @returns {Promise<string|null>} Concatenated CLAUDE.md content or null
|
|
167
|
+
*/
|
|
168
|
+
async function readClaudeMd() {
|
|
169
|
+
try {
|
|
170
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
171
|
+
const content = await fs.readFile(claudeMdPath, 'utf-8');
|
|
172
|
+
return content || null;
|
|
173
|
+
} catch (_) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
104
178
|
/**
|
|
105
179
|
* Get project name from git remote URL or directory name
|
|
106
180
|
* @param {string|null} gitRemote - Git remote URL
|
|
@@ -628,6 +702,81 @@ async function fetchRelevantStandardsFromAPI(apiUrl, authToken, characteristics,
|
|
|
628
702
|
});
|
|
629
703
|
}
|
|
630
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Call mindmeld_init_session via JSON-RPC to get banded injection (Band A/B/C).
|
|
707
|
+
* This converges the hook injection path with the MCP tool path — one deterministic
|
|
708
|
+
* banded output regardless of whether the MCP tool also fires during the session.
|
|
709
|
+
*/
|
|
710
|
+
async function callBandedInitSession(apiUrl, authToken, params) {
|
|
711
|
+
if (!authToken) {
|
|
712
|
+
throw new Error('No auth token available');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const https = require('https');
|
|
716
|
+
const url = require('url');
|
|
717
|
+
|
|
718
|
+
const jsonRpcPayload = JSON.stringify({
|
|
719
|
+
jsonrpc: '2.0',
|
|
720
|
+
id: 1,
|
|
721
|
+
method: 'tools/call',
|
|
722
|
+
params: {
|
|
723
|
+
name: 'mindmeld_init_session',
|
|
724
|
+
arguments: params
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const parsedUrl = url.parse(`${apiUrl}/api/mcp/mindmeld`);
|
|
729
|
+
|
|
730
|
+
const options = {
|
|
731
|
+
hostname: parsedUrl.hostname,
|
|
732
|
+
port: parsedUrl.port || 443,
|
|
733
|
+
path: parsedUrl.path,
|
|
734
|
+
method: 'POST',
|
|
735
|
+
headers: {
|
|
736
|
+
'Content-Type': 'application/json',
|
|
737
|
+
'Authorization': `Bearer ${authToken}`,
|
|
738
|
+
'Content-Length': Buffer.byteLength(jsonRpcPayload)
|
|
739
|
+
},
|
|
740
|
+
timeout: 8000
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
return new Promise((resolve, reject) => {
|
|
744
|
+
const req = https.request(options, (res) => {
|
|
745
|
+
let data = '';
|
|
746
|
+
res.on('data', chunk => { data += chunk; });
|
|
747
|
+
res.on('end', () => {
|
|
748
|
+
try {
|
|
749
|
+
const rpcResponse = JSON.parse(data);
|
|
750
|
+
if (rpcResponse.error) {
|
|
751
|
+
const err = new Error(rpcResponse.error.message || 'JSON-RPC error');
|
|
752
|
+
err.statusCode = res.statusCode;
|
|
753
|
+
reject(err);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const content = rpcResponse.result?.content;
|
|
757
|
+
if (!content || !content[0] || content[0].type !== 'text') {
|
|
758
|
+
reject(new Error('Unexpected init_session response shape'));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const initResult = JSON.parse(content[0].text);
|
|
762
|
+
resolve(initResult);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
reject(new Error('Invalid banded init_session response'));
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
req.on('error', reject);
|
|
770
|
+
req.on('timeout', () => {
|
|
771
|
+
req.destroy();
|
|
772
|
+
reject(new Error('Banded init_session timeout'));
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
req.write(jsonRpcPayload);
|
|
776
|
+
req.end();
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
631
780
|
/**
|
|
632
781
|
* Fetch weekly splash data from API
|
|
633
782
|
* Returns splash summary with stats, or null if already acknowledged
|
|
@@ -744,11 +893,14 @@ async function injectContext() {
|
|
|
744
893
|
}
|
|
745
894
|
|
|
746
895
|
// 1. Parallel local reads (no network, no DB)
|
|
747
|
-
const [fingerprintConfig, authToken, apiConfig, preferences] = await Promise.all([
|
|
896
|
+
const [fingerprintConfig, authToken, apiConfig, preferences, gitMetadata, memoryContent, claudeMdContent] = await Promise.all([
|
|
748
897
|
loadFingerprintConfig(),
|
|
749
898
|
loadAuthToken(),
|
|
750
899
|
loadApiConfig(),
|
|
751
|
-
loadStandardsPreferences()
|
|
900
|
+
loadStandardsPreferences(),
|
|
901
|
+
detectGitMetadata(),
|
|
902
|
+
readClaudeMemory(),
|
|
903
|
+
readClaudeMd()
|
|
752
904
|
]);
|
|
753
905
|
|
|
754
906
|
// 2. Initialize MindmeldClient (used for project detection, session tracking, team context)
|
|
@@ -771,13 +923,33 @@ async function injectContext() {
|
|
|
771
923
|
// 3b. Persist session context for pre-compact hook
|
|
772
924
|
// Pre-compact creates a new MindmeldClient and needs project context
|
|
773
925
|
try {
|
|
926
|
+
// Derive transcript path for watcher breadcrumb
|
|
927
|
+
const encodedCwd = process.cwd().replace(/\//g, '-');
|
|
928
|
+
const claudeProjectDir = path.join(require('os').homedir(), '.claude', 'projects', encodedCwd);
|
|
929
|
+
let transcriptPath = null;
|
|
930
|
+
try {
|
|
931
|
+
const dirFiles = await fs.readdir(claudeProjectDir);
|
|
932
|
+
const jsonlFiles = dirFiles.filter(f => f.endsWith('.jsonl'));
|
|
933
|
+
let newestMtime = 0;
|
|
934
|
+
for (const f of jsonlFiles) {
|
|
935
|
+
const fp = path.join(claudeProjectDir, f);
|
|
936
|
+
const s = await fs.stat(fp);
|
|
937
|
+
if (s.mtimeMs > newestMtime) { newestMtime = s.mtimeMs; transcriptPath = fp; }
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
// Claude project dir may not exist yet
|
|
941
|
+
}
|
|
942
|
+
|
|
774
943
|
const sessionContext = {
|
|
775
944
|
sessionId: sessionId,
|
|
776
945
|
projectId: context.projectId,
|
|
777
946
|
projectName: context.projectName,
|
|
778
947
|
companyId: context.companyId,
|
|
779
948
|
userEmail: context.userEmail,
|
|
780
|
-
startedAt: new Date().toISOString()
|
|
949
|
+
startedAt: new Date().toISOString(),
|
|
950
|
+
transcriptPath: transcriptPath,
|
|
951
|
+
commitSha: gitMetadata.commitSha,
|
|
952
|
+
gitBranch: gitMetadata.gitBranch,
|
|
781
953
|
};
|
|
782
954
|
const mindmeldDir = path.join(process.cwd(), '.mindmeld');
|
|
783
955
|
await fs.mkdir(mindmeldDir, { recursive: true });
|
|
@@ -789,89 +961,101 @@ async function injectContext() {
|
|
|
789
961
|
console.error('[MindMeld] Could not persist session context (non-fatal):', err.message);
|
|
790
962
|
}
|
|
791
963
|
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
} else if (standardsResult.status === 'rejected' &&
|
|
808
|
-
standardsResult.reason.message.includes('subscription required')) {
|
|
809
|
-
// Subscription enforcement — do NOT fall through to file-based injection
|
|
810
|
-
console.error('[MindMeld] Active subscription required. Subscribe at app.mindmeld.dev');
|
|
811
|
-
return '';
|
|
812
|
-
} else if (standardsResult.status === 'rejected' &&
|
|
813
|
-
(standardsResult.reason.statusCode === 401 || standardsResult.reason.message === 'Unauthorized')) {
|
|
814
|
-
// Auth token expired or invalid — trigger re-auth and use file-based fallback
|
|
815
|
-
console.error('[MindMeld] Auth token expired. Triggering re-authentication...');
|
|
816
|
-
spawnBackgroundLogin();
|
|
817
|
-
const categories = mindmeld.relevanceDetector.mapCharacteristicsToCategories(characteristics);
|
|
818
|
-
relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories, characteristics);
|
|
819
|
-
console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback (scored)`);
|
|
820
|
-
} else {
|
|
821
|
-
if (standardsResult.status === 'rejected') {
|
|
822
|
-
console.error(`[MindMeld] API fallback: ${standardsResult.reason.message}`);
|
|
964
|
+
// 3c. Spawn background session watcher for continuous pattern harvesting
|
|
965
|
+
// (covers 1M context sessions where pre-compact may never fire)
|
|
966
|
+
// Watcher survives /clear — if already alive, it picks up the new transcript via breadcrumb
|
|
967
|
+
try {
|
|
968
|
+
const pidPath = path.join(process.cwd(), '.mindmeld', 'watcher.pid');
|
|
969
|
+
let watcherAlive = false;
|
|
970
|
+
try {
|
|
971
|
+
const existingPid = parseInt(await fs.readFile(pidPath, 'utf-8'), 10);
|
|
972
|
+
if (existingPid && !isNaN(existingPid)) {
|
|
973
|
+
process.kill(existingPid, 0); // Check if alive
|
|
974
|
+
watcherAlive = true;
|
|
975
|
+
console.error(`[MindMeld] Existing watcher alive (PID: ${existingPid}), will pick up new transcript via breadcrumb`);
|
|
976
|
+
}
|
|
977
|
+
} catch (err) {
|
|
978
|
+
// No PID file or process gone — spawn a new one
|
|
823
979
|
}
|
|
824
|
-
const categories = mindmeld.relevanceDetector.mapCharacteristicsToCategories(characteristics);
|
|
825
|
-
relevantStandards = await mindmeld.relevanceDetector.loadStandardsFromFiles(categories, characteristics);
|
|
826
|
-
console.error(`[MindMeld] ${relevantStandards.length} standards from file fallback (scored)`);
|
|
827
|
-
}
|
|
828
980
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
981
|
+
if (!watcherAlive) {
|
|
982
|
+
const { spawn } = require('child_process');
|
|
983
|
+
const watcherScript = path.resolve(__dirname, 'session-watcher.js');
|
|
984
|
+
await fs.access(watcherScript);
|
|
985
|
+
const parentPid = process.ppid || process.pid;
|
|
986
|
+
const watcher = spawn(process.execPath, [watcherScript, process.cwd(), String(parentPid)], {
|
|
987
|
+
detached: true,
|
|
988
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
989
|
+
cwd: process.cwd(),
|
|
990
|
+
env: { ...process.env }
|
|
991
|
+
});
|
|
992
|
+
watcher.unref();
|
|
993
|
+
console.error(`[MindMeld] Session watcher spawned (PID: ${watcher.pid})`);
|
|
994
|
+
}
|
|
995
|
+
} catch (err) {
|
|
996
|
+
console.error(`[MindMeld] Watcher spawn failed (non-fatal): ${err.message}`);
|
|
833
997
|
}
|
|
834
998
|
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
999
|
+
// 4. Banded injection path (converged with MCP init_session)
|
|
1000
|
+
// Band B content gated on resolved companyId — null scope would mis-match snapshots
|
|
1001
|
+
const bandBGated = !!context.companyId;
|
|
1002
|
+
const initParams = {
|
|
1003
|
+
project_path: process.cwd(),
|
|
1004
|
+
commit_sha: gitMetadata.commitSha || undefined,
|
|
1005
|
+
git_branch: gitMetadata.gitBranch || undefined,
|
|
1006
|
+
git_root: gitMetadata.gitRoot || undefined,
|
|
1007
|
+
memory_content: bandBGated ? (memoryContent || undefined) : undefined,
|
|
1008
|
+
claude_md_content: bandBGated ? (claudeMdContent || undefined) : undefined
|
|
1009
|
+
};
|
|
840
1010
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
: [];
|
|
846
|
-
const recentLearning = projectContext && projectContext.recentLearning
|
|
847
|
-
? projectContext.recentLearning
|
|
848
|
-
: [];
|
|
1011
|
+
const [bandedResult, splashResult] = await Promise.allSettled([
|
|
1012
|
+
callBandedInitSession(apiConfig.apiUrl, authToken, initParams),
|
|
1013
|
+
fetchSplashData(apiConfig.apiUrl, authToken)
|
|
1014
|
+
]);
|
|
849
1015
|
|
|
850
|
-
// 10. Resolve splash data
|
|
851
1016
|
const splashData = splashResult.status === 'fulfilled' ? splashResult.value : null;
|
|
852
|
-
|
|
853
|
-
// Auto-acknowledge splash (fire-and-forget) so it doesn't show again this week
|
|
854
|
-
if (splashData && splashData.show_splash && splashData.summary) {
|
|
1017
|
+
if (splashData?.show_splash && splashData?.summary) {
|
|
855
1018
|
acknowledgeSplash(apiConfig.apiUrl, authToken, splashData.summary.week_start);
|
|
856
1019
|
}
|
|
857
1020
|
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1021
|
+
// 6. Resolve injection: banded → flat fallback
|
|
1022
|
+
let injection;
|
|
1023
|
+
if (bandedResult.status === 'fulfilled' && bandedResult.value.formatted_injection) {
|
|
1024
|
+
const initResponse = bandedResult.value;
|
|
1025
|
+
const summary = initResponse.injection_summary || {};
|
|
1026
|
+
console.error(`[MindMeld] Banded injection: A=${summary.band_a_tokens || 0}t B=${summary.band_b_tokens || 0}t C=${summary.band_c_tokens || 0}t / ${summary.budget_tokens || 400}t budget`);
|
|
1027
|
+
|
|
1028
|
+
injection = formatConvergedInjection({
|
|
1029
|
+
bandedMarkdown: initResponse.formatted_injection,
|
|
1030
|
+
collaborators: context.collaborators,
|
|
1031
|
+
splash: splashData
|
|
1032
|
+
});
|
|
869
1033
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1034
|
+
try {
|
|
1035
|
+
const standardsForCache = (initResponse.injected_rules || []).map(r => ({
|
|
1036
|
+
pattern_id: r.rule_id, element: r.standard, rule: r.text,
|
|
1037
|
+
maturity: r.maturity, relevance_score: r.relevance_score
|
|
1038
|
+
}));
|
|
1039
|
+
await cacheSubagentContext(standardsForCache, [], context.projectName);
|
|
1040
|
+
} catch (cacheError) {
|
|
1041
|
+
console.error('[MindMeld] Subagent context cache failed (non-fatal):', cacheError.message);
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
const bandedError = bandedResult.status === 'rejected' ? bandedResult.reason : null;
|
|
1045
|
+
if (bandedError) {
|
|
1046
|
+
if (bandedError.message.includes('subscription required')) {
|
|
1047
|
+
console.error('[MindMeld] Active subscription required. Subscribe at app.mindmeld.dev');
|
|
1048
|
+
return '';
|
|
1049
|
+
}
|
|
1050
|
+
if (bandedError.statusCode === 401 || bandedError.message === 'Unauthorized') {
|
|
1051
|
+
console.error('[MindMeld] Auth token expired. Triggering re-authentication...');
|
|
1052
|
+
spawnBackgroundLogin();
|
|
1053
|
+
}
|
|
1054
|
+
console.error(`[MindMeld] API unavailable: ${bandedError.message}`);
|
|
1055
|
+
} else {
|
|
1056
|
+
console.error('[MindMeld] API returned empty injection');
|
|
1057
|
+
}
|
|
1058
|
+
injection = '<!-- MindMeld: API unavailable — this session is proceeding without governance injection. Standards were not loaded. -->';
|
|
875
1059
|
}
|
|
876
1060
|
|
|
877
1061
|
const elapsed = Date.now() - startTime;
|
|
@@ -880,9 +1064,8 @@ async function injectContext() {
|
|
|
880
1064
|
return injection;
|
|
881
1065
|
|
|
882
1066
|
} catch (error) {
|
|
883
|
-
// Graceful degradation - never block Claude Code session
|
|
884
1067
|
console.error('[MindMeld] Hook error (non-fatal):', error.message);
|
|
885
|
-
return '';
|
|
1068
|
+
return '<!-- MindMeld: hook error — this session is proceeding without governance injection. Standards were not loaded. -->';
|
|
886
1069
|
}
|
|
887
1070
|
}
|
|
888
1071
|
|
|
@@ -903,6 +1086,27 @@ async function checkMindmeldConfiguration() {
|
|
|
903
1086
|
}
|
|
904
1087
|
}
|
|
905
1088
|
|
|
1089
|
+
/**
|
|
1090
|
+
* Validate that a standard has enough content to be useful when injected.
|
|
1091
|
+
* Mirrors StandardsIngestion.isViableStandard() for the output path.
|
|
1092
|
+
*/
|
|
1093
|
+
function isViableForInjection(standard) {
|
|
1094
|
+
const el = (standard.element || '').trim();
|
|
1095
|
+
const rl = (standard.rule || '').trim();
|
|
1096
|
+
|
|
1097
|
+
if (!el || !rl) return false;
|
|
1098
|
+
if (/^(USE|ALWAYS|NEVER|PREFER|AVOID|DO|DON'T|SET|ADD|RUN|CALL)$/i.test(el)) return false;
|
|
1099
|
+
if (/^(Standard Pattern|Core Principle)$/i.test(el) && rl.length < 30) return false;
|
|
1100
|
+
if (/^(ALWAYS|NEVER|USE|PREFER|AVOID)[:\s]*$/i.test(rl)) return false;
|
|
1101
|
+
if (rl.length < 15) return false;
|
|
1102
|
+
|
|
1103
|
+
// Word-count guard: element must have >=2 words or be a compound term (>=8 chars)
|
|
1104
|
+
const wordCount = el.split(/[\s.]+/).filter(w => w.length > 0).length;
|
|
1105
|
+
if (wordCount < 2 && el.length < 8) return false;
|
|
1106
|
+
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
906
1110
|
/**
|
|
907
1111
|
* Cache condensed context for subagent injection
|
|
908
1112
|
* Subagents don't get session-start hooks, so we cache the essentials
|
|
@@ -922,25 +1126,55 @@ async function cacheSubagentContext(standards, teamPatterns, projectName) {
|
|
|
922
1126
|
sections.push('Follow these standards when writing or modifying code:');
|
|
923
1127
|
sections.push('');
|
|
924
1128
|
|
|
925
|
-
|
|
1129
|
+
let injectedCount = 0;
|
|
1130
|
+
let skippedCount = 0;
|
|
1131
|
+
|
|
926
1132
|
if (standards && standards.length > 0) {
|
|
927
1133
|
for (const s of standards) {
|
|
928
|
-
|
|
1134
|
+
if (!isViableForInjection(s)) {
|
|
1135
|
+
skippedCount++;
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const maturity = s.maturity_tier || s.maturity || 'validated';
|
|
1140
|
+
const prefix = maturity === 'reinforced' ? '[MUST]' :
|
|
1141
|
+
maturity === 'enforced' ? '[MUST]' :
|
|
1142
|
+
maturity === 'solidified' ? '[SHOULD]' : '[CONSIDER]';
|
|
1143
|
+
|
|
1144
|
+
sections.push(`### ${prefix} ${s.element}`);
|
|
1145
|
+
sections.push(`**Category**: ${s.category}`);
|
|
1146
|
+
sections.push(`**Rule**: ${s.rule}`);
|
|
1147
|
+
|
|
1148
|
+
// Include first anti-pattern if available — high signal-to-token ratio
|
|
1149
|
+
if (s.anti_patterns && s.anti_patterns.length > 0) {
|
|
1150
|
+
const ap = s.anti_patterns[0];
|
|
1151
|
+
const desc = typeof ap === 'string' ? ap : (ap.description || '');
|
|
1152
|
+
if (desc) {
|
|
1153
|
+
sections.push(`**Avoid**: ${desc}`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
sections.push('');
|
|
1158
|
+
injectedCount++;
|
|
929
1159
|
}
|
|
930
|
-
sections.push('');
|
|
931
1160
|
}
|
|
932
1161
|
|
|
933
|
-
// Include team patterns
|
|
1162
|
+
// Include team patterns with validation
|
|
934
1163
|
if (teamPatterns && teamPatterns.length > 0) {
|
|
935
1164
|
sections.push('## Team Patterns');
|
|
936
1165
|
for (const p of teamPatterns) {
|
|
1166
|
+
if (!isViableForInjection(p)) {
|
|
1167
|
+
skippedCount++;
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
937
1170
|
sections.push(`- **${p.element}** (${(p.correlation * 100).toFixed(0)}% correlation): ${p.rule}`);
|
|
1171
|
+
injectedCount++;
|
|
938
1172
|
}
|
|
939
1173
|
sections.push('');
|
|
940
1174
|
}
|
|
941
1175
|
|
|
942
1176
|
await fs.writeFile(cachePath, sections.join('\n'));
|
|
943
|
-
console.error(`[MindMeld] Cached subagent context (${
|
|
1177
|
+
console.error(`[MindMeld] Cached subagent context (${injectedCount} standards, ${skippedCount} skipped as non-viable)`);
|
|
944
1178
|
}
|
|
945
1179
|
|
|
946
1180
|
/**
|
|
@@ -958,7 +1192,53 @@ function getMaturityPrefix(maturityTier) {
|
|
|
958
1192
|
}
|
|
959
1193
|
|
|
960
1194
|
/**
|
|
961
|
-
*
|
|
1195
|
+
* Wrap banded init_session output with hook-only sections (splash, team).
|
|
1196
|
+
* The banded markdown is self-contained (header, copyright, bands A/B/C, footer).
|
|
1197
|
+
* Hook-only content is inserted between the copyright block and the first band heading.
|
|
1198
|
+
*/
|
|
1199
|
+
function formatConvergedInjection({ bandedMarkdown, collaborators, splash }) {
|
|
1200
|
+
const extraSections = [];
|
|
1201
|
+
|
|
1202
|
+
if (splash && splash.show_splash) {
|
|
1203
|
+
if (splash.is_greenfield && splash.tips && splash.tips.length > 0) {
|
|
1204
|
+
extraSections.push('## Getting Started with MindMeld');
|
|
1205
|
+
for (const tip of splash.tips) extraSections.push(`- ${tip}`);
|
|
1206
|
+
extraSections.push('');
|
|
1207
|
+
} else if (splash.summary) {
|
|
1208
|
+
const s = splash.summary;
|
|
1209
|
+
const formatDate = (dateStr) => {
|
|
1210
|
+
const d = new Date(dateStr + 'T00:00:00Z');
|
|
1211
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
|
1212
|
+
};
|
|
1213
|
+
const hours = Math.floor(s.total_duration_minutes / 60);
|
|
1214
|
+
const mins = s.total_duration_minutes % 60;
|
|
1215
|
+
const duration = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
1216
|
+
extraSections.push(`## Weekly Recap (${formatDate(s.week_start)} - ${formatDate(s.week_end)})`);
|
|
1217
|
+
extraSections.push(`${s.sessions_count} sessions | ${duration} total | ${s.active_projects} project${s.active_projects !== 1 ? 's' : ''}`);
|
|
1218
|
+
extraSections.push(`${s.standards_injected} standards injected, ${s.standards_followed} followed, ${s.violations_detected} violation${s.violations_detected !== 1 ? 's' : ''}`);
|
|
1219
|
+
if (s.patterns_harvested > 0) extraSections.push(`${s.patterns_harvested} patterns harvested`);
|
|
1220
|
+
extraSections.push('');
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (collaborators && collaborators.length > 0) {
|
|
1225
|
+
extraSections.push('## Team');
|
|
1226
|
+
extraSections.push(collaborators.map(c => `- ${c.name} (${c.email})`).join('\n'));
|
|
1227
|
+
extraSections.push('');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (extraSections.length === 0) return bandedMarkdown;
|
|
1231
|
+
|
|
1232
|
+
const firstBandHeading = bandedMarkdown.indexOf('\n## ');
|
|
1233
|
+
if (firstBandHeading === -1) return bandedMarkdown;
|
|
1234
|
+
|
|
1235
|
+
return bandedMarkdown.slice(0, firstBandHeading + 1) +
|
|
1236
|
+
extraSections.join('\n') + '\n' +
|
|
1237
|
+
bandedMarkdown.slice(firstBandHeading + 1);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Format flat context injection for Claude Code (fallback when banded path unavailable)
|
|
962
1242
|
* @param {object} data - Context data to inject
|
|
963
1243
|
* @param {object} data.fingerprint - Fingerprint config {userId, companyId, tier}
|
|
964
1244
|
*/
|