groove-dev 0.26.38 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CLAUDE.md +24 -19
  3. package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
  6. package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
  7. package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
  8. package/node_modules/@groove-dev/daemon/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
  10. package/node_modules/@groove-dev/daemon/src/api.js +346 -22
  11. package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
  12. package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
  13. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
  14. package/node_modules/@groove-dev/daemon/src/index.js +28 -4
  15. package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
  17. package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
  18. package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
  19. package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
  20. package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
  21. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  22. package/node_modules/@groove-dev/daemon/src/process.js +141 -9
  23. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  24. package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
  25. package/node_modules/@groove-dev/daemon/src/router.js +43 -0
  26. package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
  27. package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
  28. package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
  30. package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
  31. package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
  32. package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
  33. package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
  34. package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
  35. package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
  37. package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
  38. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  39. package/node_modules/@groove-dev/gui/package.json +1 -4
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  49. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  50. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
  51. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
  52. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  53. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  54. package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
  55. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
  56. package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
  57. package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
  58. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
  59. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
  60. package/package.json +2 -8
  61. package/packages/cli/bin/groove.js +2 -0
  62. package/packages/cli/package.json +1 -1
  63. package/packages/cli/src/commands/nuke.js +16 -4
  64. package/packages/cli/src/commands/stop.js +17 -2
  65. package/packages/daemon/integrations-registry.json +681 -75
  66. package/packages/daemon/package.json +1 -1
  67. package/packages/daemon/src/adaptive.js +23 -25
  68. package/packages/daemon/src/api.js +346 -22
  69. package/packages/daemon/src/classifier.js +53 -6
  70. package/packages/daemon/src/firstrun.js +14 -1
  71. package/packages/daemon/src/gateways/manager.js +2 -2
  72. package/packages/daemon/src/index.js +28 -4
  73. package/packages/daemon/src/integrations.js +215 -14
  74. package/packages/daemon/src/introducer.js +84 -11
  75. package/packages/daemon/src/journalist.js +43 -1
  76. package/packages/daemon/src/lockmanager.js +60 -0
  77. package/packages/daemon/src/mcp-manager.js +270 -0
  78. package/packages/daemon/src/memory.js +370 -0
  79. package/packages/daemon/src/pm.js +1 -1
  80. package/packages/daemon/src/process.js +141 -9
  81. package/packages/daemon/src/registry.js +1 -1
  82. package/packages/daemon/src/rotator.js +334 -31
  83. package/packages/daemon/src/router.js +43 -0
  84. package/packages/daemon/src/tokentracker.js +70 -18
  85. package/packages/daemon/src/validate.js +5 -13
  86. package/packages/daemon/templates/groove-slides.cjs +306 -0
  87. package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
  88. package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
  89. package/packages/gui/dist/index.html +2 -2
  90. package/packages/gui/package.json +1 -4
  91. package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
  92. package/packages/gui/src/components/agents/agent-config.jsx +22 -1
  93. package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
  94. package/packages/gui/src/components/agents/agent-node.jsx +132 -90
  95. package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
  96. package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
  97. package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
  98. package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
  99. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  100. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  101. package/packages/gui/src/components/layout/app-shell.jsx +24 -19
  102. package/packages/gui/src/components/layout/command-palette.jsx +2 -2
  103. package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  104. package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  105. package/packages/gui/src/lib/format.js +0 -6
  106. package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
  107. package/packages/gui/src/stores/groove.js +59 -9
  108. package/packages/gui/src/views/agents.jsx +84 -10
  109. package/packages/gui/src/views/dashboard.jsx +24 -21
  110. package/packages/gui/src/views/marketplace.jsx +153 -85
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
  112. package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
  113. package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
  114. package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
  115. package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
  116. package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
  117. package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
  118. package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
  119. package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
  120. package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
  121. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
  122. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
  123. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
  124. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
  125. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
  126. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
  127. package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
  128. package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
  129. package/node_modules/@radix-ui/react-popover/README.md +0 -3
  130. package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
  131. package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
  132. package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
  133. package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
  134. package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
  135. package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
  136. package/node_modules/@radix-ui/react-popover/package.json +0 -82
  137. package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
  138. package/node_modules/@radix-ui/react-separator/README.md +0 -3
  139. package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
  140. package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
  141. package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
  142. package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
  143. package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
  144. package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
  145. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
  146. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
  147. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
  148. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
  149. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
  150. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
  151. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
  152. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
  153. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
  154. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
  155. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
  156. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
  157. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
  158. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
  159. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
  160. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
  161. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
  162. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
  163. package/node_modules/@radix-ui/react-separator/package.json +0 -69
  164. package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
  165. package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
  166. package/packages/gui/dist/groove-logo-short.png +0 -0
  167. package/packages/gui/dist/groove-logo.png +0 -0
  168. package/packages/gui/public/groove-logo-short.png +0 -0
  169. package/packages/gui/public/groove-logo.png +0 -0
  170. package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
  171. package/packages/gui/src/lib/hooks/use-media-query.js +0 -18
