@cpretzinger/boss-claude 1.0.0 → 1.0.2

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 (87) hide show
  1. package/README.md +304 -1
  2. package/bin/boss-claude.js +1138 -0
  3. package/bin/commands/mode.js +250 -0
  4. package/bin/onyx-guard.js +259 -0
  5. package/bin/onyx-guard.sh +251 -0
  6. package/bin/prompts.js +284 -0
  7. package/bin/rollback.js +85 -0
  8. package/bin/setup-wizard.js +492 -0
  9. package/config/.env.example +17 -0
  10. package/lib/README.md +83 -0
  11. package/lib/agent-logger.js +61 -0
  12. package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
  13. package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
  14. package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
  15. package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
  16. package/lib/agents/memory-supervisor.js +526 -0
  17. package/lib/agents/registry.js +135 -0
  18. package/lib/auto-monitor.js +131 -0
  19. package/lib/checkpoint-hook.js +112 -0
  20. package/lib/checkpoint.js +319 -0
  21. package/lib/commentator.js +213 -0
  22. package/lib/context-scribe.js +120 -0
  23. package/lib/delegation-strategies.js +326 -0
  24. package/lib/hierarchy-validator.js +643 -0
  25. package/lib/index.js +15 -0
  26. package/lib/init-with-mode.js +261 -0
  27. package/lib/init.js +44 -6
  28. package/lib/memory-result-aggregator.js +252 -0
  29. package/lib/memory.js +35 -7
  30. package/lib/mode-enforcer.js +473 -0
  31. package/lib/onyx-banner.js +169 -0
  32. package/lib/onyx-identity.js +214 -0
  33. package/lib/onyx-monitor.js +381 -0
  34. package/lib/onyx-reminder.js +188 -0
  35. package/lib/onyx-tool-interceptor.js +341 -0
  36. package/lib/onyx-wrapper.js +315 -0
  37. package/lib/orchestrator-gate.js +334 -0
  38. package/lib/output-formatter.js +296 -0
  39. package/lib/postgres.js +1 -1
  40. package/lib/prompt-injector.js +220 -0
  41. package/lib/prompts.js +532 -0
  42. package/lib/session.js +153 -6
  43. package/lib/setup/README.md +187 -0
  44. package/lib/setup/env-manager.js +785 -0
  45. package/lib/setup/error-recovery.js +630 -0
  46. package/lib/setup/explain-scopes.js +385 -0
  47. package/lib/setup/github-instructions.js +333 -0
  48. package/lib/setup/github-repo.js +254 -0
  49. package/lib/setup/import-credentials.js +498 -0
  50. package/lib/setup/index.js +62 -0
  51. package/lib/setup/init-postgres.js +785 -0
  52. package/lib/setup/init-redis.js +456 -0
  53. package/lib/setup/integration-test.js +652 -0
  54. package/lib/setup/progress.js +357 -0
  55. package/lib/setup/rollback.js +670 -0
  56. package/lib/setup/rollback.test.js +452 -0
  57. package/lib/setup/setup-with-rollback.example.js +351 -0
  58. package/lib/setup/summary.js +400 -0
  59. package/lib/setup/test-github-setup.js +10 -0
  60. package/lib/setup/test-postgres-init.js +98 -0
  61. package/lib/setup/verify-setup.js +102 -0
  62. package/lib/task-agent-worker.js +235 -0
  63. package/lib/token-monitor.js +466 -0
  64. package/lib/tool-wrapper-integration.js +369 -0
  65. package/lib/tool-wrapper.js +387 -0
  66. package/lib/validators/README.md +497 -0
  67. package/lib/validators/config.js +583 -0
  68. package/lib/validators/config.test.js +175 -0
  69. package/lib/validators/github.js +310 -0
  70. package/lib/validators/github.test.js +61 -0
  71. package/lib/validators/index.js +15 -0
  72. package/lib/validators/postgres.js +525 -0
  73. package/package.json +98 -13
  74. package/scripts/benchmark-memory.js +433 -0
  75. package/scripts/check-secrets.sh +12 -0
  76. package/scripts/fetch-todos.mjs +148 -0
  77. package/scripts/graceful-shutdown.sh +156 -0
  78. package/scripts/install-onyx-hooks.js +373 -0
  79. package/scripts/install.js +119 -18
  80. package/scripts/redis-monitor.js +284 -0
  81. package/scripts/redis-setup.js +412 -0
  82. package/scripts/test-memory-retrieval.js +201 -0
  83. package/scripts/validate-exports.js +68 -0
  84. package/scripts/validate-package.js +120 -0
  85. package/scripts/verify-onyx-deployment.js +309 -0
  86. package/scripts/verify-redis-deployment.js +354 -0
  87. package/scripts/verify-redis-init.js +219 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * ENHANCED INIT WITH MODE ENFORCEMENT
