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.
- package/CLAUDE.md +0 -7
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +11 -5
- package/node_modules/@groove-dev/daemon/src/lockmanager.js +22 -12
- package/node_modules/@groove-dev/daemon/src/process.js +3 -3
- package/node_modules/@groove-dev/daemon/src/teams.js +38 -9
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +1 -1
- package/node_modules/@groove-dev/daemon/test/teams.test.js +13 -3
- package/{packages/gui/dist/assets/index-zzVaD3-G.js → node_modules/@groove-dev/gui/dist/assets/index-B5Uor698.js} +252 -252
- package/node_modules/@groove-dev/gui/dist/assets/index-VGmIZurO.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +5 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +12 -0
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +25 -7
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +11 -5
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -9
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +13 -0
- package/node_modules/@groove-dev/gui/vite.config.js +0 -3
- package/package.json +2 -3
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +11 -5
- package/packages/daemon/src/lockmanager.js +22 -12
- package/packages/daemon/src/process.js +3 -3
- package/packages/daemon/src/teams.js +38 -9
- package/packages/daemon/src/tool-executor.js +1 -1
- package/{node_modules/@groove-dev/gui/dist/assets/index-zzVaD3-G.js → packages/gui/dist/assets/index-B5Uor698.js} +252 -252
- package/packages/gui/dist/assets/index-VGmIZurO.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/editor/terminal.jsx +5 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
- package/packages/gui/src/components/layout/status-bar.jsx +12 -0
- package/packages/gui/src/components/layout/terminal-panel.jsx +25 -7
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/stores/groove.js +11 -5
- package/packages/gui/src/views/agents.jsx +5 -9
- package/packages/gui/src/views/settings.jsx +13 -0
- package/packages/gui/vite.config.js +0 -3
- package/node_modules/@groove-dev/gui/dist/assets/index-Dx7i-7_K.css +0 -1
- package/node_modules/@groove-dev/gui/src/lib/edition.js +0 -4
- package/packages/gui/dist/assets/index-Dx7i-7_K.css +0 -1
- 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 -->
|
|
@@ -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 (
|
|
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:
|
|
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:
|
|
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 ->
|
|
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,
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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,
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
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 = {
|
|
41
|
-
|
|
42
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
|
139
|
-
const
|
|
140
|
-
|
|
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', () => {
|