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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.11.0",
3
+ "version": "0.27.0",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -103,9 +103,9 @@ export class AdaptiveThresholds {
103
103
  const errorCount = signals.errorCount || 0;
104
104
  score -= errorCount * 5;
105
105
 
106
- // Repetitions: each detected repetition costs 8 points (agent repeating itself)
106
+ // Repetitions: 3+ Write/Edit to the same file in a sliding window
107
107
  const repetitions = signals.repetitions || 0;
108
- score -= repetitions * 8;
108
+ score -= Math.min(repetitions * 6, 30);
109
109
 
110
110
  // Out-of-scope access: each violation costs 10 points
111
111
  const scopeViolations = signals.scopeViolations || 0;
@@ -116,12 +116,12 @@ export class AdaptiveThresholds {
116
116
  const toolFailures = signals.toolFailures || 0;
117
117
  if (toolCalls > 0) {
118
118
  const successRate = (toolCalls - toolFailures) / toolCalls;
119
- score += Math.round((successRate - 0.8) * 20); // Bonus/penalty around 80%
119
+ score += Math.round((successRate - 0.8) * 20);
120
120
  }
121
121
 
122
- // File churn: editing same file 3+ times signals circular refactoring
122
+ // File churn: same file written 5+ times genuine circular refactoring
123
123
  const fileChurn = signals.fileChurn || 0;
124
- score -= fileChurn * 12;
124
+ score -= fileChurn * 10;
125
125
 
126
126
  // Error trend: increasing errors in second half = degradation
127
127
  const errorTrend = signals.errorTrend || 0;
@@ -162,46 +162,34 @@ export class AdaptiveThresholds {
162
162
  errorTrend: 0, // errors increasing in recent window → degradation signal
163
163
  };
164
164
 
165
- const recentTools = [];
166
165
  const writtenFiles = new Set();
167
- const fileWriteCounts = {}; // track how many times each file is written
168
- const errorTimestamps = [];
166
+ const fileWriteCounts = {};
167
+ const writeEditOps = [];
169
168
 
170
169
  for (const entry of entries) {
171
170
  if (entry.type === 'error') {
172
171
  signals.errorCount++;
173
- errorTimestamps.push(entry.timestamp || Date.now());
174
172
  }
175
173
 
176
174
  if (entry.type === 'tool') {
177
175
  signals.toolCalls++;
178
176
 
179
- // Track file writes and churn
180
177
  if (entry.tool === 'Write' || entry.tool === 'Edit') {
181
178
  if (entry.input) {
182
179
  writtenFiles.add(entry.input);
183
180
  fileWriteCounts[entry.input] = (fileWriteCounts[entry.input] || 0) + 1;
181
+ writeEditOps.push(entry.input);
184
182
  }
185
183
  }
186
184
 
187
- // Track tool failures — errors that follow tool calls indicate failure
188
- // (stream-json emits tool_use then tool_result with is_error)
189
- if (entry.type === 'tool' && entry.isError) {
185
+ if (entry.isError) {
190
186
  signals.toolFailures++;
191
187
  }
192
188
 
193
- // Detect repetitions: same tool+input within last 5 calls
194
- const key = `${entry.tool}:${entry.input}`;
195
- if (recentTools.includes(key)) {
196
- signals.repetitions++;
197
- }
198
- recentTools.push(key);
199
- if (recentTools.length > 5) recentTools.shift();
200
-
201
- // Detect scope violations (simplified check)
189
+ // Scope violations: writes outside declared scope
202
190
  if (agentScope && agentScope.length > 0 && entry.input) {
203
- const file = entry.input;
204
191
  if (entry.tool === 'Write' || entry.tool === 'Edit') {
192
+ const file = entry.input;
205
193
  const inScope = agentScope.some((pattern) =>
206
194
  file.includes(pattern.replace('/**', '').replace('**/', ''))
207
195
  );
@@ -213,9 +201,19 @@ export class AdaptiveThresholds {
213
201
 
214
202
  signals.filesWritten = writtenFiles.size;
215
203
 
216
- // File churn: count files written 3+ times (circular refactoring signal)
204
+ // Repetitions: only count Write/Edit to the same file as circular behavior.
205
+ // Read/Grep/Glob revisits are normal investigation — not degradation.
206
+ // Require 3+ writes to the same file within a 15-op sliding window.
207
+ for (let i = 0; i < writeEditOps.length; i++) {
208
+ const windowStart = Math.max(0, i - 14);
209
+ const window = writeEditOps.slice(windowStart, i);
210
+ const count = window.filter((f) => f === writeEditOps[i]).length;
211
+ if (count >= 2) signals.repetitions++;
212
+ }
213
+
214
+ // File churn: files written 5+ times (circular refactoring, not normal iteration)
217
215
  for (const count of Object.values(fileWriteCounts)) {
218
- if (count >= 3) signals.fileChurn++;
216
+ if (count >= 5) signals.fileChurn++;
219
217
  }
220
218
 
221
219
  // Error trend: compare error rate in first half vs second half of session
@@ -117,6 +117,116 @@ export function createApi(app, daemon) {
117
117
  res.json(daemon.locks.getAll());
118
118
  });
119
119
 
120
+ // Coordination protocol — agents declare intent on shared resources
121
+ // (npm install, server restart, package.json edit) to prevent races.
122
+ // Returns 423 Locked if another agent holds a conflicting resource.
123
+ app.post('/api/coordination/declare', (req, res) => {
124
+ const { agentId, operation, resources, ttlMs } = req.body || {};
125
+ if (!agentId || !operation || !Array.isArray(resources) || resources.length === 0) {
126
+ return res.status(400).json({ error: 'agentId, operation, and resources[] required' });
127
+ }
128
+ const result = daemon.locks.declareOperation(agentId, operation, resources, ttlMs);
129
+ if (result.conflict) {
130
+ daemon.audit.log('coordination.conflict', { agentId, operation, resource: result.resource, owner: result.owner });
131
+ return res.status(423).json(result);
132
+ }
133
+ daemon.audit.log('coordination.declared', { agentId, operation, resources });
134
+ daemon.broadcast({ type: 'coordination:declared', agentId, operation, resources });
135
+ res.json({ declared: true, operation, resources });
136
+ });
137
+
138
+ app.post('/api/coordination/complete', (req, res) => {
139
+ const { agentId } = req.body || {};
140
+ if (!agentId) return res.status(400).json({ error: 'agentId required' });
141
+ const removed = daemon.locks.completeOperation(agentId);
142
+ daemon.audit.log('coordination.completed', { agentId });
143
+ daemon.broadcast({ type: 'coordination:completed', agentId });
144
+ res.json({ completed: removed });
145
+ });
146
+
147
+ app.get('/api/coordination', (req, res) => {
148
+ res.json({ operations: daemon.locks.getOperations() });
149
+ });
150
+
151
+ // --- Persistent Memory (Layer 7) ---
152
+ // Constraints: project rules discovered by agents / set by user
153
+ app.get('/api/memory/constraints', (req, res) => {
154
+ res.json({ constraints: daemon.memory.listConstraints() });
155
+ });
156
+
157
+ app.post('/api/memory/constraints', (req, res) => {
158
+ const { text, category } = req.body || {};
159
+ const result = daemon.memory.addConstraint({ text, category });
160
+ if (!result.added && result.error) {
161
+ return res.status(400).json(result);
162
+ }
163
+ if (result.added) {
164
+ daemon.audit.log('memory.constraint.added', { hash: result.hash, category });
165
+ daemon.broadcast({ type: 'memory:constraint:added', hash: result.hash });
166
+ }
167
+ res.json(result);
168
+ });
169
+
170
+ app.delete('/api/memory/constraints/:hash', (req, res) => {
171
+ const removed = daemon.memory.removeConstraint(req.params.hash);
172
+ if (removed) {
173
+ daemon.audit.log('memory.constraint.removed', { hash: req.params.hash });
174
+ daemon.broadcast({ type: 'memory:constraint:removed', hash: req.params.hash });
175
+ }
176
+ res.json({ removed });
177
+ });
178
+
179
+ // Handoff chains (per role)
180
+ app.get('/api/memory/handoff-chain/:role', (req, res) => {
181
+ res.json({
182
+ role: req.params.role,
183
+ entries: daemon.memory.getHandoffChain(req.params.role),
184
+ });
185
+ });
186
+
187
+ app.get('/api/memory/handoff-chain/:role/recent', (req, res) => {
188
+ const count = Math.min(parseInt(req.query.count) || 3, 10);
189
+ res.json({
190
+ role: req.params.role,
191
+ markdown: daemon.memory.getRecentHandoffMarkdown(req.params.role, count, 10_000),
192
+ });
193
+ });
194
+
195
+ app.get('/api/memory/handoff-chain', (req, res) => {
196
+ res.json({ roles: daemon.memory.listHandoffRoles() });
197
+ });
198
+
199
+ // Discoveries (error → fix pairs)
200
+ app.get('/api/memory/discoveries', (req, res) => {
201
+ const role = req.query.role;
202
+ const limit = Math.min(parseInt(req.query.limit) || 100, 500);
203
+ res.json({ discoveries: daemon.memory.listDiscoveries({ role, limit }) });
204
+ });
205
+
206
+ app.post('/api/memory/discoveries', (req, res) => {
207
+ const { agentId, role, trigger, fix, outcome } = req.body || {};
208
+ const result = daemon.memory.addDiscovery({ agentId, role, trigger, fix, outcome });
209
+ if (!result.added && result.error) {
210
+ return res.status(400).json(result);
211
+ }
212
+ if (result.added) {
213
+ daemon.audit.log('memory.discovery.added', { agentId, role });
214
+ daemon.broadcast({ type: 'memory:discovery:added', agentId, role });
215
+ }
216
+ res.json(result);
217
+ });
218
+
219
+ // Specializations (per-agent + per-role quality profiles)
220
+ app.get('/api/memory/specializations', (req, res) => {
221
+ res.json(daemon.memory.getAllSpecializations());
222
+ });
223
+
224
+ app.get('/api/memory/specializations/:agentId', (req, res) => {
225
+ const spec = daemon.memory.getSpecialization(req.params.agentId);
226
+ if (!spec) return res.status(404).json({ error: 'No specialization data for this agent' });
227
+ res.json(spec);
228
+ });
229
+
120
230
  // Token usage
121
231
  app.get('/api/tokens', (req, res) => {
122
232
  res.json(daemon.tokens.getAll());
@@ -375,6 +485,14 @@ export function createApi(app, daemon) {
375
485
  res.json(rec);
376
486
  });
377
487
 
488
+ // Downshift suggestion — NEVER auto-applied. User must accept via UI.
489
+ // Returns null (204) when classifier has no strong suggestion.
490
+ app.get('/api/agents/:id/routing/suggestion', (req, res) => {
491
+ const suggestion = daemon.router.getSuggestion(req.params.id);
492
+ if (!suggestion) return res.status(204).send();
493
+ res.json(suggestion);
494
+ });
495
+
378
496
  // Daemon status
379
497
  app.get('/api/status', (req, res) => {
380
498
  res.json({
@@ -459,6 +577,68 @@ export function createApi(app, daemon) {
459
577
  res.json(daemon.tokens.getSummary());
460
578
  });
461
579
 
580
+ // Per-team token burn ranked by total. Answers "which team burned the most?"
581
+ app.get('/api/tokens/by-team', (req, res) => {
582
+ const agents = daemon.registry.getAll();
583
+ const usage = daemon.tokens.getAll();
584
+ const teams = daemon.teams.list();
585
+ const unassignedId = '__unassigned__';
586
+
587
+ const perTeam = new Map();
588
+ for (const t of teams) {
589
+ perTeam.set(t.id, {
590
+ teamId: t.id,
591
+ teamName: t.name,
592
+ isDefault: !!t.isDefault,
593
+ agentCount: 0,
594
+ totalTokens: 0,
595
+ totalCostUsd: 0,
596
+ avgTokensPerAgent: 0,
597
+ });
598
+ }
599
+ perTeam.set(unassignedId, {
600
+ teamId: unassignedId,
601
+ teamName: '(unassigned)',
602
+ isDefault: false,
603
+ agentCount: 0,
604
+ totalTokens: 0,
605
+ totalCostUsd: 0,
606
+ avgTokensPerAgent: 0,
607
+ });
608
+
609
+ for (const agent of agents) {
610
+ const bucket = perTeam.get(agent.teamId) || perTeam.get(unassignedId);
611
+ const u = usage[agent.id];
612
+ if (!u) continue;
613
+ bucket.agentCount += 1;
614
+ bucket.totalTokens += u.total || 0;
615
+ bucket.totalCostUsd += u.totalCostUsd || 0;
616
+ }
617
+
618
+ const result = [...perTeam.values()]
619
+ .map((t) => ({
620
+ ...t,
621
+ avgTokensPerAgent: t.agentCount > 0 ? Math.round(t.totalTokens / t.agentCount) : 0,
622
+ }))
623
+ .filter((t) => t.agentCount > 0 || t.isDefault)
624
+ .sort((a, b) => b.totalTokens - a.totalTokens);
625
+
626
+ res.json({ teams: result });
627
+ });
628
+
629
+ // Stop an agent's current work without killing the agent
630
+ app.post('/api/agents/:id/stop', async (req, res) => {
631
+ try {
632
+ const agent = daemon.registry.get(req.params.id);
633
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
634
+ await daemon.processes.stop(req.params.id);
635
+ daemon.audit.log('agent.stop', { id: req.params.id, name: agent.name });
636
+ res.json({ id: req.params.id, status: 'stopped' });
637
+ } catch (err) {
638
+ res.status(500).json({ error: err.message });
639
+ }
640
+ });
641
+
462
642
  // Rotate an agent
463
643
  app.post('/api/agents/:id/rotate', async (req, res) => {
464
644
  try {
@@ -542,7 +722,7 @@ export function createApi(app, daemon) {
542
722
  '\nAnswer concisely based on the agent context above.',
543
723
  ].filter(Boolean).join('\n');
544
724
 
545
- const response = await daemon.journalist.callHeadless(prompt);
725
+ const response = await daemon.journalist.callHeadless(prompt, { trackAs: '__agent_qa__' });
546
726
  res.json({ response, agentId: agent.id, agentName: agent.name });
547
727
  } catch (err) {
548
728
  res.status(400).json({ error: err.message });
@@ -721,7 +901,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
721
901
 
722
902
  // Slow path: CLI fallback for subscription auth (~10s)
723
903
  const fullPrompt = `${PLAN_SYSTEM}\n\n${prompt}`;
724
- const response = await daemon.journalist.callHeadless(fullPrompt);
904
+ const response = await daemon.journalist.callHeadless(fullPrompt, { trackAs: '__planner__' });
725
905
  res.json({ response, mode: 'cli' });
726
906
  } catch (err) {
727
907
  res.status(500).json({ error: err.message });
@@ -1025,6 +1205,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
1025
1205
  }
1026
1206
  });
1027
1207
 
1208
+ app.post('/api/integrations/google-workspace/oauth/start', (req, res) => {
1209
+ try {
1210
+ const { integrationIds } = req.body || {};
1211
+ if (!integrationIds?.length) return res.status(400).json({ error: 'integrationIds required' });
1212
+ const url = daemon.integrations.getGoogleWorkspaceOAuthUrl(integrationIds);
1213
+ res.json({ url });
1214
+ } catch (err) {
1215
+ res.status(400).json({ error: err.message });
1216
+ }
1217
+ });
1218
+
1028
1219
  // Parameterized :id routes (after specific routes above)
1029
1220
 
1030
1221
  app.post('/api/integrations/:id/authenticate', (req, res) => {
@@ -1092,6 +1283,134 @@ Keep responses concise. Help them think, don't lecture them about the system the
1092
1283
  }
1093
1284
  });
1094
1285
 
1286
+ // --- Integration Execution (provider-agnostic) ---
1287
+
1288
+ const _execRates = new Map();
1289
+ const EXEC_RATE_LIMIT = 30;
1290
+ const EXEC_RATE_WINDOW = 60_000;
1291
+
1292
+ app.post('/api/integrations/:id/exec', async (req, res) => {
1293
+ try {
1294
+ const { tool, params, approvalId, agent: agentId } = req.body || {};
1295
+ if (!tool || typeof tool !== 'string') {
1296
+ return res.status(400).json({ error: 'tool (string) is required' });
1297
+ }
1298
+ if (params !== undefined && (typeof params !== 'object' || Array.isArray(params))) {
1299
+ return res.status(400).json({ error: 'params must be an object' });
1300
+ }
1301
+ const integrationId = req.params.id;
1302
+ if (!daemon.integrations._isInstalled(integrationId)) {
1303
+ return res.status(400).json({ error: 'Integration not installed' });
1304
+ }
1305
+
1306
+ // Rate limiting — sliding window per integration
1307
+ const now = Date.now();
1308
+ let window = _execRates.get(integrationId) || [];
1309
+ window = window.filter((t) => now - t < EXEC_RATE_WINDOW);
1310
+ if (window.length >= EXEC_RATE_LIMIT) {
1311
+ daemon.audit.log('integration.exec.rate_limited', { integrationId, tool, agentId });
1312
+ return res.status(429).json({ error: `Rate limit exceeded (${EXEC_RATE_LIMIT}/min) for ${integrationId}` });
1313
+ }
1314
+ window.push(now);
1315
+ _execRates.set(integrationId, window);
1316
+
1317
+ // Approval gate — dangerous tools require human approval (unless agent is set to auto)
1318
+ const entry = daemon.integrations.registry.find((s) => s.id === integrationId);
1319
+ const callingAgent = agentId ? daemon.registry.get(agentId) : null;
1320
+ const autoApprove = callingAgent?.integrationApproval === 'auto';
1321
+ if (entry?.requiresApproval?.includes(tool) && !autoApprove) {
1322
+ if (approvalId) {
1323
+ const approval = daemon.supervisor.getApproval(approvalId);
1324
+ if (!approval) return res.status(404).json({ error: 'Approval not found' });
1325
+ if (approval.status === 'rejected') {
1326
+ return res.status(403).json({ error: 'Approval rejected', reason: approval.reason });
1327
+ }
1328
+ if (approval.status !== 'approved') {
1329
+ return res.status(202).json({ requiresApproval: true, approvalId, status: 'pending', message: 'Waiting for human approval' });
1330
+ }
1331
+ } else {
1332
+ const paramsSummary = params ? JSON.stringify(params).slice(0, 500) : '{}';
1333
+ const approval = daemon.supervisor.requestApproval(agentId || null, {
1334
+ type: 'integration_exec',
1335
+ integrationId,
1336
+ tool,
1337
+ params: paramsSummary,
1338
+ description: `${entry.name}: ${tool}`,
1339
+ });
1340
+ daemon.audit.log('integration.exec.blocked', { integrationId, tool, approvalId: approval.id, agentId });
1341
+ return res.status(202).json({
1342
+ requiresApproval: true,
1343
+ approvalId: approval.id,
1344
+ message: `Tool "${tool}" requires approval. Retry with this approvalId once approved.`,
1345
+ });
1346
+ }
1347
+ }
1348
+
1349
+ const result = await daemon.mcpManager.execTool(integrationId, tool, params || {});
1350
+ daemon.audit.log('integration.exec', { integrationId, tool, params: params ? JSON.stringify(params).slice(0, 200) : '{}', agentId });
1351
+ res.json({ result });
1352
+ } catch (err) {
1353
+ res.status(400).json({ error: err.message });
1354
+ }
1355
+ });
1356
+
1357
+ app.get('/api/integrations/:id/tools', async (req, res) => {
1358
+ try {
1359
+ if (!daemon.integrations._isInstalled(req.params.id)) {
1360
+ return res.status(400).json({ error: 'Integration not installed' });
1361
+ }
1362
+ const tools = await daemon.mcpManager.listTools(req.params.id);
1363
+ res.json({ tools });
1364
+ } catch (err) {
1365
+ res.status(400).json({ error: err.message });
1366
+ }
1367
+ });
1368
+
1369
+ // --- Google Drive Upload (file → native Google Workspace format) ---
1370
+
1371
+ app.post('/api/integrations/google-drive/upload', async (req, res) => {
1372
+ try {
1373
+ const { filePath, name, folderId, convert, approvalId, agent: agentId } = req.body || {};
1374
+ if (!filePath || typeof filePath !== 'string') {
1375
+ return res.status(400).json({ error: 'filePath (string) is required' });
1376
+ }
1377
+
1378
+ // Approval gate (unless agent is set to auto)
1379
+ const uploadAgent = agentId ? daemon.registry.get(agentId) : null;
1380
+ const autoApproveUpload = uploadAgent?.integrationApproval === 'auto';
1381
+ if (!autoApproveUpload) {
1382
+ if (approvalId) {
1383
+ const approval = daemon.supervisor.getApproval(approvalId);
1384
+ if (!approval) return res.status(404).json({ error: 'Approval not found' });
1385
+ if (approval.status === 'rejected') return res.status(403).json({ error: 'Approval rejected', reason: approval.reason });
1386
+ if (approval.status !== 'approved') return res.status(202).json({ requiresApproval: true, approvalId, status: 'pending' });
1387
+ } else {
1388
+ const approval = daemon.supervisor.requestApproval(agentId || null, {
1389
+ type: 'google_drive_upload',
1390
+ filePath,
1391
+ name: name || filePath.split('/').pop(),
1392
+ description: `Upload to Google Drive: ${name || filePath.split('/').pop()}`,
1393
+ });
1394
+ daemon.audit.log('integration.upload.blocked', { filePath, approvalId: approval.id, agentId });
1395
+ return res.status(202).json({
1396
+ requiresApproval: true,
1397
+ approvalId: approval.id,
1398
+ message: `Upload requires approval. Retry with this approvalId once approved.`,
1399
+ });
1400
+ }
1401
+ }
1402
+
1403
+ const result = await daemon.integrations.uploadToGoogleDrive(filePath, {
1404
+ name, folderId, convert: convert !== false,
1405
+ });
1406
+
1407
+ daemon.audit.log('integration.upload', { filePath, driveFileId: result.id, name: result.name, agentId });
1408
+ res.json(result);
1409
+ } catch (err) {
1410
+ res.status(400).json({ error: err.message });
1411
+ }
1412
+ });
1413
+
1095
1414
  // --- Agent Integrations (attach/detach) ---
1096
1415
 
1097
1416
  app.post('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
@@ -1939,8 +2258,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
1939
2258
  // Recalculate savings estimates
1940
2259
  const estimated = registryTokens + tokenSummary.savings.total;
1941
2260
  tokenSummary.savings.estimatedWithoutGroove = estimated;
1942
- tokenSummary.savings.percentage = estimated > 0
1943
- ? Math.round((tokenSummary.savings.total / estimated) * 100) : 0;
2261
+ const rawPct = estimated > 0 ? (tokenSummary.savings.total / estimated) * 100 : 0;
2262
+ tokenSummary.savings.percentage = rawPct > 0 && rawPct < 1
2263
+ ? Math.round(rawPct * 10) / 10 : Math.round(rawPct);
1944
2264
  }
1945
2265
  const rotationStats = daemon.rotator.getStats();
1946
2266
  const rotationHistory = daemon.rotator.getHistory();
@@ -1964,27 +2284,31 @@ Keep responses concise. Help them think, don't lecture them about the system the
1964
2284
  // Per-agent enriched data with quality signals
1965
2285
  const agentBreakdown = agents.map((a) => {
1966
2286
  const tokenData = daemon.tokens.getAgent(a.id);
1967
- const agentCacheTotal = (tokenData.cacheReadTokens || 0) + (tokenData.cacheCreationTokens || 0) + (tokenData.inputTokens || 0);
2287
+ // Cache rate denominator: reads + creation (cacheable), excludes fresh inputTokens
2288
+ const agentCacheable = (tokenData.cacheReadTokens || 0) + (tokenData.cacheCreationTokens || 0);
1968
2289
 
1969
- // Quality signals from classifier + adaptive
1970
2290
  let quality = null;
1971
2291
  try {
1972
- const signals = daemon.adaptive.extractSignals ? daemon.adaptive.extractSignals(a.id) : null;
2292
+ const events = daemon.classifier.agentWindows[a.id] || [];
2293
+ const signals = events.length >= 6 ? daemon.adaptive.extractSignals(events, a.scope) : null;
2294
+ const score = signals ? daemon.adaptive.scoreSession(signals) : null;
1973
2295
  const classification = daemon.classifier.classify(a.id);
1974
- if (signals || classification) {
1975
- quality = {
1976
- score: signals?.score || null,
1977
- errorCount: signals?.errorCount || 0,
1978
- toolCalls: signals?.toolCalls || 0,
1979
- toolFailures: signals?.toolFailures || 0,
1980
- toolSuccessRate: signals?.toolCalls > 0 ? 1 - (signals.toolFailures / signals.toolCalls) : 1,
1981
- filesWritten: signals?.filesWritten || 0,
1982
- fileChurn: signals?.fileChurn || 0,
1983
- repetitions: signals?.repetitions || 0,
1984
- tier: classification?.tier || 'medium',
1985
- };
1986
- }
1987
- } catch { /* classifier/adaptive may not have data for this agent */ }
2296
+ const history = daemon.rotator.scoreHistory[a.id] || [];
2297
+ quality = {
2298
+ score,
2299
+ scoreHistory: history,
2300
+ errorCount: signals?.errorCount || 0,
2301
+ toolCalls: signals?.toolCalls || 0,
2302
+ toolFailures: signals?.toolFailures || 0,
2303
+ toolSuccessRate: signals?.toolCalls > 0 ? 1 - (signals.toolFailures / signals.toolCalls) : 1,
2304
+ filesWritten: signals?.filesWritten || 0,
2305
+ fileChurn: signals?.fileChurn || 0,
2306
+ repetitions: signals?.repetitions || 0,
2307
+ errorTrend: signals?.errorTrend || 0,
2308
+ tier: classification?.tier || classification || 'medium',
2309
+ eventCount: events.length,
2310
+ };
2311
+ } catch { /* classifier/adaptive may not have data yet */ }
1988
2312
 
1989
2313
  return {
1990
2314
  id: a.id,
@@ -2000,7 +2324,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2000
2324
  costSource: a.provider === 'claude-code' ? 'actual' : a.provider === 'ollama' ? 'local' : 'estimated',
2001
2325
  inputTokens: tokenData.inputTokens || 0,
2002
2326
  outputTokens: tokenData.outputTokens || 0,
2003
- cacheHitRate: agentCacheTotal > 0 ? Math.round(((tokenData.cacheReadTokens || 0) / agentCacheTotal) * 1000) / 1000 : 0,
2327
+ cacheHitRate: agentCacheable > 0 ? Math.round(((tokenData.cacheReadTokens || 0) / agentCacheable) * 1000) / 1000 : 0,
2004
2328
  contextUsage: a.contextUsage || 0,
2005
2329
  rotationThreshold: daemon.adaptive.getThreshold(a.provider, a.role),
2006
2330
  durationMs: a.durationMs || tokenData.totalDurationMs || 0,
@@ -35,17 +35,64 @@ const LIGHT_SIGNALS = [
35
35
  ];
36
36
 
37
37
  export class TaskClassifier {
38
- constructor() {
39
- this.windowSize = 50; // Look at last N events needs to be large enough
38
+ constructor(daemon = null) {
39
+ this.windowSize = 200; // Large enough for quality signal extraction across tool calls
40
40
  this.agentWindows = {}; // for degradation detection and adaptive scoring
41
+ this.daemon = daemon; // optional — enables broadcast of classification updates
42
+ this._lastBroadcastCount = {}; // per-agent throttle
41
43
  }
42
44
 
43
- // Add an event to the classification window
44
45
  addEvent(agentId, event) {
45
46
  if (!this.agentWindows[agentId]) this.agentWindows[agentId] = [];
46
- this.agentWindows[agentId].push(event);
47
- if (this.agentWindows[agentId].length > this.windowSize) {
48
- this.agentWindows[agentId].shift();
47
+ const window = this.agentWindows[agentId];
48
+
49
+ // Extract structured tool/error events from Claude Code content blocks.
50
+ // Claude's stream-json emits assistant messages with content arrays containing
51
+ // tool_use and tool_result blocks that drive quality signal extraction.
52
+ let extracted = false;
53
+ if (event.type === 'activity' && Array.isArray(event.data)) {
54
+ for (const block of event.data) {
55
+ if (block.type === 'tool_use') {
56
+ window.push({
57
+ type: 'tool',
58
+ tool: block.name,
59
+ input: block.input?.file_path || block.input?.path || block.input?.command || '',
60
+ isError: false,
61
+ timestamp: Date.now(),
62
+ });
63
+ extracted = true;
64
+ }
65
+ if (block.type === 'tool_result') {
66
+ if (block.is_error) {
67
+ window.push({ type: 'error', timestamp: Date.now() });
68
+ extracted = true;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // Only push the raw event if we didn't extract structured events from it —
75
+ // avoids double-counting and window bloat from activity wrappers.
76
+ if (!extracted) {
77
+ window.push({ ...event, timestamp: event.timestamp || Date.now() });
78
+ }
79
+ while (window.length > this.windowSize) window.shift();
80
+
81
+ // Broadcast classification updates periodically. Enables GUI to surface
82
+ // downshift suggestions ("agent's been doing light work — switch to Haiku?").
83
+ // Requires 40+ events before any broadcast; throttles to every 20 events.
84
+ if (this.daemon?.broadcast && window.length >= 40) {
85
+ const lastBroadcast = this._lastBroadcastCount[agentId] || 0;
86
+ if (window.length - lastBroadcast >= 20) {
87
+ this._lastBroadcastCount[agentId] = window.length;
88
+ const tier = this.classify(agentId);
89
+ this.daemon.broadcast({
90
+ type: 'classifier:update',
91
+ agentId,
92
+ tier,
93
+ eventCount: window.length,
94
+ });
95
+ }
49
96
  }
50
97
  }
51
98
 
@@ -15,6 +15,16 @@ const DEFAULT_CONFIG = {
15
15
  qcThreshold: 4,
16
16
  maxAgents: 10,
17
17
  defaultProvider: 'claude-code',
18
+ // Self-healing rotation triggers. Catch pathological agent behavior
19
+ // (stuck loops, runaway tool-call cycles) and auto-rotate with fresh
20
+ // context. Tokens carry forward; journalist generates handoff brief.
21
+ // Set autoRotate=false to disable and get broadcast-only notifications.
22
+ safety: {
23
+ autoRotate: true,
24
+ tokenCeilingPerAgent: 5_000_000,
25
+ velocityWindowSeconds: 300,
26
+ velocityTokenThreshold: 1_500_000,
27
+ },
18
28
  };
19
29
 
20
30
  export function isFirstRun(grooveDir) {
@@ -123,7 +133,10 @@ export function loadConfig(grooveDir) {
123
133
 
124
134
  try {
125
135
  const saved = JSON.parse(readFileSync(configPath, 'utf8'));
126
- return { ...DEFAULT_CONFIG, ...saved };
136
+ const merged = { ...DEFAULT_CONFIG, ...saved };
137
+ // Deep-merge safety subtree so partial user config doesn't drop defaults
138
+ merged.safety = { ...DEFAULT_CONFIG.safety, ...(saved.safety || {}) };
139
+ return merged;
127
140
  } catch {
128
141
  return { ...DEFAULT_CONFIG };
129
142
  }
@@ -638,7 +638,7 @@ export class GatewayManager {
638
638
  '\nSynthesize a concise answer based on the team\'s collective context.',
639
639
  ].join('\n');
640
640
 
641
- const response = await this.daemon.journalist.callHeadless(prompt);
641
+ const response = await this.daemon.journalist.callHeadless(prompt, { trackAs: '__gateway__' });
642
642
  return { text: `\ud83d\udcac Team ${result.team.name}:\n${truncate(response, 3000)}` };
643
643
  }
644
644
 
@@ -659,7 +659,7 @@ export class GatewayManager {
659
659
  '\nAnswer concisely based on the agent context above.',
660
660
  ].filter(Boolean).join('\n');
661
661
 
662
- const response = await this.daemon.journalist.callHeadless(prompt);
662
+ const response = await this.daemon.journalist.callHeadless(prompt, { trackAs: '__gateway__' });
663
663
  return { text: `\ud83d\udcac ${agent.name || agent.id}:\n${truncate(response, 3000)}` };
664
664
  }
665
665