3
+ *
4
+ * This module extends the standard init.js to include mode enforcement status.
5
+ * Use this when you want full orchestrator mode visibility on startup.
6
+ */
7
+
8
+ import { loadIdentity, addRepo } from './identity.js';
9
+ import { getEnforcer, MODES } from './mode-enforcer.js';
10
+ import { getGate, initOrchestratorMode } from './orchestrator-gate.js';
11
+ import Redis from 'ioredis';
12
+ import dotenv from 'dotenv';
13
+ import { fileURLToPath } from 'url';
14
+ import { dirname, join } from 'path';
15
+ import { existsSync } from 'fs';
16
+ import os from 'os';
17
+ import { exec } from 'child_process';
18
+ import { promisify } from 'util';
19
+
20
+ const execAsync = promisify(exec);
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+
25
+ // Load environment variables
26
+ const envPath = join(os.homedir(), '.boss-claude', '.env');
27
+ if (existsSync(envPath)) {
28
+ dotenv.config({ path: envPath });
29
+ }
30
+
31
+ let redis = null;
32
+
33
+ function getRedis() {
34
+ if (!redis) {
35
+ if (!process.env.REDIS_URL) {
36
+ throw new Error('REDIS_URL not found. Please run: boss-claude init');
37
+ }
38
+ redis = new Redis(process.env.REDIS_URL);
39
+ }
40
+ return redis;
41
+ }
42
+
43
+ async function getCurrentRepo() {
44
+ try {
45
+ const { stdout: repoPath } = await execAsync('git rev-parse --show-toplevel');
46
+ const { stdout: repoUrl } = await execAsync('git config --get remote.origin.url');
47
+
48
+ const repoName = repoUrl.trim().split('/').pop().replace('.git', '');
49
+
50
+ return {
51
+ name: repoName,
52
+ path: repoPath.trim(),
53
+ url: repoUrl.trim()
54
+ };
55
+ } catch (error) {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get full status including mode enforcement
62
+ */
63
+ export async function getStatusWithMode() {
64
+ // Load Boss identity
65
+ const boss = await loadIdentity();
66
+
67
+ // Calculate XP to next level
68
+ const xp_to_next_level = boss.level * 100;
69
+
70
+ // Get current repo info
71
+ const repo = await getCurrentRepo();
72
+
73
+ let repoStats = null;
74
+
75
+ if (repo) {
76
+ // Register repo if new
77
+ await addRepo(repo.name);
78
+
79
+ // Get repo stats
80
+ const client = getRedis();
81
+ const repoKey = `boss:repo:${repo.name}`;
82
+ const repoData = await client.get(repoKey);
83
+
84
+ if (repoData) {
85
+ repoStats = JSON.parse(repoData);
86
+ } else {
87
+ // Initialize repo stats
88
+ repoStats = {
89
+ name: repo.name,
90
+ path: repo.path,
91
+ session_count: 0,
92
+ first_seen: new Date().toISOString(),
93
+ last_active: null
94
+ };
95
+ await client.set(repoKey, JSON.stringify(repoStats));
96
+ }
97
+ }
98
+
99
+ // Get mode enforcement status
100
+ const enforcer = getEnforcer();
101
+ const currentMode = await enforcer.getCurrentMode();
102
+ const modeMetadata = await enforcer.getModeMetadata();
103
+ const agentIdentity = await enforcer.getAgentIdentity();
104
+
105
+ return {
106
+ boss: {
107
+ ...boss,
108
+ xp_to_next_level
109
+ },
110
+ repo: repo ? {
111
+ ...repo,
112
+ ...repoStats
113
+ } : null,
114
+ mode: {
115
+ current: currentMode,
116
+ metadata: modeMetadata,
117
+ agent: agentIdentity
118
+ }
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Format status for Claude with mode enforcement
124
+ */
125
+ export async function formatStatusForClaudeWithMode() {
126
+ const status = await getStatusWithMode();
127
+
128
+ let output = `
129
+ 🤖 BOSS CLAUDE ORCHESTRATOR MODE
130
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
131
+
132
+ 👤 BOSS IDENTITY
133
+ Level ${status.boss.level} • ${status.boss.xp}/${status.boss.xp_to_next_level} XP (${Math.floor((status.boss.xp / status.boss.xp_to_next_level) * 100)}%)
134
+ 💰 Token Bank: ${status.boss.token_bank.toLocaleString()} tokens
135
+ 📊 Total Sessions: ${status.boss.total_sessions}
136
+ 🏢 Repos Managed: ${status.boss.repos_managed}
137
+
138
+ 🎯 MODE ENFORCEMENT
139
+ Active Mode: ${status.mode.current.toUpperCase()}
140
+ Set By: ${status.mode.metadata?.setBy || 'system'}
141
+ Set At: ${status.mode.metadata?.setAt ? new Date(status.mode.metadata.setAt).toLocaleString() : 'N/A'}
142
+ Reason: ${status.mode.metadata?.reason || 'N/A'}
143
+ `;
144
+
145
+ if (status.mode.agent) {
146
+ output += ` Agent: ${status.mode.agent.agent}`;
147
+ if (status.mode.agent.domain) {
148
+ output += ` (${status.mode.agent.domain})`;
149
+ }
150
+ output += `\n`;
151
+ }
152
+
153
+ if (status.repo) {
154
+ output += `
155
+ 📁 CURRENT REPOSITORY
156
+ Name: ${status.repo.name}
157
+ Path: ${status.repo.path}
158
+ Sessions: ${status.repo.session_count}
159
+ Last Active: ${status.repo.last_active || 'Never'}
160
+ `;
161
+ } else {
162
+ output += `
163
+ ⚠️ Not currently in a git repository
164
+ `;
165
+ }
166
+
167
+ output += `
168
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
169
+ 💡 Mode Commands: boss-claude mode [orchestrator|specialist|worker|review|learning]
170
+ 📊 Status: boss-claude mode status
171
+ 📜 History: boss-claude mode history
172
+ `;
173
+
174
+ return output;
175
+ }
176
+
177
+ /**
178
+ * Initialize orchestrator session with mode enforcement
179
+ */
180
+ export async function initOrchestratorSession(sessionId = null) {
181
+ // Generate session ID if not provided
182
+ if (!sessionId) {
183
+ sessionId = `session-${Date.now()}`;
184
+ }
185
+
186
+ // Initialize orchestrator mode
187
+ await initOrchestratorMode(sessionId);
188
+
189
+ // Get and display status
190
+ const status = await formatStatusForClaudeWithMode();
191
+
192
+ console.log(status);
193
+
194
+ return {
195
+ sessionId,
196
+ mode: MODES.ORCHESTRATOR,
197
+ ready: true
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Pre-action hook - checks mode before every action
203
+ *
204
+ * Usage:
205
+ * await preActionHook('delegate', { agent: 'postgres-specialist', tokens: 15000 });
206
+ */
207
+ export async function preActionHook(actionType, options = {}) {
208
+ const gate = getGate();
209
+
210
+ switch (actionType) {
211
+ case 'delegate':
212
+ await gate.beforeDelegate(
213
+ options.agent,
214
+ { description: options.description || 'task' },
215
+ options.tokens || 0
216
+ );
217
+ break;
218
+
219
+ case 'execute':
220
+ await gate.beforeExecute(
221
+ { description: options.description || 'action' },
222
+ options.tokens || 0
223
+ );
224
+ break;
225
+
226
+ case 'review':
227
+ await gate.beforeReview(
228
+ options.agent,
229
+ options.code || {}
230
+ );
231
+ break;
232
+
233
+ case 'learn':
234
+ await gate.beforeLearn(
235
+ options.type || 'general',
236
+ options.data || {}
237
+ );
238
+ break;
239
+
240
+ case 'config':
241
+ await gate.beforeConfigChange(
242
+ options.key,
243
+ options.value
244
+ );
245
+ break;
246
+
247
+ default:
248
+ throw new Error(`Unknown action type: ${actionType}`);
249
+ }
250
+
251
+ return true;
252
+ }
253
+
254
+ /**
255
+ * Exports
256
+ */
257
+ export {
258
+ getStatusWithMode,
259
+ initOrchestratorSession,
260
+ preActionHook
261
+ };
package/lib/init.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { loadIdentity, addRepo } from './identity.js';
2
+ import { getEfficiencyStats } from './session.js';
2
3
  import Redis from 'ioredis';
3
4
  import dotenv from 'dotenv';
4
5
  import { fileURLToPath } from 'url';
@@ -7,6 +8,7 @@ import { existsSync } from 'fs';
7
8
  import os from 'os';
8
9
  import { exec } from 'child_process';
9
10
  import { promisify } from 'util';
11
+ import { displayOnyxBanner } from './onyx-banner.js';
10
12
 
11
13
  const execAsync = promisify(exec);
12
14
 
@@ -99,11 +101,32 @@ export async function getStatus() {
99
101
  export async function formatStatusForClaude() {
100
102
  const status = await getStatus();
101
103
 
102
- let output = `
103
- 🎮 BOSS CLAUDE AUTO-LOADED
104
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104
+ // Display ONYX MODE banner at conversation start
105
+ const onyxBanner = displayOnyxBanner(true);
105
106
 
106
- 👤 BOSS STATUS
107
+ let output = `${onyxBanner}
108
+
109
+ ═══════════════════════════════════════════════════════════════════════════════
110
+ 🎼 YOU ARE ONYX - THE CONDUCTOR (I direct, I don't play)
111
+ ═══════════════════════════════════════════════════════════════════════════════
112
+
113
+ 🏆 WIN CONDITION: I win by SHIPPING complete, working, bug-free, sellable, scalable projects.
114
+ "I don't get XP for talking. I get XP for shipping."
115
+
116
+ ⛔ CONDUCTOR'S RULE: I NEVER play an instrument.
117
+ I wave the baton (Task tool). My musicians (agents) make the music.
118
+
119
+ 🎯 DELEGATION MATRIX:
120
+ "find/search/where" → Task(Explore): "Search codebase for..."
121
+ "read/show/what's" → Task(Explore): "Read and summarize..."
122
+ "build/create/fix" → Task(general-purpose): "Implement..."
123
+ "run/npm/git" → Task(Bash): "Execute..."
124
+ "plan/design" → Task(Plan): "Design approach..."
125
+
126
+ ❌ FORBIDDEN: Read, Write, Edit, Bash, Grep, Glob, NotebookEdit
127
+ ✅ ALLOWED: Task, WebFetch, WebSearch, TodoWrite, Skill
128
+
129
+ 🎮 BOSS STATUS
107
130
  Level ${status.boss.level} • ${status.boss.xp}/${status.boss.xp_to_next_level} XP (${Math.floor((status.boss.xp / status.boss.xp_to_next_level) * 100)}%)
108
131
  💰 Token Bank: ${status.boss.token_bank.toLocaleString()} tokens
109
132
  📊 Total Sessions: ${status.boss.total_sessions}
@@ -124,9 +147,24 @@ export async function formatStatusForClaude() {
124
147
  `;
125
148
  }
126
149
 
150
+ // Add efficiency stats if available
151
+ const efficiency = await getEfficiencyStats();
152
+ if (efficiency) {
153
+ output += `
154
+ ⚡ EFFICIENCY TRACKER (XP Multiplier)
155
+ 🎺 ONYX Tokens: ${efficiency.onyx_tokens.toLocaleString()} (orchestration)
156
+ 🎻 Agent Tokens: ${efficiency.agent_tokens.toLocaleString()} (work done)
157
+ 📈 Efficiency Ratio: ${efficiency.efficiency_ratio}
158
+ 🎯 Delegations: ${efficiency.delegations}
159
+ 💎 Projected Bonus XP: +${efficiency.projected_bonus_xp} (efficiency) +${efficiency.delegation_bonus_xp} (delegation)
160
+ `;
161
+ }
162
+
127
163
  output += `
128
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
129
- 💡 Commands: boss-claude status | save | recall
164
+ ═══════════════════════════════════════════════════════════════════════════════
165
+ 💡 Commands: boss-claude status | save | recall | checkpoint:status
166
+ ⏱️ CONTEXT REFRESH: Run "boss-claude status" every 30 seconds
167
+ ═══════════════════════════════════════════════════════════════════════════════
130
168
  `;
131
169
 
132
170
  return output;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Memory Result Aggregator
3
+ *
4
+ * Consolidates results from 4 memory engineers:
5
+ * - Redis (session cache)
6
+ * - GitHub (long-term memory)
7
+ * - PostgreSQL (structured data)
8
+ * - Qdrant (vector search)
9
+ *
10
+ * Features:
11
+ * - Deduplicates by session ID / unique identifier
12
+ * - Scores by relevance (source weight + recency + similarity)
13
+ * - Ranks results highest to lowest
14
+ * - Returns top N results
15
+ */
16
+
17
+ // Source weights for scoring (higher = more authoritative)
18
+ const SOURCE_WEIGHTS = {
19
+ qdrant: 1.0, // Vector similarity is most relevant
20
+ redis: 0.9, // Recent session data is highly relevant
21
+ postgres: 0.8, // Structured data is authoritative
22
+ github: 0.7 // Long-term memory is valuable but older
23
+ };
24
+
25
+ // Recency decay - older memories get lower scores
26
+ const RECENCY_HALF_LIFE_DAYS = 30; // Score halves every 30 days
27
+
28
+ /**
29
+ * Calculate recency score based on age
30
+ * @param {Date|string} timestamp - When the memory was created
31
+ * @returns {number} Score from 0-1 (1 = just created, 0.5 = 30 days old)
32
+ */
33
+ function calculateRecencyScore(timestamp) {
34
+ if (!timestamp) return 0.5; // Default to mid-range if no timestamp
35
+
36
+ const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
37
+
38
+ // FIX: Validate date to prevent Invalid Date propagation
39
+ if (isNaN(date.getTime())) {
40
+ console.warn('[Aggregator] Invalid timestamp, using default score:', timestamp);
41
+ return 0.5;
42
+ }
43
+
44
+ const ageMs = Date.now() - date.getTime();
45
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
46
+
47
+ // Exponential decay: score = 2^(-age/half_life)
48
+ return Math.pow(2, -ageDays / RECENCY_HALF_LIFE_DAYS);
49
+ }
50
+
51
+ /**
52
+ * Extract unique identifier from memory object
53
+ * @param {Object} memory - Memory object from any source
54
+ * @returns {string} Unique identifier
55
+ */
56
+ function extractIdentifier(memory) {
57
+ // Try various ID fields across different sources
58
+ const directId = memory.session_id
59
+ || memory.id
60
+ || memory.issue_number
61
+ || memory.uuid
62
+ || memory.point_id
63
+ || memory.url;
64
+
65
+ if (directId) return directId;
66
+
67
+ // FIX: Prevent DoS from circular references in JSON.stringify
68
+ // Use a simple hash of primitive fields instead
69
+ try {
70
+ const hashableFields = {
71
+ type: memory.type,
72
+ created: memory.created_at || memory.timestamp,
73
+ // Include first 500 chars of content (increased from 100) for better deduplication
74
+ content: typeof memory.content === 'string' ? memory.content.substring(0, 500) : memory.content
75
+ };
76
+ return JSON.stringify(hashableFields);
77
+ } catch (error) {
78
+ // If even this fails, return a timestamp-based ID
79
+ console.warn('[Aggregator] Failed to generate identifier, using timestamp');
80
+ return `fallback-${Date.now()}-${Math.random()}`;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Extract timestamp from memory object
86
+ * @param {Object} memory - Memory object from any source
87
+ * @returns {Date|string|null} Timestamp
88
+ */
89
+ function extractTimestamp(memory) {
90
+ return memory.created_at
91
+ || memory.timestamp
92
+ || memory.updated_at
93
+ || memory.date
94
+ || null;
95
+ }
96
+
97
+ /**
98
+ * Extract similarity score from memory object (if available)
99
+ * @param {Object} memory - Memory object from any source
100
+ * @returns {number} Similarity score 0-1
101
+ */
102
+ function extractSimilarity(memory) {
103
+ // Qdrant and vector sources typically provide a score
104
+ if (memory.score !== undefined) return memory.score;
105
+ if (memory.similarity !== undefined) return memory.similarity;
106
+ if (memory._distance !== undefined) return 1 - memory._distance; // Convert distance to similarity
107
+ return 0.5; // Default mid-range if no similarity provided
108
+ }
109
+
110
+ /**
111
+ * Calculate composite relevance score
112
+ * @param {Object} memory - Memory object
113
+ * @param {string} source - Source identifier (redis, github, postgres, qdrant)
114
+ * @returns {number} Composite score
115
+ */
116
+ function calculateRelevanceScore(memory, source) {
117
+ const sourceWeight = SOURCE_WEIGHTS[source] || 0.5;
118
+ const recencyScore = calculateRecencyScore(extractTimestamp(memory));
119
+ const similarityScore = extractSimilarity(memory);
120
+
121
+ // Weighted average: 40% source, 30% recency, 30% similarity
122
+ return (sourceWeight * 0.4) + (recencyScore * 0.3) + (similarityScore * 0.3);
123
+ }
124
+
125
+ /**
126
+ * Deduplicate memories by unique identifier
127
+ * @param {Array} memories - Array of memory objects with metadata
128
+ * @returns {Array} Deduplicated memories (keeps highest scoring duplicate)
129
+ */
130
+ function deduplicate(memories) {
131
+ const seen = new Map();
132
+
133
+ for (const item of memories) {
134
+ const id = extractIdentifier(item.memory);
135
+
136
+ if (!seen.has(id)) {
137
+ seen.set(id, item);
138
+ } else {
139
+ // Keep the one with higher score
140
+ const existing = seen.get(id);
141
+ if (item.score > existing.score) {
142
+ seen.set(id, item);
143
+ }
144
+ }
145
+ }
146
+
147
+ return Array.from(seen.values());
148
+ }
149
+
150
+ /**
151
+ * Aggregate and rank results from multiple memory sources
152
+ * @param {Object} results - Object containing results from each source
153
+ * @param {Array} results.redis - Results from Redis cache
154
+ * @param {Array} results.github - Results from GitHub issues
155
+ * @param {Array} results.postgres - Results from PostgreSQL
156
+ * @param {Array} results.qdrant - Results from Qdrant vector DB
157
+ * @param {number} topN - Number of top results to return (default: 10)
158
+ * @returns {Array} Ranked and scored results
159
+ */
160
+ export function aggregateMemoryResults(results = {}, topN = 10) {
161
+ const {
162
+ redis = [],
163
+ github = [],
164
+ postgres = [],
165
+ qdrant = []
166
+ } = results;
167
+
168
+ // Tag each memory with its source and calculate relevance score
169
+ const scoredMemories = [
170
+ ...redis.map(m => ({
171
+ memory: m,
172
+ source: 'redis',
173
+ score: calculateRelevanceScore(m, 'redis')
174
+ })),
175
+ ...github.map(m => ({
176
+ memory: m,
177
+ source: 'github',
178
+ score: calculateRelevanceScore(m, 'github')
179
+ })),
180
+ ...postgres.map(m => ({
181
+ memory: m,
182
+ source: 'postgres',
183
+ score: calculateRelevanceScore(m, 'postgres')
184
+ })),
185
+ ...qdrant.map(m => ({
186
+ memory: m,
187
+ source: 'qdrant',
188
+ score: calculateRelevanceScore(m, 'qdrant')
189
+ }))
190
+ ];
191
+
192
+ // Deduplicate by identifier (keeps highest scoring duplicate)
193
+ const deduplicated = deduplicate(scoredMemories);
194
+
195
+ // Sort by score descending (highest first)
196
+ deduplicated.sort((a, b) => b.score - a.score);
197
+
198
+ // Return top N results
199
+ return deduplicated.slice(0, topN);
200
+ }
201
+
202
+ /**
203
+ * Format aggregated results for display
204
+ * @param {Array} aggregatedResults - Results from aggregateMemoryResults
205
+ * @returns {Array} Formatted results with readable metadata
206
+ */
207
+ export function formatResults(aggregatedResults) {
208
+ return aggregatedResults.map((item, index) => ({
209
+ rank: index + 1,
210
+ score: item.score.toFixed(3),
211
+ source: item.source,
212
+ id: extractIdentifier(item.memory),
213
+ timestamp: extractTimestamp(item.memory),
214
+ similarity: extractSimilarity(item.memory).toFixed(3),
215
+ memory: item.memory
216
+ }));
217
+ }
218
+
219
+ /**
220
+ * Get statistics about aggregated results
221
+ * @param {Array} aggregatedResults - Results from aggregateMemoryResults
222
+ * @returns {Object} Statistics summary
223
+ */
224
+ export function getAggregationStats(aggregatedResults) {
225
+ const sources = aggregatedResults.reduce((acc, item) => {
226
+ acc[item.source] = (acc[item.source] || 0) + 1;
227
+ return acc;
228
+ }, {});
229
+
230
+ const scores = aggregatedResults.map(r => r.score);
231
+ const avgScore = scores.length > 0
232
+ ? scores.reduce((a, b) => a + b, 0) / scores.length
233
+ : 0;
234
+
235
+ return {
236
+ total: aggregatedResults.length,
237
+ sources,
238
+ averageScore: avgScore.toFixed(3),
239
+ topScore: scores.length > 0 ? Math.max(...scores).toFixed(3) : 0,
240
+ bottomScore: scores.length > 0 ? Math.min(...scores).toFixed(3) : 0
241
+ };
242
+ }
243
+
244
+ export default {
245
+ aggregateMemoryResults,
246
+ formatResults,
247
+ getAggregationStats,
248
+ calculateRelevanceScore,
249
+ extractIdentifier,
250
+ extractTimestamp,
251
+ extractSimilarity
252
+ };
package/lib/memory.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
5
  import { existsSync } from 'fs';
6
6
  import os from 'os';
7
+ import { queryMemorySupervisor, invalidateMemoryCache, getMemoryCacheStats } from './agents/memory-supervisor.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
@@ -41,6 +42,13 @@ export async function saveMemory({ repo_name, summary, content, tags = [] }) {
41
42
  labels: ['session', repo_name, ...tags]
42
43
  });
43
44
 
45
+ // Invalidate all memory caches after saving new memory
46
+ try {
47
+ await invalidateMemoryCache();
48
+ } catch (err) {
49
+ console.warn('Failed to invalidate cache after save:', err.message);
50
+ }
51
+
44
52
  return {
45
53
  issue_number: issue.data.number,
46
54
  url: issue.data.html_url,
@@ -55,23 +63,37 @@ export async function searchMemory(query, limit = 5) {
55
63
  const owner = process.env.GITHUB_OWNER || 'cpretzinger';
56
64
  const repo = process.env.GITHUB_MEMORY_REPO || 'boss-claude-memory';
57
65
 
58
- // Search issues
59
- const { data } = await client.search.issuesAndPullRequests({
60
- q: `repo:${owner}/${repo} ${query} label:session`,
66
+ const { data } = await client.issues.listForRepo({
67
+ owner,
68
+ repo,
69
+ labels: 'session',
70
+ state: 'all',
61
71
  sort: 'created',
62
- order: 'desc',
63
- per_page: limit
72
+ direction: 'desc',
73
+ per_page: 100
64
74
  });
65
75
 
66
- return data.items.map(issue => ({
76
+ const queryLower = query.toLowerCase();
77
+ const filtered = data.filter(issue => {
78
+ const titleMatch = issue.title.toLowerCase().includes(queryLower);
79
+ const bodyMatch = issue.body && issue.body.toLowerCase().includes(queryLower);
80
+ const labelMatch = issue.labels.some(l => l.name.toLowerCase().includes(queryLower));
81
+ return titleMatch || bodyMatch || labelMatch;
82
+ }).slice(0, limit);
83
+
84
+ return filtered.map(issue => ({
67
85
  title: issue.title,
68
- summary: issue.body.split('\n\n')[1] || '',
86
+ summary: issue.body ? issue.body.split('\n\n')[1] || '' : '',
69
87
  url: issue.html_url,
70
88
  created_at: issue.created_at,
71
89
  labels: issue.labels.map(l => l.name)
72
90
  }));
73
91
  }
74
92
 
93
+ export async function searchMemoryAdvanced(query, options = {}) {
94
+ return await queryMemorySupervisor(query, options);
95
+ }
96
+
75
97
  export async function getMemoryByIssue(issueNumber) {
76
98
  const client = getOctokit();
77
99
 
@@ -92,3 +114,9 @@ export async function getMemoryByIssue(issueNumber) {
92
114
  labels: data.labels.map(l => l.name)
93
115
  };
94
116
  }
117
+
118
+ export const memorySupervisor = {
119
+ query: queryMemorySupervisor,
120
+ invalidateCache: invalidateMemoryCache,
121
+ getCacheStats: getMemoryCacheStats
122
+ };