groove-dev 0.26.32 → 0.26.35

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 (29) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/node_modules/@groove-dev/daemon/src/api.js +3 -0
  3. package/node_modules/@groove-dev/daemon/src/introducer.js +13 -0
  4. package/node_modules/@groove-dev/daemon/src/journalist.js +146 -28
  5. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -2
  6. package/node_modules/@groove-dev/daemon/test/journalist.test.js +3 -3
  7. package/node_modules/@groove-dev/gui/dist/assets/index-86xvrzfI.js +638 -0
  8. package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +1 -0
  9. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  10. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +11 -8
  11. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +511 -0
  12. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +3 -1
  13. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +26 -4
  14. package/package.json +1 -1
  15. package/packages/daemon/src/api.js +3 -0
  16. package/packages/daemon/src/introducer.js +13 -0
  17. package/packages/daemon/src/journalist.js +146 -28
  18. package/packages/daemon/src/providers/claude-code.js +4 -2
  19. package/packages/gui/dist/assets/index-86xvrzfI.js +638 -0
  20. package/packages/gui/dist/assets/index-CEFKgLGB.css +1 -0
  21. package/packages/gui/dist/index.html +2 -2
  22. package/packages/gui/src/components/dashboard/fleet-panel.jsx +11 -8
  23. package/packages/gui/src/components/marketplace/integration-wizard.jsx +511 -0
  24. package/packages/gui/src/views/dashboard.jsx +3 -1
  25. package/packages/gui/src/views/marketplace.jsx +26 -4
  26. package/node_modules/@groove-dev/gui/dist/assets/index-BnNZzcsd.css +0 -1
  27. package/node_modules/@groove-dev/gui/dist/assets/index-CwUZRfEx.js +0 -638
  28. package/packages/gui/dist/assets/index-BnNZzcsd.css +0 -1
  29. package/packages/gui/dist/assets/index-CwUZRfEx.js +0 -638
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.26.33 — Self-building pipeline, team persistence, agent quality overhaul (2026-04-11)
4
+
5
+ Major release: Groove now builds itself. Full team lifecycle, intelligent context synthesis, and agent quality improvements across the board.
6
+
7
+ **Team Persistence and Agent Reuse**
8
+ - Teams are persistent — agents stay across tasks instead of respawning every time
9
+ - Launch flow reuses existing agents by role: planner delegates, existing frontend/backend resume with new task and full context
10
+ - Auto-delegate: when all required roles exist in the team, planner skips the Launch modal and routes work directly (toast notification)
11
+ - QC lifecycle: spawns idle when no work, auto-triggers with teammate context when agents complete real work
12
+ - Cross-scope handoffs: agents write `.groove/handoffs/{role}.md`, system auto-routes to the owning agent
13
+
14
+ **Planner Intelligence**
15
+ - Mode 1 (team creation): full codebase exploration, detailed team structure
16
+ - Mode 2 (task routing): detects existing team via AGENTS_REGISTRY.md, reads only relevant files, routes to existing agents — fast, under 5 tool calls
17
+ - Always writes recommended-team.json, even for team-building-only mode (empty prompts = agents await instructions)
18
+
19
+ **Agent Quality**
20
+ - Role prompts for all 16 agent roles — frontend gets full design system (colors, CSS variables, fonts, components), backend gets ESM conventions and compliance rules
21
+ - All roles default to Heavy tier (Opus) — intelligence out of the box, users opt into cheaper models
22
+ - Permission and scope changes now persist (added to registry SAFE_FIELDS)
23
+
24
+ **Journalist Overhaul**
25
+ - Dropped exploration noise: Read/Glob/Grep tool calls no longer clutter synthesis
26
+ - Edit diffs captured: `"bg-[#3e4451]" -> "HEX.accent"` shows in project map
27
+ - Bash output captured: `npm test -> 141 passed` gives agents build/test context
28
+ - Thinking threshold raised (50 -> 200 chars) — only real decisions survive
29
+ - Transient stderr errors dropped — no more phantom error degradation
30
+ - User feedback tracking: messages to agents recorded and injected into future agent context, persisted to disk
31
+ - Decisions log deduplication: >60% word overlap with previous entry = skip
32
+ - Completed agents persist in project map for 30 minutes after finishing
33
+ - Decisions log capped at 20 entries
34
+
35
+ **Promote Pipeline**
36
+ - `./promote.sh`: build GUI -> run tests -> staging daemon on :31416 -> verify -> publish to npm + push to GitHub
37
+ - Staging uses isolated `.groove-staging/` directory — doesn't kill running daemon
38
+ - npm registry retry with version verification and cache clear
39
+ - Explicit file staging (no `git add -A`) for safety
40
+
41
+ **UX Improvements**
42
+ - Thinking indicator: rich animated phases (Analyzing, Reasoning, Planning...), elapsed timer, shimmer sweep, fires immediately on all launch paths
43
+ - Chat message dedup: Claude Code assistant + result events no longer double up
44
+ - Chat input persists across tab switches (stored in Zustand per agent)
45
+ - Agent tree: debounced fitView prevents jitter on team launch, node positions saved by name (stable across resumes)
46
+ - Edge handles recalculated from actual node positions on every render
47
+ - HTML preview in editor: Code/Preview toggle for .html files via sandboxed iframe
48
+ - Context bars use teal accent color (agent nodes + fleet panel)
49
+
50
+ **Infrastructure**
51
+ - Terminal PTY uses crypto.randomUUID() instead of predictable counters
52
+ - Codex agents skip PM gate (sandboxed providers can't reach localhost)
53
+ - Skill marketplace: Pull Latest, Uninstall, and Update flow
54
+ - Auth logs scrubbed — username only, no PII
55
+ - GC: immediate cleanup on team delete, orphaned logs removed instantly
56
+ - QC agents verify builds (`npm run build`), never start long-running dev servers
57
+
3
58
  ## v0.20.0 — Settings page, Ollama setup, GUI v2 rebuild, full system overhaul (2026-04-08)
4
59
 
5
60
  Major release: complete GUI rebuild + Settings page + Ollama setup + tech debt cleanup.
@@ -484,6 +484,9 @@ export function createApi(app, daemon) {
484
484
  const agent = daemon.registry.get(req.params.id);
485
485
  if (!agent) return res.status(404).json({ error: 'Agent not found' });
486
486
 
487
+ // Record user feedback so the journalist can include it in future agent context
488
+ if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, message.trim());
489
+
487
490
  // Agent loop path — send message directly to the running loop
488
491
  if (daemon.processes.hasAgentLoop(req.params.id)) {
489
492
  const sent = await daemon.processes.sendMessage(req.params.id, message.trim());
@@ -85,6 +85,19 @@ export class Introducer {
85
85
  lines.push(` GROOVE will automatically wake the target agent and deliver your request.`);
86
86
  lines.push(`- Check AGENTS_REGISTRY.md for the latest team state.`);
87
87
 
88
+ // User feedback from previous tasks — critical context about what the user
89
+ // observed and what needs to change. Prevents agents from repeating mistakes.
90
+ const feedback = this.daemon.journalist?.getUserFeedback() || [];
91
+ if (feedback.length > 0) {
92
+ lines.push('');
93
+ lines.push(`## User Feedback (from previous tasks)`);
94
+ lines.push('');
95
+ lines.push(`The user sent these messages about previous agents' work. Pay close attention — these indicate issues that previous agents missed:`);
96
+ for (const fb of feedback.slice(-10)) {
97
+ lines.push(`- **${fb.agentName}** (${fb.role}): "${fb.message}"`);
98
+ }
99
+ }
100
+
88
101
  // Project files section — tell the new agent what exists and what to read
89
102
  if (allTeamFiles.length > 0) {
90
103
  lines.push('');
@@ -57,9 +57,18 @@ export class Journalist {
57
57
  if (this.synthesizing) return; // Don't overlap
58
58
 
59
59
  const agents = this.daemon.registry.getAll();
60
- const activeAgents = agents.filter((a) => a.status === 'running');
60
+ const running = agents.filter((a) => a.status === 'running');
61
+
62
+ // Include recently completed agents (last 30 min) so their work persists
63
+ // in the project map instead of vanishing the moment they finish
64
+ const thirtyMinAgo = new Date(Date.now() - 30 * 60 * 1000).toISOString();
65
+ const recentlyCompleted = agents.filter((a) =>
66
+ a.status === 'completed' && a.lastActivity && a.lastActivity > thirtyMinAgo
67
+ && !running.some((r) => r.id === a.id)
68
+ );
69
+ const activeAgents = [...running, ...recentlyCompleted];
61
70
 
62
- // Skip if no active agents
71
+ // Skip if no agents to synthesize
63
72
  if (activeAgents.length === 0) return;
64
73
 
65
74
  // Smart scheduling: skip if no new log output since last cycle
@@ -149,33 +158,75 @@ export class Journalist {
149
158
  }
150
159
 
151
160
  filterLog(rawLog, agent) {
152
- // Parse stream-json lines and extract meaningful events
161
+ // Parse stream-json lines and extract meaningful events.
162
+ // Focus on PROGRESS (writes, edits, commands with results) not EXPLORATION (reads, greps).
153
163
  const entries = [];
154
164
  const lines = rawLog.split('\n');
165
+ const toolResults = new Map(); // tool_use_id -> result text
155
166
 
167
+ // First pass: collect tool results so we can attach them to tool calls
168
+ for (const line of lines) {
169
+ if (!line.trim() || line.startsWith('[')) continue;
170
+ try {
171
+ const data = JSON.parse(line);
172
+ if (data.type === 'user' && data.message?.content) {
173
+ const content = Array.isArray(data.message.content) ? data.message.content : [];
174
+ for (const block of content) {
175
+ if (block.type === 'tool_result' && block.tool_use_id) {
176
+ const text = typeof block.content === 'string' ? block.content
177
+ : Array.isArray(block.content) ? block.content.map((c) => c.text || '').join('').slice(0, 300)
178
+ : '';
179
+ if (text) toolResults.set(block.tool_use_id, text);
180
+ }
181
+ }
182
+ }
183
+ } catch { /* skip */ }
184
+ }
185
+
186
+ // Second pass: extract meaningful events
156
187
  for (const line of lines) {
157
- // Skip empty lines and GROOVE spawn headers
158
188
  if (!line.trim() || line.startsWith('[')) continue;
159
189
 
160
190
  try {
161
191
  const data = JSON.parse(line);
162
192
 
163
- // Tool use (file edits, commands)
193
+ // Tool use only keep WRITES, EDITS, and COMMANDS (progress, not exploration)
164
194
  if (data.type === 'assistant' && data.message?.content) {
165
195
  const content = data.message.content;
166
196
  if (Array.isArray(content)) {
167
197
  for (const block of content) {
168
198
  if (block.type === 'tool_use') {
169
- entries.push({
199
+ const tool = block.name;
200
+
201
+ // Skip exploration tools — reads/searches are noise for synthesis
202
+ if (tool === 'Read' || tool === 'Glob' || tool === 'Grep') continue;
203
+
204
+ const entry = {
170
205
  type: 'tool',
171
- tool: block.name,
172
- input: this.summarizeToolInput(block.name, block.input),
206
+ tool,
207
+ input: this.summarizeToolInput(tool, block.input),
173
208
  timestamp: data.timestamp,
174
- });
209
+ };
210
+
211
+ // Capture what was actually changed for Edit operations
212
+ if (tool === 'Edit' && block.input) {
213
+ const old = (block.input.old_string || '').slice(0, 150);
214
+ const nw = (block.input.new_string || '').slice(0, 150);
215
+ if (old && nw) entry.diff = `"${old}" → "${nw}"`;
216
+ }
217
+
218
+ // Capture Bash command output (exit code, first line of result)
219
+ if (tool === 'Bash' && block.id) {
220
+ const result = toolResults.get(block.id);
221
+ if (result) entry.output = result.slice(0, 200);
222
+ }
223
+
224
+ entries.push(entry);
175
225
  } else if (block.type === 'text' && block.text) {
176
- // Only keep substantial text (decisions, explanations)
226
+ // Only keep substantial reasoning (decisions, conclusions)
227
+ // Short fragments like "Let me check..." are noise
177
228
  const text = block.text.trim();
178
- if (text.length > 50) {
229
+ if (text.length > 200) {
179
230
  entries.push({ type: 'thinking', text: text.slice(0, 2000), timestamp: data.timestamp });
180
231
  }
181
232
  }
@@ -194,10 +245,8 @@ export class Journalist {
194
245
  });
195
246
  }
196
247
  } catch {
197
- // Not JSON — check for plain text signals
198
- if (line.includes('Error') || line.includes('error')) {
199
- entries.push({ type: 'error', text: line.trim().slice(0, 200) });
200
- }
248
+ // Skip non-JSON lines entirely transient stderr like ENOENT
249
+ // causes degradation when agents try to "fix" phantom errors
201
250
  }
202
251
  }
203
252
 
@@ -208,15 +257,11 @@ export class Journalist {
208
257
  if (!input) return '';
209
258
  switch (toolName) {
210
259
  case 'Write':
211
- case 'Edit':
212
260
  return input.file_path || input.path || '';
213
- case 'Read':
261
+ case 'Edit':
214
262
  return input.file_path || input.path || '';
215
263
  case 'Bash':
216
- return (input.command || '').slice(0, 100);
217
- case 'Glob':
218
- case 'Grep':
219
- return input.pattern || '';
264
+ return (input.command || '').slice(0, 150);
220
265
  default:
221
266
  return JSON.stringify(input).slice(0, 100);
222
267
  }
@@ -295,14 +340,16 @@ export class Journalist {
295
340
 
296
341
  formatEntry(entry) {
297
342
  switch (entry.type) {
298
- case 'tool':
299
- return `- [tool] ${entry.tool}: ${entry.input}`;
343
+ case 'tool': {
344
+ let line = `- [${entry.tool}] ${entry.input}`;
345
+ if (entry.diff) line += ` — changed: ${entry.diff}`;
346
+ if (entry.output) line += ` → ${entry.output.split('\n')[0]}`;
347
+ return line;
348
+ }
300
349
  case 'thinking':
301
- return `- [thought] ${entry.text.slice(0, 200)}`;
350
+ return `- [thought] ${entry.text.slice(0, 300)}`;
302
351
  case 'result':
303
- return `- [result] ${entry.text.slice(0, 200)} (${entry.turns} turns, ${entry.duration}ms)`;
304
- case 'error':
305
- return `- [error] ${entry.text}`;
352
+ return `- [result] ${entry.text.slice(0, 300)} (${entry.turns} turns, ${entry.duration}ms)`;
306
353
  default:
307
354
  return `- [${entry.type}] ${entry.text || ''}`;
308
355
  }
@@ -515,8 +562,25 @@ export class Journalist {
515
562
  existing = readFileSync(path, 'utf8').replace(/^# GROOVE Decisions Log[\s\S]*?\n\n/, '');
516
563
  }
517
564
 
565
+ // Deduplicate — skip if the new decisions are substantially similar to the most recent entry.
566
+ // Extract the last cycle's decisions text for comparison.
567
+ if (existing) {
568
+ const lastEntry = existing.match(/^## Cycle[\s\S]*?\n\n([\s\S]*?)(?=\n---|\n\n\*Auto|\n## Cycle|$)/);
569
+ if (lastEntry) {
570
+ const lastText = lastEntry[1].trim().toLowerCase().replace(/\s+/g, ' ');
571
+ const newText = decisions.trim().toLowerCase().replace(/\s+/g, ' ');
572
+ // If >60% of the new text appears in the last entry, skip (near-duplicate)
573
+ const newWords = newText.split(' ').filter((w) => w.length > 3);
574
+ const overlap = newWords.filter((w) => lastText.includes(w)).length;
575
+ if (newWords.length > 0 && overlap / newWords.length > 0.6) return;
576
+ }
577
+ }
578
+
518
579
  const entry = `## Cycle ${this.cycleCount} — ${new Date().toISOString()}\n\n${decisions}\n\n`;
519
- writeFileSync(path, header + entry + existing);
580
+
581
+ // Keep last 20 entries max to prevent unbounded growth
582
+ const entries = (entry + existing).split(/(?=^## Cycle)/m).slice(0, 20).join('');
583
+ writeFileSync(path, header + entries);
520
584
  }
521
585
 
522
586
  writeAgentSessionLogs(agents, filteredLogs) {
@@ -724,6 +788,60 @@ export class Journalist {
724
788
  }
725
789
  }
726
790
 
791
+ // --- User Feedback Tracking ---
792
+
793
+ /**
794
+ * Record user feedback/messages sent to agents.
795
+ * This gets included in the project map and handoff briefs so future
796
+ * agents know what the user said about previous work.
797
+ */
798
+ recordUserFeedback(agent, message) {
799
+ if (!this._userFeedback) this._loadFeedback();
800
+ if (!this._userFeedback[agent.id]) this._userFeedback[agent.id] = [];
801
+ this._userFeedback[agent.id].push({
802
+ agentName: agent.name,
803
+ role: agent.role,
804
+ message: message.slice(0, 500),
805
+ timestamp: new Date().toISOString(),
806
+ });
807
+ // Keep last 20 per agent
808
+ if (this._userFeedback[agent.id].length > 20) {
809
+ this._userFeedback[agent.id] = this._userFeedback[agent.id].slice(-20);
810
+ }
811
+ this._saveFeedback();
812
+ }
813
+
814
+ _loadFeedback() {
815
+ const p = resolve(this.daemon.grooveDir, 'user-feedback.json');
816
+ try {
817
+ if (existsSync(p)) this._userFeedback = JSON.parse(readFileSync(p, 'utf8'));
818
+ else this._userFeedback = {};
819
+ } catch { this._userFeedback = {}; }
820
+ }
821
+
822
+ _saveFeedback() {
823
+ try {
824
+ writeFileSync(resolve(this.daemon.grooveDir, 'user-feedback.json'), JSON.stringify(this._userFeedback, null, 2));
825
+ } catch { /* non-fatal */ }
826
+ }
827
+
828
+ /**
829
+ * Get user feedback for an agent (or all agents in a team).
830
+ * Used by the introducer to give agents context about previous attempts.
831
+ */
832
+ getUserFeedback(agentOrTeamId) {
833
+ if (!this._userFeedback) this._loadFeedback();
834
+ if (!this._userFeedback) return [];
835
+ // Check if it's an agent ID
836
+ if (this._userFeedback[agentOrTeamId]) return this._userFeedback[agentOrTeamId];
837
+ // Check by team — collect feedback for all agents in the team
838
+ const all = [];
839
+ for (const [, entries] of Object.entries(this._userFeedback)) {
840
+ all.push(...entries);
841
+ }
842
+ return all.sort((a, b) => a.timestamp.localeCompare(b.timestamp)).slice(-30);
843
+ }
844
+
727
845
  // --- Accessors ---
728
846
 
729
847
  getLastSynthesis() {
@@ -75,9 +75,11 @@ export class ClaudeCodeProvider extends Provider {
75
75
  }
76
76
 
77
77
  buildHeadlessCommand(prompt, model) {
78
- const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
78
+ // Pass prompt via stdin to avoid OS argument length limits.
79
+ // Long prompts (journalist synthesis with agent logs) can exceed ARG_MAX.
80
+ const args = ['-p', '--output-format', 'stream-json', '--verbose'];
79
81
  if (model) args.push('--model', model);
80
- return { command: 'claude', args, env: {} };
82
+ return { command: 'claude', args, env: {}, stdin: prompt };
81
83
  }
82
84
 
83
85
  buildFullPrompt(agent) {
@@ -52,13 +52,13 @@ describe('Journalist', () => {
52
52
  assert.equal(entries[0].input, 'src/api/auth.js');
53
53
  });
54
54
 
55
- it('should extract errors from logs', () => {
55
+ it('should skip non-JSON lines (transient errors are noise)', () => {
56
56
  const { daemon } = createMockDaemon();
57
57
  const journalist = new Journalist(daemon);
58
58
 
59
+ // Non-JSON error lines are dropped to prevent context degradation
59
60
  const entries = journalist.filterLog('Error: something broke\nTypeError: undefined is not a function', {});
60
- assert.equal(entries.length, 2);
61
- assert.equal(entries[0].type, 'error');
61
+ assert.equal(entries.length, 0);
62
62
  });
63
63
 
64
64
  it('should extract result events', () => {