@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.
Files changed (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -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(),
@@ -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
- // 4. Detect project characteristics locally (file system only, no DB)
793
- const characteristics = await mindmeld.relevanceDetector.detectProjectCharacteristics();
794
-
795
- // 5. Parallel API calls: standards + team context + splash
796
- const [standardsResult, projectContextResult, splashResult] = await Promise.allSettled([
797
- fetchRelevantStandardsFromAPI(apiConfig.apiUrl, authToken, characteristics, context.projectId, preferences),
798
- mindmeld.loadProjectContext(context.projectId),
799
- fetchSplashData(apiConfig.apiUrl, authToken)
800
- ]);
801
-
802
- // 6. Resolve standards: API → file-based fallback (only for network errors, not subscription blocks)
803
- let relevantStandards = [];
804
- if (standardsResult.status === 'fulfilled' && standardsResult.value.length > 0) {
805
- relevantStandards = standardsResult.value;
806
- console.error(`[MindMeld] ${relevantStandards.length} standards from API`);
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
- // 7. Filter by project preferences
830
- if (preferences) {
831
- relevantStandards = filterStandardsByPreferences(relevantStandards, preferences);
832
- console.error(`[MindMeld] Filtered to ${relevantStandards.length} standards by preferences`);
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
- // Take top 10 most relevant after filtering
836
- relevantStandards = relevantStandards.slice(0, 10);
837
-
838
- // 8. Record standards shown (fire-and-forget, non-blocking)
839
- mindmeld.recordStandardsShown(sessionId, relevantStandards, context.projectId, context.userEmail);
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
- // 9. Resolve team context from parallel API call
842
- const projectContext = projectContextResult.status === 'fulfilled' ? projectContextResult.value : null;
843
- const teamPatterns = projectContext && projectContext.patterns
844
- ? projectContext.patterns.filter(p => p.confidence > 0.7)
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
- // 11. Build context injection with fingerprint
859
- const injection = formatContextInjection({
860
- project: context.projectName,
861
- sessionId: sessionId,
862
- collaborators: context.collaborators,
863
- relevantStandards: relevantStandards,
864
- teamPatterns: teamPatterns,
865
- recentLearning: recentLearning,
866
- fingerprint: fingerprintConfig,
867
- splash: splashData
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
- // 12. Cache condensed context for subagent injection
871
- try {
872
- await cacheSubagentContext(relevantStandards, teamPatterns, context.projectName);
873
- } catch (cacheError) {
874
- console.error('[MindMeld] Subagent context cache failed (non-fatal):', cacheError.message);
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
- // Include standards as compact rules (no examples/anti-patterns to save tokens)
1129
+ let injectedCount = 0;
1130
+ let skippedCount = 0;
1131
+
926
1132
  if (standards && standards.length > 0) {
927
1133
  for (const s of standards) {
928
- sections.push(`- **${s.element}** [${s.category}]: ${s.rule}`);
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 as compact rules
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 (${sections.length} lines)`);
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
- * Format context injection for Claude Code
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
  */