@@ -0,0 +1,270 @@
1
+ // GROOVE — MCP Manager (Provider-Agnostic Integration Execution)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { spawn as cpSpawn } from 'child_process';
5
+
6
+ const IDLE_TIMEOUT_MS = 10 * 60 * 1000;
7
+ const MAX_RETRIES = 3;
8
+
9
+ export class McpManager {
10
+ constructor(daemon) {
11
+ this.daemon = daemon;
12
+ this.servers = new Map();
13
+ this._crashCounts = new Map();
14
+ this._nextId = 1;
15
+ }
16
+
17
+ async startServer(integrationId) {
18
+ if (this.servers.has(integrationId)) {
19
+ const existing = this.servers.get(integrationId);
20
+ if (existing.proc && !existing.proc.killed) {
21
+ return existing.tools;
22
+ }
23
+ this._cleanup(integrationId);
24
+ }
25
+
26
+ const entry = this.daemon.integrations.registry.find((s) => s.id === integrationId);
27
+ if (!entry) throw new Error(`Integration not found: ${integrationId}`);
28
+
29
+ if (!this.daemon.integrations._isInstalled(integrationId)) {
30
+ throw new Error(`Integration not installed: ${integrationId}`);
31
+ }
32
+
33
+ if ((this._crashCounts.get(integrationId) || 0) >= MAX_RETRIES) {
34
+ throw new Error(`Integration ${integrationId} crashed ${MAX_RETRIES} times — not restarting`);
35
+ }
36
+
37
+ const command = entry.command || 'npx';
38
+ const args = entry.args || ['-y', entry.npmPackage];
39
+
40
+ const env = { ...process.env };
41
+ const spawnEnv = this.daemon.integrations.getSpawnEnv([integrationId]);
42
+ Object.assign(env, spawnEnv);
43
+
44
+ const proc = cpSpawn(command, args, {
45
+ env,
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ detached: false,
48
+ });
49
+
50
+ const server = {
51
+ proc,
52
+ integrationId,
53
+ tools: [],
54
+ pending: new Map(),
55
+ buffer: '',
56
+ lastCall: Date.now(),
57
+ idleTimer: null,
58
+ };
59
+
60
+ this.servers.set(integrationId, server);
61
+
62
+ proc.stdout.on('data', (chunk) => {
63
+ server.buffer += chunk.toString();
64
+ this._processBuffer(server);
65
+ });
66
+
67
+ proc.stderr.on('data', (chunk) => {
68
+ console.log(`[Groove:MCP:${integrationId}] stderr: ${chunk.toString().slice(0, 200)}`);
69
+ });
70
+
71
+ proc.on('error', (err) => {
72
+ console.log(`[Groove:MCP:${integrationId}] Process error: ${err.message}`);
73
+ this._handleCrash(integrationId);
74
+ });
75
+
76
+ proc.on('exit', (code, signal) => {
77
+ console.log(`[Groove:MCP:${integrationId}] Process exited: code=${code} signal=${signal}`);
78
+ if ((code !== 0 && code !== null) || (code === null && signal)) {
79
+ this._handleCrash(integrationId);
80
+ }
81
+ });
82
+
83
+ const tools = await this._initialize(server);
84
+ server.tools = tools;
85
+ this._crashCounts.delete(integrationId);
86
+ this._resetIdleTimer(server);
87
+
88
+ console.log(`[Groove:MCP:${integrationId}] Started — ${tools.length} tools available`);
89
+ return tools;
90
+ }
91
+
92
+ stopServer(integrationId) {
93
+ const server = this.servers.get(integrationId);
94
+ if (!server) return;
95
+ this._cleanup(integrationId);
96
+ console.log(`[Groove:MCP:${integrationId}] Stopped`);
97
+ }
98
+
99
+ async execTool(integrationId, toolName, params) {
100
+ let server = this.servers.get(integrationId);
101
+ if (!server || server.proc.killed) {
102
+ await this.startServer(integrationId);
103
+ server = this.servers.get(integrationId);
104
+ }
105
+ if (!server) throw new Error(`Failed to start MCP server for ${integrationId}`);
106
+
107
+ server.lastCall = Date.now();
108
+ this._resetIdleTimer(server);
109
+
110
+ const id = this._nextId++;
111
+ const msg = {
112
+ jsonrpc: '2.0',
113
+ id,
114
+ method: 'tools/call',
115
+ params: { name: toolName, arguments: params || {} },
116
+ };
117
+
118
+ const result = await this._sendRequest(server, id, msg);
119
+
120
+ if (result.error) {
121
+ throw new Error(result.error.message || JSON.stringify(result.error));
122
+ }
123
+
124
+ return result.result;
125
+ }
126
+
127
+ async listTools(integrationId) {
128
+ const server = this.servers.get(integrationId);
129
+ if (server && !server.proc.killed && server.tools.length > 0) {
130
+ return server.tools;
131
+ }
132
+ return this.startServer(integrationId);
133
+ }
134
+
135
+ stopAll() {
136
+ for (const integrationId of this.servers.keys()) {
137
+ this._cleanup(integrationId);
138
+ }
139
+ console.log('[Groove:MCP] All servers stopped');
140
+ }
141
+
142
+ _processBuffer(server) {
143
+ const lines = server.buffer.split('\n');
144
+ server.buffer = lines.pop() || '';
145
+
146
+ for (const line of lines) {
147
+ const trimmed = line.trim();
148
+ if (!trimmed) continue;
149
+ try {
150
+ const msg = JSON.parse(trimmed);
151
+ if (msg.id !== undefined && server.pending.has(msg.id)) {
152
+ const { resolve } = server.pending.get(msg.id);
153
+ server.pending.delete(msg.id);
154
+ resolve(msg);
155
+ }
156
+ } catch {
157
+ // Not JSON — ignore
158
+ }
159
+ }
160
+ }
161
+
162
+ _sendRequest(server, id, msg) {
163
+ return new Promise((resolve, reject) => {
164
+ const timeout = setTimeout(() => {
165
+ server.pending.delete(id);
166
+ reject(new Error(`MCP request timed out (id=${id}, method=${msg.method})`));
167
+ }, 30_000);
168
+
169
+ server.pending.set(id, {
170
+ resolve: (result) => {
171
+ clearTimeout(timeout);
172
+ resolve(result);
173
+ },
174
+ });
175
+
176
+ try {
177
+ server.proc.stdin.write(JSON.stringify(msg) + '\n');
178
+ } catch (err) {
179
+ clearTimeout(timeout);
180
+ server.pending.delete(id);
181
+ reject(new Error(`Failed to write to MCP server: ${err.message}`));
182
+ }
183
+ });
184
+ }
185
+
186
+ async _initialize(server) {
187
+ const initId = this._nextId++;
188
+ const initMsg = {
189
+ jsonrpc: '2.0',
190
+ id: initId,
191
+ method: 'initialize',
192
+ params: {
193
+ protocolVersion: '2024-11-05',
194
+ capabilities: {},
195
+ clientInfo: { name: 'groove', version: '1.0.0' },
196
+ },
197
+ };
198
+
199
+ const initResp = await this._sendRequest(server, initId, initMsg);
200
+ if (initResp.error) {
201
+ throw new Error(initResp.error.message || 'MCP initialize failed');
202
+ }
203
+
204
+ const notif = { jsonrpc: '2.0', method: 'notifications/initialized' };
205
+ try {
206
+ server.proc.stdin.write(JSON.stringify(notif) + '\n');
207
+ } catch {
208
+ throw new Error('MCP server died during initialization');
209
+ }
210
+
211
+ const listId = this._nextId++;
212
+ const listMsg = {
213
+ jsonrpc: '2.0',
214
+ id: listId,
215
+ method: 'tools/list',
216
+ params: {},
217
+ };
218
+
219
+ const listResp = await this._sendRequest(server, listId, listMsg);
220
+ return listResp.result?.tools || [];
221
+ }
222
+
223
+ _handleCrash(integrationId) {
224
+ const server = this.servers.get(integrationId);
225
+ if (!server) return;
226
+
227
+ for (const [, { resolve }] of server.pending) {
228
+ resolve({ error: { message: 'MCP server crashed' } });
229
+ }
230
+ server.pending.clear();
231
+
232
+ const crashes = (this._crashCounts.get(integrationId) || 0) + 1;
233
+ this._crashCounts.set(integrationId, crashes);
234
+
235
+ if (crashes >= MAX_RETRIES) {
236
+ console.log(`[Groove:MCP:${integrationId}] Max retries reached (${crashes}/${MAX_RETRIES}) — giving up`);
237
+ } else {
238
+ console.log(`[Groove:MCP:${integrationId}] Crash ${crashes}/${MAX_RETRIES} — will restart on next call`);
239
+ }
240
+ this._cleanup(integrationId);
241
+ }
242
+
243
+ _resetIdleTimer(server) {
244
+ if (server.idleTimer) clearTimeout(server.idleTimer);
245
+ server.idleTimer = setTimeout(() => {
246
+ console.log(`[Groove:MCP:${server.integrationId}] Idle timeout — stopping`);
247
+ this.stopServer(server.integrationId);
248
+ }, IDLE_TIMEOUT_MS);
249
+ }
250
+
251
+ _cleanup(integrationId) {
252
+ const server = this.servers.get(integrationId);
253
+ if (!server) return;
254
+
255
+ if (server.idleTimer) clearTimeout(server.idleTimer);
256
+
257
+ for (const [, { resolve }] of server.pending) {
258
+ resolve({ error: { message: 'MCP server stopped' } });
259
+ }
260
+ server.pending.clear();
261
+
262
+ try {
263
+ if (server.proc && !server.proc.killed) {
264
+ server.proc.kill('SIGTERM');
265
+ }
266
+ } catch { /* ignore */ }
267
+
268
+ this.servers.delete(integrationId);
269
+ }
270
+ }
@@ -0,0 +1,370 @@
1
+ // GROOVE — Persistent Agent Memory (Layer 7)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+ //
4
+ // Four file types, all in .groove/memory/:
5
+ // - project-constraints.md Discovered project rules, do-not-touch, required patterns
6
+ // - handoff-chain/<role>.md Cumulative rotation briefs (newest first, last 10 kept)
7
+ // - agent-discoveries.jsonl Error→fix pairs (only successes stored)
8
+ // - agent-specializations.json Per-agent and per-project-role quality profiles
9
+ //
10
+ // Read by the introducer on every spawn so agent #50 knows what agent #1 learned.
11
+
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync, statSync } from 'fs';
13
+ import { resolve } from 'path';
14
+ import { createHash } from 'crypto';
15
+
16
+ const MAX_CONSTRAINTS = 50;
17
+ const MAX_HANDOFF_ROTATIONS = 10;
18
+ const MAX_DISCOVERIES = 1000;
19
+ const HANDOFF_BRIEF_MAX_CHARS = 4000;
20
+
21
+ function hashText(text) {
22
+ return createHash('sha1').update(text.trim().toLowerCase()).digest('hex').slice(0, 12);
23
+ }
24
+
25
+ function safeName(role) {
26
+ return (role || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40);
27
+ }
28
+
29
+ function truncate(text, max) {
30
+ if (!text || text.length <= max) return text || '';
31
+ return text.slice(0, max - 3) + '...';
32
+ }
33
+
34
+ export class MemoryStore {
35
+ constructor(grooveDir) {
36
+ this.memDir = resolve(grooveDir, 'memory');
37
+ this.constraintsPath = resolve(this.memDir, 'project-constraints.md');
38
+ this.handoffDir = resolve(this.memDir, 'handoff-chain');
39
+ this.discoveriesPath = resolve(this.memDir, 'agent-discoveries.jsonl');
40
+ this.specializationsPath = resolve(this.memDir, 'agent-specializations.json');
41
+ this._ensureDirs();
42
+ }
43
+
44
+ _ensureDirs() {
45
+ try {
46
+ mkdirSync(this.memDir, { recursive: true });
47
+ mkdirSync(this.handoffDir, { recursive: true });
48
+ } catch { /* best-effort */ }
49
+ }
50
+
51
+ // --- Project Constraints ---
52
+
53
+ listConstraints() {
54
+ if (!existsSync(this.constraintsPath)) return [];
55
+ try {
56
+ const content = readFileSync(this.constraintsPath, 'utf8');
57
+ const constraints = [];
58
+ const blocks = content.split(/\n(?=- )/); // each constraint starts with "- "
59
+ for (const block of blocks) {
60
+ const m = block.match(/^- \[([a-f0-9]+)\] \*([^*]+)\* (.+)$/s);
61
+ if (m) {
62
+ constraints.push({
63
+ hash: m[1],
64
+ category: m[2].trim(),
65
+ text: m[3].trim(),
66
+ });
67
+ }
68
+ }
69
+ return constraints;
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ addConstraint({ text, category = 'general' }) {
76
+ if (!text || typeof text !== 'string') return { added: false, error: 'text required' };
77
+ const trimmed = text.trim();
78
+ if (trimmed.length < 3) return { added: false, error: 'text too short' };
79
+ if (trimmed.length > 500) return { added: false, error: 'text too long (max 500 chars)' };
80
+
81
+ const hash = hashText(trimmed);
82
+ const existing = this.listConstraints();
83
+ if (existing.some((c) => c.hash === hash)) {
84
+ return { added: false, hash, reason: 'duplicate' };
85
+ }
86
+
87
+ existing.push({ hash, category, text: trimmed });
88
+ // Keep most recent MAX_CONSTRAINTS
89
+ const pruned = existing.slice(-MAX_CONSTRAINTS);
90
+ this._writeConstraints(pruned);
91
+ return { added: true, hash };
92
+ }
93
+
94
+ removeConstraint(hash) {
95
+ const existing = this.listConstraints();
96
+ const filtered = existing.filter((c) => c.hash !== hash);
97
+ if (filtered.length === existing.length) return false;
98
+ this._writeConstraints(filtered);
99
+ return true;
100
+ }
101
+
102
+ _writeConstraints(constraints) {
103
+ const lines = [
104
+ '# Project Constraints',
105
+ `*Auto-managed by GROOVE memory (Layer 7). Last updated: ${new Date().toISOString()}*`,
106
+ '',
107
+ ];
108
+ for (const c of constraints) {
109
+ lines.push(`- [${c.hash}] *${c.category}* ${c.text}`);
110
+ }
111
+ lines.push('');
112
+ try {
113
+ writeFileSync(this.constraintsPath, lines.join('\n'));
114
+ } catch { /* best-effort */ }
115
+ }
116
+
117
+ getConstraintsMarkdown(maxChars = 4000) {
118
+ const constraints = this.listConstraints();
119
+ if (constraints.length === 0) return '';
120
+ const byCategory = {};
121
+ for (const c of constraints) {
122
+ byCategory[c.category] = byCategory[c.category] || [];
123
+ byCategory[c.category].push(c.text);
124
+ }
125
+ const lines = [];
126
+ for (const [cat, items] of Object.entries(byCategory)) {
127
+ lines.push(`**${cat}:**`);
128
+ for (const item of items) lines.push(`- ${item}`);
129
+ lines.push('');
130
+ }
131
+ return truncate(lines.join('\n').trim(), maxChars);
132
+ }
133
+
134
+ // --- Handoff Chain ---
135
+
136
+ _chainPath(role) {
137
+ return resolve(this.handoffDir, `${safeName(role)}.md`);
138
+ }
139
+
140
+ getHandoffChain(role) {
141
+ const path = this._chainPath(role);
142
+ if (!existsSync(path)) return [];
143
+ try {
144
+ const content = readFileSync(path, 'utf8');
145
+ const entries = [];
146
+ // Parse entries — each starts with "## Rotation N —"
147
+ // Body includes the header + all content up to (but not including) the
148
+ // trailing --- separator and the next entry.
149
+ const blocks = content.split(/\n(?=## Rotation )/);
150
+ for (const block of blocks) {
151
+ const headerMatch = block.match(/^## Rotation (\d+) —/);
152
+ if (!headerMatch) continue;
153
+ const body = block.replace(/\n---\s*$/, '').trim();
154
+ entries.push({
155
+ rotationN: parseInt(headerMatch[1], 10),
156
+ body,
157
+ });
158
+ }
159
+ return entries;
160
+ } catch {
161
+ return [];
162
+ }
163
+ }
164
+
165
+ appendHandoffBrief(role, entry) {
166
+ if (!role || !entry) return false;
167
+ const chain = this.getHandoffChain(role);
168
+ const nextN = (chain[0]?.rotationN || 0) + 1;
169
+
170
+ const block = [
171
+ `## Rotation ${nextN} — ${entry.timestamp || new Date().toISOString()} (${entry.agentId || '?'} → ${entry.newAgentId || '?'})`,
172
+ `**Reason:** ${entry.reason || 'unknown'}`,
173
+ entry.oldTokens != null ? `**Tokens carried:** ${entry.oldTokens.toLocaleString()}` : '',
174
+ entry.contextUsage != null ? `**Context at rotation:** ${Math.round(entry.contextUsage * 100)}%` : '',
175
+ '',
176
+ '**Brief summary:**',
177
+ truncate(entry.brief || '(no brief)', HANDOFF_BRIEF_MAX_CHARS),
178
+ '',
179
+ ].filter(Boolean).join('\n');
180
+
181
+ // Prepend new entry (newest first), keep last N
182
+ const newChain = [{ rotationN: nextN, body: block }, ...chain].slice(0, MAX_HANDOFF_ROTATIONS);
183
+
184
+ const lines = [
185
+ `# ${role[0].toUpperCase() + role.slice(1)} Handoff Chain`,
186
+ `*Cumulative rotation briefs. Newest first. Last ${MAX_HANDOFF_ROTATIONS} kept.*`,
187
+ '',
188
+ ];
189
+ for (const e of newChain) {
190
+ lines.push(e.body || '');
191
+ lines.push('---');
192
+ lines.push('');
193
+ }
194
+
195
+ try {
196
+ writeFileSync(this._chainPath(role), lines.join('\n'));
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ getRecentHandoffMarkdown(role, count = 3, maxChars = 4000) {
204
+ const chain = this.getHandoffChain(role);
205
+ if (chain.length === 0) return '';
206
+ const recent = chain.slice(0, count);
207
+ const out = recent.map((e) => e.body || '').join('\n\n---\n\n');
208
+ return truncate(out, maxChars);
209
+ }
210
+
211
+ listHandoffRoles() {
212
+ if (!existsSync(this.handoffDir)) return [];
213
+ try {
214
+ return readdirSync(this.handoffDir)
215
+ .filter((f) => f.endsWith('.md'))
216
+ .map((f) => f.replace(/\.md$/, ''));
217
+ } catch {
218
+ return [];
219
+ }
220
+ }
221
+
222
+ // --- Discoveries (error → fix pairs) ---
223
+
224
+ addDiscovery({ agentId, role, trigger, fix, outcome = 'success' }) {
225
+ if (!trigger || !fix) return { added: false, error: 'trigger and fix required' };
226
+ if (outcome !== 'success') return { added: false, reason: 'only successes stored' };
227
+
228
+ const entry = {
229
+ ts: new Date().toISOString(),
230
+ agentId: agentId || null,
231
+ role: role || 'unknown',
232
+ trigger: truncate(String(trigger).trim(), 300),
233
+ fix: truncate(String(fix).trim(), 500),
234
+ outcome,
235
+ };
236
+
237
+ // Dedup: same trigger+fix = skip
238
+ const existing = this.listDiscoveries({ limit: 200 });
239
+ const key = hashText(entry.trigger + '||' + entry.fix);
240
+ if (existing.some((d) => hashText(d.trigger + '||' + d.fix) === key)) {
241
+ return { added: false, reason: 'duplicate' };
242
+ }
243
+
244
+ try {
245
+ appendFileSync(this.discoveriesPath, JSON.stringify(entry) + '\n');
246
+ this._pruneDiscoveries();
247
+ return { added: true };
248
+ } catch (err) {
249
+ return { added: false, error: err.message };
250
+ }
251
+ }
252
+
253
+ listDiscoveries({ role, limit = 100 } = {}) {
254
+ if (!existsSync(this.discoveriesPath)) return [];
255
+ try {
256
+ const lines = readFileSync(this.discoveriesPath, 'utf8').split('\n').filter(Boolean);
257
+ const entries = [];
258
+ for (const line of lines) {
259
+ try {
260
+ const e = JSON.parse(line);
261
+ if (!role || e.role === role) entries.push(e);
262
+ } catch { /* skip malformed */ }
263
+ }
264
+ return entries.slice(-limit).reverse(); // newest first
265
+ } catch {
266
+ return [];
267
+ }
268
+ }
269
+
270
+ _pruneDiscoveries() {
271
+ if (!existsSync(this.discoveriesPath)) return;
272
+ try {
273
+ const stat = statSync(this.discoveriesPath);
274
+ // Only prune on larger files to avoid thrashing
275
+ if (stat.size < 50_000) return;
276
+ const lines = readFileSync(this.discoveriesPath, 'utf8').split('\n').filter(Boolean);
277
+ if (lines.length <= MAX_DISCOVERIES) return;
278
+ const kept = lines.slice(-MAX_DISCOVERIES);
279
+ writeFileSync(this.discoveriesPath, kept.join('\n') + '\n');
280
+ } catch { /* best-effort */ }
281
+ }
282
+
283
+ getDiscoveriesMarkdown(role, limit = 20, maxChars = 4000) {
284
+ const entries = this.listDiscoveries({ role, limit });
285
+ if (entries.length === 0) return '';
286
+ const lines = entries.map((d) => `- When \`${d.trigger}\` → fix: ${d.fix}`);
287
+ return truncate(lines.join('\n'), maxChars);
288
+ }
289
+
290
+ // --- Specializations ---
291
+
292
+ _loadSpecializations() {
293
+ if (!existsSync(this.specializationsPath)) {
294
+ return { perAgent: {}, perProjectRole: {} };
295
+ }
296
+ try {
297
+ const data = JSON.parse(readFileSync(this.specializationsPath, 'utf8'));
298
+ return {
299
+ perAgent: data.perAgent || {},
300
+ perProjectRole: data.perProjectRole || {},
301
+ };
302
+ } catch {
303
+ return { perAgent: {}, perProjectRole: {} };
304
+ }
305
+ }
306
+
307
+ _saveSpecializations(data) {
308
+ try {
309
+ writeFileSync(this.specializationsPath, JSON.stringify(data, null, 2));
310
+ } catch { /* best-effort */ }
311
+ }
312
+
313
+ updateSpecialization(agentId, { role, qualityScore, filesTouched, signals, threshold }) {
314
+ if (!agentId) return false;
315
+ const data = this._loadSpecializations();
316
+
317
+ const agentEntry = data.perAgent[agentId] || {
318
+ role: role || 'unknown',
319
+ sessionCount: 0,
320
+ avgQualityScore: 0,
321
+ qualityTotal: 0,
322
+ fileTouches: {},
323
+ signatureErrors: [],
324
+ };
325
+ agentEntry.sessionCount += 1;
326
+ if (typeof qualityScore === 'number') {
327
+ agentEntry.qualityTotal += qualityScore;
328
+ agentEntry.avgQualityScore = Math.round(agentEntry.qualityTotal / agentEntry.sessionCount);
329
+ }
330
+ if (Array.isArray(filesTouched)) {
331
+ for (const f of filesTouched) {
332
+ agentEntry.fileTouches[f] = (agentEntry.fileTouches[f] || 0) + 1;
333
+ }
334
+ }
335
+ if (role) agentEntry.role = role;
336
+ if (threshold != null) agentEntry.preferredThreshold = threshold;
337
+ data.perAgent[agentId] = agentEntry;
338
+
339
+ if (role) {
340
+ const roleEntry = data.perProjectRole[role] || {
341
+ sessionCount: 0,
342
+ avgQualityScore: 0,
343
+ qualityTotal: 0,
344
+ topFileChurn: {},
345
+ };
346
+ roleEntry.sessionCount += 1;
347
+ if (typeof qualityScore === 'number') {
348
+ roleEntry.qualityTotal += qualityScore;
349
+ roleEntry.avgQualityScore = Math.round(roleEntry.qualityTotal / roleEntry.sessionCount);
350
+ }
351
+ if (Array.isArray(filesTouched)) {
352
+ for (const f of filesTouched) {
353
+ roleEntry.topFileChurn[f] = (roleEntry.topFileChurn[f] || 0) + 1;
354
+ }
355
+ }
356
+ data.perProjectRole[role] = roleEntry;
357
+ }
358
+
359
+ this._saveSpecializations(data);
360
+ return true;
361
+ }
362
+
363
+ getSpecialization(agentId) {
364
+ return this._loadSpecializations().perAgent[agentId] || null;
365
+ }
366
+
367
+ getAllSpecializations() {
368
+ return this._loadSpecializations();
369
+ }
370
+ }
@@ -54,7 +54,7 @@ Review: Is this within scope? Conflicts with other agents? Aligns with project?
54
54
  Respond in ONE line: APPROVED: <reason> or REJECTED: <reason>`;
55
55
 
56
56
  try {
57
- const result = await this.daemon.journalist.callHeadless(prompt);
57
+ const result = await this.daemon.journalist.callHeadless(prompt, { trackAs: '__pm__' });
58
58
  const text = (result || '').trim();
59
59
  const approved = !text.toUpperCase().startsWith('REJECTED');
60
60
  const reason = text.replace(/^(APPROVED|REJECTED):?\s*/i, '').trim();