groove-dev 0.27.41 → 0.27.44

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 (47) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +11 -5
  5. package/node_modules/@groove-dev/daemon/src/lockmanager.js +22 -12
  6. package/node_modules/@groove-dev/daemon/src/process.js +3 -3
  7. package/node_modules/@groove-dev/daemon/src/teams.js +38 -9
  8. package/node_modules/@groove-dev/daemon/src/tool-executor.js +1 -1
  9. package/node_modules/@groove-dev/daemon/test/teams.test.js +13 -3
  10. package/{packages/gui/dist/assets/index-zzVaD3-G.js → node_modules/@groove-dev/gui/dist/assets/index-B5Uor698.js} +252 -252
  11. package/node_modules/@groove-dev/gui/dist/assets/index-VGmIZurO.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  13. package/node_modules/@groove-dev/gui/package.json +1 -1
  14. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +5 -0
  15. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  16. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +12 -0
  17. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +25 -7
  18. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +11 -5
  20. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -9
  21. package/node_modules/@groove-dev/gui/src/views/settings.jsx +13 -0
  22. package/node_modules/@groove-dev/gui/vite.config.js +0 -3
  23. package/package.json +2 -3
  24. package/packages/cli/package.json +1 -1
  25. package/packages/daemon/package.json +1 -1
  26. package/packages/daemon/src/api.js +11 -5
  27. package/packages/daemon/src/lockmanager.js +22 -12
  28. package/packages/daemon/src/process.js +3 -3
  29. package/packages/daemon/src/teams.js +38 -9
  30. package/packages/daemon/src/tool-executor.js +1 -1
  31. package/{node_modules/@groove-dev/gui/dist/assets/index-zzVaD3-G.js → packages/gui/dist/assets/index-B5Uor698.js} +252 -252
  32. package/packages/gui/dist/assets/index-VGmIZurO.css +1 -0
  33. package/packages/gui/dist/index.html +2 -2
  34. package/packages/gui/package.json +1 -1
  35. package/packages/gui/src/components/editor/terminal.jsx +5 -0
  36. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  37. package/packages/gui/src/components/layout/status-bar.jsx +12 -0
  38. package/packages/gui/src/components/layout/terminal-panel.jsx +25 -7
  39. package/packages/gui/src/components/ui/toast.jsx +1 -1
  40. package/packages/gui/src/stores/groove.js +11 -5
  41. package/packages/gui/src/views/agents.jsx +5 -9
  42. package/packages/gui/src/views/settings.jsx +13 -0
  43. package/packages/gui/vite.config.js +0 -3
  44. package/node_modules/@groove-dev/gui/dist/assets/index-Dx7i-7_K.css +0 -1
  45. package/node_modules/@groove-dev/gui/src/lib/edition.js +0 -4
  46. package/packages/gui/dist/assets/index-Dx7i-7_K.css +0 -1
  47. package/packages/gui/src/lib/edition.js +0 -4
package/CLAUDE.md CHANGED
@@ -263,10 +263,3 @@ 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.41",
3
+ "version": "0.27.44",
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.41",
3
+ "version": "0.27.44",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -15,13 +15,15 @@ import { ROLE_INTEGRATIONS } from './process.js';
15
15
 
16
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
18
- const isPro = process.env.GROOVE_EDITION === 'pro';
19
18
 
20
19
  let _daemon = null;
21
20
 
