groove-dev 0.27.28 → 0.27.29

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.
package/CLAUDE.md CHANGED
@@ -263,3 +263,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
263
263
  - Dashboard: routing donut, cache panel, context health gauges
264
264
  - Monitor/QC agent mode (stay active, loop)
265
265
  - Distribution: demo video, HN launch, Twitter content
266
+
267
+ <!-- GROOVE:START -->
268
+ ## GROOVE Orchestration (auto-injected)
269
+ Active agents: 0
270
+ See AGENTS_REGISTRY.md for full agent state.
271
+ **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
272
+ <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.28",
3
+ "version": "0.27.29",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.28",
3
+ "version": "0.27.29",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -166,7 +166,7 @@ export class Journalist {
166
166
  return false;
167
167
  }
168
168
 
169
- collectFilteredLogs(agents) {
169
+ collectFilteredLogs(agents, { since } = {}) {
170
170
  const result = {};
171
171
 
172
172
  for (const agent of agents) {
@@ -181,7 +181,7 @@ export class Journalist {
181
181
  const size = Buffer.byteLength(content);
182
182
  this.lastLogSizes[agent.id] = size;
183
183
 
184
- const { entries, explorationEntries } = this.filterLog(content, agent);
184
+ const { entries, explorationEntries } = this.filterLog(content, agent, { since });
185
185
  result[agent.id] = { agent, entries, explorationEntries };
186
186
  } catch {
187
187
  result[agent.id] = { agent, entries: [], explorationEntries: [] };
@@ -191,7 +191,7 @@ export class Journalist {
191
191
  return result;
192
192
  }
193
193
 
194
- filterLog(rawLog, agent) {
194
+ filterLog(rawLog, agent, { since } = {}) {
195
195
  // Parse stream-json lines and extract meaningful events.
196
196
  // Focus on PROGRESS (writes, edits, commands with results) not EXPLORATION (reads, greps).
197
197
  // Exploration tools are tracked separately so handoff briefs can include what was examined.
@@ -200,12 +200,15 @@ export class Journalist {
200
200
  const lines = rawLog.split('\n');
201
201
  const toolResults = new Map(); // tool_use_id -> result text
202
202
 
203
+ const sinceDate = since ? new Date(since) : null;
204
+
203
205
  // First pass: collect tool results (and error flags) so we can attach them to tool calls
204
206
  const toolErrors = new Set(); // tool_use_ids that returned errors
205
207
  for (const line of lines) {
206
208
  if (!line.trim() || line.startsWith('[')) continue;
207
209
  try {
208
210
  const data = JSON.parse(line);
211
+ if (sinceDate && data.timestamp && new Date(data.timestamp) < sinceDate) continue;
209
212
  if (data.type === 'user' && data.message?.content) {
210
213
  const content = Array.isArray(data.message.content) ? data.message.content : [];
211
214
  for (const block of content) {
@@ -227,6 +230,7 @@ export class Journalist {
227
230
 
228
231
  try {
229
232
  const data = JSON.parse(line);
233
+ if (sinceDate && data.timestamp && new Date(data.timestamp) < sinceDate) continue;
230
234
 
231
235
  // Tool use — only keep WRITES, EDITS, and COMMANDS (progress, not exploration)
232
236
  if (data.type === 'assistant' && data.message?.content) {
@@ -415,6 +419,56 @@ export class Journalist {
415
419
  return parts.join('\n');
416
420
  }
417
421
 
422
+ buildRotationSynthesisPrompt(agent, entries, options = {}) {
423
+ const dir = agent.workingDir ? `\nWorking directory: ${agent.workingDir}` : '';
424
+ const reason = options.reason || 'manual rotation';
425
+
426
+ const parts = [
427
+ 'You are briefing the next AI agent taking over this exact role.',
428
+ `The previous agent (${agent.name}, role: ${agent.role}) is being rotated out.`,
429
+ `Scope: ${agent.scope?.join(', ') || 'unrestricted'}${dir}`,
430
+ `Rotation reason: ${reason}`,
431
+ '',
432
+ 'Analyze the session log below and produce a structured handoff brief.',
433
+ '',
434
+ 'Output EXACTLY these sections:',
435
+ '',
436
+ '## Accomplishments',
437
+ '(What was completed. Name files, functions, and line numbers.)',
438
+ '',
439
+ '## In Progress',
440
+ '(What was actively being worked on when rotation happened.)',
441
+ '',
442
+ '## Key Decisions',
443
+ '(Architectural or implementation choices made and why.)',
444
+ '',
445
+ '## Blockers/Errors',
446
+ '(Unresolved errors, failed attempts, things that did not work.)',
447
+ '',
448
+ '## Next Steps',
449
+ '(What should be done next, in priority order.)',
450
+ '',
451
+ 'Be specific. Name files, functions, and line numbers. Do not summarize vaguely.',
452
+ 'Keep your response under 2000 characters.',
453
+ '',
454
+ '---',
455
+ '',
456
+ `### Session Log for ${agent.name} (${agent.role})`,
457
+ '',
458
+ ];
459
+
460
+ let totalChars = 0;
461
+ const cap = 30_000;
462
+ for (const entry of entries.slice(-200)) {
463
+ const line = this.formatEntry(entry);
464
+ if (totalChars + line.length > cap) break;
465
+ parts.push(line);
466
+ totalChars += line.length;
467
+ }
468
+
469
+ return parts.join('\n');
470
+ }
471
+
418
472
  formatEntry(entry) {
419
473
  switch (entry.type) {
420
474
  case 'tool': {
@@ -739,35 +793,10 @@ export class Journalist {
739
793
  // --- Handoff Brief for Context Rotation ---
740
794
 
741
795
  async generateHandoffBrief(agent, options = {}) {
742
- const filteredLogs = this.collectFilteredLogs([agent]);
796
+ const filteredLogs = this.collectFilteredLogs([agent], { since: agent.spawnedAt });
743
797
  const agentLog = filteredLogs[agent.id];
744
798
  const entries = agentLog?.entries || [];
745
799
 
746
- const errorSummary = entries
747
- .filter((e) => e.type === 'error')
748
- .map((e) => `- ${e.text}`)
749
- .slice(-10)
750
- .join('\n');
751
-
752
- const resultSummary = entries
753
- .filter((e) => e.type === 'result')
754
- .map((e) => e.text)
755
- .slice(-3)
756
- .join('\n');
757
-
758
- // Build file changes section — group Edit/Write operations by file path
759
- const fileChanges = {};
760
- for (const e of entries.filter((e) => e.type === 'tool' && (e.tool === 'Edit' || e.tool === 'Write'))) {
761
- const file = e.input || 'unknown';
762
- if (!fileChanges[file]) fileChanges[file] = [];
763
- if (e.diff) fileChanges[file].push(e.diff);
764
- else fileChanges[file].push(e.tool === 'Write' ? 'created' : 'modified');
765
- }
766
- const fileChangesSummary = Object.entries(fileChanges)
767
- .map(([file, changes]) => `- **${file}**: ${changes.slice(0, 3).join('; ')}`)
768
- .slice(0, 20)
769
- .join('\n');
770
-
771
800
  // Layer 7 memory: discoveries, constraints, specializations
772
801
  const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 2000) || '';
773
802
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
@@ -776,26 +805,58 @@ export class Journalist {
776
805
  ? `- Quality profile: ${specialization.avgQualityScore}/100 across ${specialization.sessionCount} sessions`
777
806
  : '';
778
807
 
779
- // Pull recent rotation history from persistent memory (Layer 7).
780
- // Gives the new agent causal continuity: what the last 3 agents struggled
781
- // with, decided, and solved — not just what the current session did.
782
808
  const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000, agent.workingDir, agent.teamId) || '';
783
809
 
784
- // Pull the user's recent messages scoped to this agent
785
810
  const agentFeedback = this.getUserFeedback(agent.id).slice(-5);
786
811
  const conversationSummary = agentFeedback.length > 0
787
812
  ? agentFeedback.map((fb) => `- "${fb.message}"`).join('\n')
788
813
  : '';
789
814
 
790
- // Compact last 5 tool calls (not full output, just tool + target)
791
- const recentTools = entries
792
- .filter((e) => e.type === 'tool' || e.type === 'error')
793
- .slice(-5)
794
- .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 80)}`)
795
- .join('\n');
815
+ // Try AI-synthesized session summary
816
+ let sessionSummary = '';
817
+ try {
818
+ const prompt = this.buildRotationSynthesisPrompt(agent, entries, options);
819
+ sessionSummary = await this.callHeadless(prompt, { trackAs: '__rotation__' });
820
+ } catch {
821
+ // Fallback: structural summary from raw logs
822
+ const errorSummary = entries
823
+ .filter((e) => e.type === 'error')
824
+ .map((e) => `- ${e.text}`)
825
+ .slice(-10)
826
+ .join('\n');
827
+
828
+ const resultSummary = entries
829
+ .filter((e) => e.type === 'result')
830
+ .map((e) => e.text)
831
+ .slice(-3)
832
+ .join('\n');
833
+
834
+ const fileChanges = {};
835
+ for (const e of entries.filter((e) => e.type === 'tool' && (e.tool === 'Edit' || e.tool === 'Write'))) {
836
+ const file = e.input || 'unknown';
837
+ if (!fileChanges[file]) fileChanges[file] = [];
838
+ if (e.diff) fileChanges[file].push(e.diff);
839
+ else fileChanges[file].push(e.tool === 'Write' ? 'created' : 'modified');
840
+ }
841
+ const fileChangesSummary = Object.entries(fileChanges)
842
+ .map(([file, changes]) => `- **${file}**: ${changes.slice(0, 3).join('; ')}`)
843
+ .slice(0, 20)
844
+ .join('\n');
845
+
846
+ const recentTools = entries
847
+ .filter((e) => e.type === 'tool' || e.type === 'error')
848
+ .slice(-5)
849
+ .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 80)}`)
850
+ .join('\n');
851
+
852
+ const fallbackParts = [];
853
+ if (errorSummary) fallbackParts.push(`## Unresolved Errors\n\n${errorSummary}`);
854
+ if (recentTools) fallbackParts.push(`## Last 5 Tool Calls\n\n${recentTools}`);
855
+ if (fileChangesSummary) fallbackParts.push(`## Files Modified\n\n${fileChangesSummary}`);
856
+ if (resultSummary) fallbackParts.push(`## Accomplishments\n\n${resultSummary}`);
857
+ sessionSummary = fallbackParts.join('\n\n');
858
+ }
796
859
 
797
- // Brief priority: errors > constraints > recent tools > accomplishments
798
- // The rotator already wraps this with session continuation context
799
860
  return [
800
861
  `# Handoff Brief — ${agent.name} (${agent.role})`,
801
862
  ``,
@@ -804,13 +865,10 @@ export class Journalist {
804
865
  `Rotation: ${options.reason || 'manual'}${options.qualityScore ? ` (quality: ${options.qualityScore}/100)` : ''} | Tokens: ${agent.tokensUsed}`,
805
866
  specLine,
806
867
  ``,
807
- errorSummary ? `## Unresolved Errors (fix these first)\n\n${errorSummary}\n` : '',
868
+ sessionSummary ? `## Session Summary\n\n${sessionSummary}\n` : '',
808
869
  constraints ? `## Project Constraints (must follow)\n\n${constraints}\n` : '',
809
870
  discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
810
871
  conversationSummary ? `## Recent User Messages\n\n${conversationSummary}\n` : '',
811
- recentTools ? `## Last 5 Tool Calls\n\n${recentTools}\n` : '',
812
- fileChangesSummary ? `## Files Modified\n\n${fileChangesSummary}\n` : '',
813
- resultSummary ? `## Accomplishments\n\n${resultSummary}\n` : '',
814
872
  recentChain ? `## Rotation History\n\n${recentChain}\n` : '',
815
873
  agent.prompt ? `## Original Task\n\n${agent.prompt}\n` : '',
816
874
  ``,
@@ -201,7 +201,7 @@ describe('Journalist', () => {
201
201
  assert.ok(brief.includes('src/api/**'));
202
202
  assert.ok(brief.includes('5000'));
203
203
  assert.ok(brief.includes('Build the auth API'));
204
- assert.ok(brief.includes('Write'));
204
+ assert.ok(brief.includes('Session Summary') || brief.includes('Write'));
205
205
  });
206
206
 
207
207
  it('instructs the agent to deliver the output, not passively wait', async () => {