groove-dev 0.27.40 → 0.27.42
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/analyist/groove-security-audit.md +323 -0
- 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/adaptive.js +24 -5
- package/node_modules/@groove-dev/daemon/src/api.js +26 -8
- package/node_modules/@groove-dev/daemon/src/lockmanager.js +22 -12
- package/node_modules/@groove-dev/daemon/src/preview.js +30 -11
- package/node_modules/@groove-dev/daemon/src/process.js +26 -13
- 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/node_modules/@groove-dev/gui/dist/assets/{index-zzVaD3-G.js → index-C1C2biHU.js} +250 -250
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +10 -5
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +4 -8
- 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/adaptive.js +24 -5
- package/packages/daemon/src/api.js +26 -8
- package/packages/daemon/src/lockmanager.js +22 -12
- package/packages/daemon/src/preview.js +30 -11
- package/packages/daemon/src/process.js +26 -13
- package/packages/daemon/src/teams.js +38 -9
- package/packages/daemon/src/tool-executor.js +1 -1
- package/packages/gui/dist/assets/{index-zzVaD3-G.js → index-C1C2biHU.js} +250 -250
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/stores/groove.js +10 -5
- package/packages/gui/src/views/agents.jsx +4 -8
- package/packages/gui/src/views/settings.jsx +13 -0
- package/packages/gui/vite.config.js +0 -3
- package/node_modules/@groove-dev/gui/src/lib/edition.js +0 -4
- package/packages/gui/src/lib/edition.js +0 -4
|
@@ -377,18 +377,21 @@ export class ProcessManager {
|
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
// Scope collision check: refuse to spawn if another running agent
|
|
381
|
-
// claims overlapping files.
|
|
382
|
-
//
|
|
380
|
+
// Scope collision check: refuse to spawn if another running agent in the
|
|
381
|
+
// SAME working directory already claims overlapping files. Scopes are
|
|
382
|
+
// relative patterns rooted at the agent's workingDir, so two agents in
|
|
383
|
+
// different workingDirs can't step on each other even if their patterns
|
|
384
|
+
// look identical. Oversight roles (planner, QC, security) and ambassadors
|
|
385
|
+
// bypass entirely since their job requires broad access.
|
|
383
386
|
const SCOPE_BYPASS_ROLES = new Set(['planner', 'fullstack', 'qc', 'pm', 'supervisor', 'security', 'ambassador']);
|
|
384
387
|
if (config.scope && config.scope.length > 0 && !SCOPE_BYPASS_ROLES.has(config.role) && !config.allowScopeOverlap) {
|
|
385
|
-
const conflict = locks.findOverlappingOwner(config.scope);
|
|
388
|
+
const conflict = locks.findOverlappingOwner(config.scope, config.workingDir);
|
|
386
389
|
if (conflict.overlap) {
|
|
387
390
|
const owner = registry.get(conflict.owner);
|
|
388
|
-
if (owner && owner.status === 'running') {
|
|
391
|
+
if (owner && owner.status === 'running' && owner.workingDir === config.workingDir) {
|
|
389
392
|
const ownerScope = Array.isArray(conflict.ownerScope) ? conflict.ownerScope.join(', ') : '';
|
|
390
393
|
throw new Error(
|
|
391
|
-
`Scope collision: ${config.role} scope [${config.scope.join(', ')}] overlaps with ${owner.name} (${owner.role}) which owns [${ownerScope}]. ` +
|
|
394
|
+
`Scope collision: ${config.role} scope [${config.scope.join(', ')}] overlaps with ${owner.name} (${owner.role}) which owns [${ownerScope}] in the same workspace. ` +
|
|
392
395
|
`Two agents cannot edit the same files. Either narrow the scope or wait for ${owner.name} to finish.`
|
|
393
396
|
);
|
|
394
397
|
}
|
|
@@ -460,7 +463,7 @@ export class ProcessManager {
|
|
|
460
463
|
|
|
461
464
|
// Register file locks for the agent's scope
|
|
462
465
|
if (agent.scope && agent.scope.length > 0) {
|
|
463
|
-
locks.register(agent.id, agent.scope);
|
|
466
|
+
locks.register(agent.id, agent.scope, agent.workingDir);
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
// Register ambassador with federation system
|
|
@@ -1098,16 +1101,25 @@ For normal file edits within your scope, proceed without review.
|
|
|
1098
1101
|
* - The daemon has a preview plan stashed for this team (planner wrote one).
|
|
1099
1102
|
* - No pending phase 2 groups for this team (QC hasn't spawned yet).
|
|
1100
1103
|
* - Every non-planner team agent is in a terminal state.
|
|
1101
|
-
* - At least one non-planner agent completed successfully
|
|
1102
|
-
*
|
|
1104
|
+
* - At least one non-planner agent completed successfully.
|
|
1105
|
+
*
|
|
1106
|
+
* The plan is intentionally NOT cleared on failure so the user can hit the
|
|
1107
|
+
* "Launch Preview" button manually after fixing whatever blocked the auto
|
|
1108
|
+
* attempt (wrong cwd, missing deps, etc). We guard against infinite re-fires
|
|
1109
|
+
* with _previewAttempted per teamId.
|
|
1103
1110
|
*/
|
|
1104
1111
|
_checkPreviewReady(teamId) {
|
|
1105
1112
|
const preview = this.daemon.preview;
|
|
1106
1113
|
if (!preview) return;
|
|
1114
|
+
if (!this._previewAttempted) this._previewAttempted = new Set();
|
|
1115
|
+
if (this._previewAttempted.has(teamId)) return;
|
|
1116
|
+
|
|
1107
1117
|
const plan = preview.getPlan(teamId);
|
|
1108
|
-
if (!plan)
|
|
1118
|
+
if (!plan) {
|
|
1119
|
+
this.daemon.audit?.log('preview.skipped', { teamId, reason: 'no_plan_stashed' });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1109
1122
|
|
|
1110
|
-
// If a phase 2 group for this team is still pending, let it spawn first.
|
|
1111
1123
|
const pendingPhase2 = this.daemon._pendingPhase2 || [];
|
|
1112
1124
|
for (const group of pendingPhase2) {
|
|
1113
1125
|
for (const id of group.waitFor) {
|
|
@@ -1123,7 +1135,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1123
1135
|
const anyCompleted = teamAgents.some((a) => a.status === 'completed');
|
|
1124
1136
|
if (!allDone || !anyCompleted) return;
|
|
1125
1137
|
|
|
1126
|
-
|
|
1138
|
+
this._previewAttempted.add(teamId);
|
|
1127
1139
|
const workingDir = plan.workingDir;
|
|
1128
1140
|
preview.launch(teamId, workingDir, plan.preview).then((result) => {
|
|
1129
1141
|
if (!result.launched) {
|
|
@@ -1137,6 +1149,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1137
1149
|
}
|
|
1138
1150
|
}).catch((err) => {
|
|
1139
1151
|
console.error(`[Groove] Preview launch error for team ${teamId}:`, err.message);
|
|
1152
|
+
this.daemon.broadcast({ type: 'preview:failed', teamId, reason: err.message });
|
|
1140
1153
|
});
|
|
1141
1154
|
}
|
|
1142
1155
|
|
|
@@ -1510,7 +1523,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1510
1523
|
|
|
1511
1524
|
// Re-register locks
|
|
1512
1525
|
if (newAgent.scope && newAgent.scope.length > 0) {
|
|
1513
|
-
locks.register(newAgent.id, newAgent.scope);
|
|
1526
|
+
locks.register(newAgent.id, newAgent.scope, newAgent.workingDir);
|
|
1514
1527
|
}
|
|
1515
1528
|
|
|
1516
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', () => {
|