21
+ // Single source of truth for Pro features: the signed-in user's subscription
22
+ // status, populated by the daemon polling the backend with the stored JWT.
23
+ // There is no build-time "Pro edition" flag — one binary, account-gated.
22
24
  function proOnly(req, res, next) {
23
25
  const sub = _daemon?.subscriptionCache || {};
24
- if (isPro || sub.active) return next();
26
+ if (sub.active) return next();
25
27
  return res.status(403).json({
26
28
  error: 'Pro subscription required',
27
29
  edition: 'community',
@@ -129,6 +131,10 @@ export function createApi(app, daemon) {
129
131
  const team = daemon.teams.get(config.teamId);
130
132
  if (team?.workingDir) config.workingDir = team.workingDir;
131
133
  }
134
+ // Inherit configured default model if the request didn't pick one
135
+ if (!config.model && daemon.config?.defaultModel) {
136
+ config.model = daemon.config.defaultModel;
137
+ }
132
138
  const agent = await daemon.processes.spawn(config);
133
139
  daemon.audit.log('agent.spawn', { id: agent.id, role: agent.role, provider: agent.provider });
134
140
  res.status(201).json(agent);
@@ -295,7 +301,7 @@ export function createApi(app, daemon) {
295
301
  // verify the path matches the scope or belongs to no one.
296
302
  if (agent.scope && agent.scope.length > 0 && targets.length > 0) {
297
303
  for (const target of targets) {
298
- const conflict = daemon.locks.check(agentId, target);
304
+ const conflict = daemon.locks.check(agentId, target, agent.workingDir);
299
305
  if (conflict.conflict) {
300
306
  daemon.audit.log('knock.denied', { agentId, toolName, target, owner: conflict.owner, pattern: conflict.pattern });
301
307
  daemon.broadcast({ type: 'knock:denied', agentId, agentName: agent.name, toolName, target, owner: conflict.owner, reason: 'scope_conflict' });
@@ -711,7 +717,7 @@ export function createApi(app, daemon) {
711
717
  app.get('/api/edition', (req, res) => {
712
718
  const sub = daemon.subscriptionCache || {};
713
719
  res.json({
714
- edition: (isPro || sub.active) ? 'pro' : 'community',
720
+ edition: sub.active ? 'pro' : 'community',
715
721
  plan: sub.plan || 'community',
716
722
  subscriptionActive: sub.active || false,
717
723
  features: sub.features || [],
@@ -734,7 +740,7 @@ export function createApi(app, daemon) {
734
740
  host: daemon.host,
735
741
  port: daemon.port,
736
742
  projectDir: daemon.projectDir,
737
- edition: (isPro || sub.active) ? 'pro' : 'community',
743
+ edition: sub.active ? 'pro' : 'community',
738
744
  });
739
745
  });
740
746
 
@@ -18,7 +18,7 @@ const DEFAULT_OPERATION_TTL_MS = 10 * 60 * 1000; // 10 minutes
18
18
  export class LockManager {
19
19
  constructor(grooveDir) {
20
20
  this.path = resolve(grooveDir, 'locks.json');
21
- this.locks = new Map(); // agentId -> glob patterns[]
21
+ this.locks = new Map(); // agentId -> { patterns, workingDir }
22
22
  this._compiledPatterns = new Map(); // agentId -> RegExp[]
23
23
  this.operations = new Map(); // agentId -> { name, resources, acquiredAt, expiresAt }
24
24
  this.load();
@@ -28,9 +28,11 @@ export class LockManager {
28
28
  if (existsSync(this.path)) {
29
29
  try {
30
30
  const data = JSON.parse(readFileSync(this.path, 'utf8'));
31
- for (const [id, patterns] of Object.entries(data)) {
32
- this.locks.set(id, patterns);
33
- this._compilePatterns(id, patterns);
31
+ for (const [id, val] of Object.entries(data)) {
32
+ // Backward compat: old format stored just patterns array
33
+ const entry = Array.isArray(val) ? { patterns: val, workingDir: null } : val;
34
+ this.locks.set(id, entry);
35
+ this._compilePatterns(id, entry.patterns);
34
36
  }
35
37
  } catch {
36
38
  // Start fresh
@@ -51,8 +53,8 @@ export class LockManager {
51
53
  this._compiledPatterns.set(agentId, compiled);
52
54
  }
53
55
 
54
- register(agentId, patterns) {
55
- this.locks.set(agentId, patterns);
56
+ register(agentId, patterns, workingDir = null) {
57
+ this.locks.set(agentId, { patterns, workingDir: workingDir || null });
56
58
  this._compilePatterns(agentId, patterns);
57
59
  this.save();
58
60
  }
@@ -64,9 +66,13 @@ export class LockManager {
64
66
  this.save();
65
67
  }
66
68
 
67
- check(agentId, filePath) {
69
+ // Scopes are per-team — only conflict with owners in the same workingDir.
70
+ // Pass workingDir=null to skip the filter (legacy behavior).
71
+ check(agentId, filePath, workingDir = null) {
68
72
  for (const [ownerId, compiled] of this._compiledPatterns) {
69
73
  if (ownerId === agentId) continue;
74
+ const ownerEntry = this.locks.get(ownerId);
75
+ if (workingDir && ownerEntry?.workingDir && ownerEntry.workingDir !== workingDir) continue;
70
76
  for (const { pattern, re } of compiled) {
71
77
  if (re && re.test(filePath)) {
72
78
  return { conflict: true, owner: ownerId, pattern };
@@ -111,11 +117,13 @@ export class LockManager {
111
117
  /**
112
118
  * Find any currently-locked agent whose scope overlaps with candidateScope.
113
119
  * Returns { overlap: true, owner, ... } for the first conflict, else {overlap:false}.
120
+ * Pass workingDir to limit the search to the same team folder (scopes are per-team).
114
121
  */
115
- findOverlappingOwner(candidateScope) {
116
- for (const [ownerId, patterns] of this.locks) {
117
- const res = LockManager.scopesOverlap(candidateScope, patterns);
118
- if (res.overlap) return { overlap: true, owner: ownerId, ownerScope: patterns, ...res };
122
+ findOverlappingOwner(candidateScope, workingDir = null) {
123
+ for (const [ownerId, entry] of this.locks) {
124
+ if (workingDir && entry.workingDir && entry.workingDir !== workingDir) continue;
125
+ const res = LockManager.scopesOverlap(candidateScope, entry.patterns);
126
+ if (res.overlap) return { overlap: true, owner: ownerId, ownerScope: entry.patterns, ...res };
119
127
  }
120
128
  return { overlap: false };
121
129
  }
@@ -140,7 +148,9 @@ export class LockManager {
140
148
  }
141
149
 
142
150
  getAll() {
143
- return Object.fromEntries(this.locks);
151
+ const obj = {};
152
+ for (const [id, entry] of this.locks) obj[id] = entry.patterns;
153
+ return obj;
144
154
  }
145
155
 
146
156
  // --- Operation locks (coordination protocol) ---
@@ -385,7 +385,7 @@ export class ProcessManager {
385
385
  // bypass entirely since their job requires broad access.
386
386
  const SCOPE_BYPASS_ROLES = new Set(['planner', 'fullstack', 'qc', 'pm', 'supervisor', 'security', 'ambassador']);
387
387
  if (config.scope && config.scope.length > 0 && !SCOPE_BYPASS_ROLES.has(config.role) && !config.allowScopeOverlap) {
388
- const conflict = locks.findOverlappingOwner(config.scope);
388
+ const conflict = locks.findOverlappingOwner(config.scope, config.workingDir);
389
389
  if (conflict.overlap) {
390
390
  const owner = registry.get(conflict.owner);
391
391
  if (owner && owner.status === 'running' && owner.workingDir === config.workingDir) {
@@ -463,7 +463,7 @@ export class ProcessManager {
463
463
 
464
464
  // Register file locks for the agent's scope
465
465
  if (agent.scope && agent.scope.length > 0) {
466
- locks.register(agent.id, agent.scope);
466
+ locks.register(agent.id, agent.scope, agent.workingDir);
467
467
  }
468
468
 
469
469
  // Register ambassador with federation system
@@ -1523,7 +1523,7 @@ For normal file edits within your scope, proceed without review.
1523
1523
 
1524
1524
  // Re-register locks
1525
1525
  if (newAgent.scope && newAgent.scope.length > 0) {
1526
- locks.register(newAgent.id, newAgent.scope);
1526
+ locks.register(newAgent.id, newAgent.scope, newAgent.workingDir);
1527
1527
  }
1528
1528
 
1529
1529
  // Spawn the resumed process
@@ -34,14 +34,30 @@ export class Teams {
34
34
  }
35
35
 
36
36
  _ensureDefault() {
37
- const hasDefault = [...this.teams.values()].some((t) => t.isDefault);
38
- if (!hasDefault) {
37
+ const defaultDir = resolve(this.daemon.projectDir, 'default');
38
+ const existing = [...this.teams.values()].find((t) => t.isDefault);
39
+
40
+ if (!existing) {
41
+ try { mkdirSync(defaultDir, { recursive: true }); } catch { /* may exist */ }
39
42
  const id = randomUUID().slice(0, 8);
40
- const team = { id, name: 'Default', isDefault: true, createdAt: new Date().toISOString() };
41
- // Default team uses the project directory (no subdirectory)
42
- team.workingDir = this.daemon.projectDir;
43
+ const team = {
44
+ id,
45
+ name: 'Default',
46
+ isDefault: true,
47
+ workingDir: defaultDir,
48
+ createdAt: new Date().toISOString(),
49
+ };
43
50
  this.teams.set(id, team);
44
51
  this._save();
52
+ return;
53
+ }
54
+
55
+ // Migrate legacy default teams that pointed at the project root — give them
56
+ // their own folder so generated files don't pile up alongside source code.
57
+ if (!existing.workingDir || existing.workingDir === this.daemon.projectDir) {
58
+ try { mkdirSync(defaultDir, { recursive: true }); } catch { /* may exist */ }
59
+ existing.workingDir = defaultDir;
60
+ this._save();
45
61
  }
46
62
  }
47
63
 
@@ -125,12 +141,13 @@ export class Teams {
125
141
  }
126
142
 
127
143
  /**
128
- * Delete a team — removes directory and all contents, moves agents to default.
144
+ * Delete a team — kills its agents, removes its directory, drops it from the
145
+ * registry. Deleting the default team regenerates a fresh empty one so users
146
+ * can wipe accumulated state and keep working without restarting the daemon.
129
147
  */
130
148
  delete(id) {
131
149
  const team = this.teams.get(id);
132
150
  if (!team) throw new Error('Team not found');
133
- if (team.isDefault) throw new Error('Cannot delete the default team');
134
151
 
135
152
  // Kill any running agents in this team
136
153
  const agents = this.daemon.registry.getAll().filter((a) => a.teamId === id);
@@ -145,8 +162,13 @@ export class Teams {
145
162
  this.daemon.registry.remove(agent.id);
146
163
  }
147
164
 
148
- // Remove the working directory
149
- if (team.workingDir && !team.isDefault && existsSync(team.workingDir)) {
165
+ // Remove the team's working directory — refuse to nuke the project root
166
+ // (legacy default teams that were never migrated point there).
167
+ if (
168
+ team.workingDir &&
169
+ team.workingDir !== this.daemon.projectDir &&
170
+ existsSync(team.workingDir)
171
+ ) {
150
172
  try {
151
173
  rmSync(team.workingDir, { recursive: true, force: true });
152
174
  } catch (err) {
@@ -158,6 +180,13 @@ export class Teams {
158
180
  this._save();
159
181
  this.daemon.broadcast({ type: 'team:deleted', teamId: id });
160
182
 
183
+ // Always keep a default team available — regenerate one with a clean folder
184
+ if (team.isDefault) {
185
+ this._ensureDefault();
186
+ const fresh = this.getDefault();
187
+ if (fresh) this.daemon.broadcast({ type: 'team:created', team: fresh });
188
+ }
189
+
161
190
  // Clean up orphaned logs immediately — don't wait for the 24h GC cycle
162
191
  try { this.daemon._gc(); } catch { /* gc should never block deletion */ }
163
192
 
@@ -158,7 +158,7 @@ export class ToolExecutor {
158
158
  _checkWriteScope(resolvedPath) {
159
159
  if (!this.daemon?.locks) return;
160
160
  const rel = relative(this.workingDir, resolvedPath);
161
- const result = this.daemon.locks.check(this.agentId, rel);
161
+ const result = this.daemon.locks.check(this.agentId, rel, this.workingDir);
162
162
  if (result.conflict) {
163
163
  // Record conflict for supervisor + token savings
164
164
  if (this.daemon.supervisor) {
@@ -135,9 +135,19 @@ describe('Teams', () => {
135
135
  assert.equal(msg.teamId, team.id);
136
136
  });
137
137
 
138
- it('should throw when deleting the default team', () => {
139
- const d = teams.getDefault();
140
- assert.throws(() => teams.delete(d.id), /Cannot delete the default/);
138
+ it('should regenerate a fresh default team when the default is deleted', () => {
139
+ const original = teams.getDefault();
140
+ broadcasts.length = 0;
141
+
142
+ teams.delete(original.id);
143
+
144
+ const fresh = teams.getDefault();
145
+ assert.ok(fresh, 'a new default team should be created');
146
+ assert.notEqual(fresh.id, original.id);
147
+ assert.equal(fresh.isDefault, true);
148
+ assert.equal(teams.list().length, 1);
149
+ assert.ok(broadcasts.find((b) => b.type === 'team:deleted' && b.teamId === original.id));
150
+ assert.ok(broadcasts.find((b) => b.type === 'team:created' && b.team?.id === fresh.id));
141
151
  });
142
152
 
143
153
  it('should throw when deleting nonexistent team', () => {