groove-dev 0.27.117 → 0.27.119

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 (48) hide show
  1. package/TRAINING_DATA_v4.md +6 -6
  2. package/moe-training/client/domain-tagger.js +3 -3
  3. package/moe-training/client/trajectory-capture.js +7 -0
  4. package/moe-training/client/transmission-queue.js +6 -0
  5. package/moe-training/test/shared/envelope-schema.test.js +3 -3
  6. package/node_modules/@groove-dev/cli/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/package.json +1 -1
  8. package/node_modules/@groove-dev/daemon/src/api.js +13 -4
  9. package/node_modules/@groove-dev/daemon/src/index.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/teams.js +70 -39
  11. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
  18. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
  19. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +29 -7
  20. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  21. package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +151 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +29 -5
  23. package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
  24. package/node_modules/@groove-dev/gui/src/views/teams.jsx +4 -0
  25. package/node_modules/moe-training/client/domain-tagger.js +3 -3
  26. package/node_modules/moe-training/client/trajectory-capture.js +7 -0
  27. package/node_modules/moe-training/client/transmission-queue.js +6 -0
  28. package/node_modules/moe-training/test/shared/envelope-schema.test.js +3 -3
  29. package/package.json +1 -1
  30. package/packages/cli/package.json +1 -1
  31. package/packages/daemon/package.json +1 -1
  32. package/packages/daemon/src/api.js +13 -4
  33. package/packages/daemon/src/index.js +4 -0
  34. package/packages/daemon/src/teams.js +70 -39
  35. package/packages/daemon/src/validate.js +10 -0
  36. package/packages/gui/dist/assets/{index-fq--PD7_.js → index-BxPCaxlC.js} +131 -131
  37. package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-DT6Jbf_q.css} +1 -1
  38. package/packages/gui/dist/index.html +2 -2
  39. package/packages/gui/package.json +1 -1
  40. package/packages/gui/src/app.jsx +2 -0
  41. package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
  42. package/packages/gui/src/components/editor/file-tree.jsx +80 -3
  43. package/packages/gui/src/components/settings/quick-connect.jsx +29 -7
  44. package/packages/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  45. package/packages/gui/src/components/ui/data-sharing-modal.jsx +151 -0
  46. package/packages/gui/src/stores/groove.js +29 -5
  47. package/packages/gui/src/views/agents.jsx +47 -11
  48. package/packages/gui/src/views/teams.jsx +4 -0
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
10
10
  import { cn } from '../lib/cn';
11
11
  import { Button } from '../components/ui/button';
12
12
  import { Badge } from '../components/ui/badge';
13
- import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, Archive } from 'lucide-react';
13
+ import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, Archive, Box, HardDrive } from 'lucide-react';
14
14
  import { PreviewWorkspace } from '../components/preview/preview-workspace';
15
15
  import { WorkspaceMode } from '../components/agents/workspace-mode';
16
16
  import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
@@ -313,6 +313,7 @@ export function TeamTabBar() {
313
313
  onOpenChange={(open) => !open && setArchiveConfirm(null)}
314
314
  onArchive={archiveTeam}
315
315
  onDeletePermanently={deleteTeamPermanently}
316
+ mode={archiveConfirm?.mode || 'sandbox'}
316
317
  />
317
318
  </div>
318
319
  );
@@ -1244,6 +1245,7 @@ function RecommendedTeamCard() {
1244
1245
  const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
1245
1246
  const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
1246
1247
  const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
1248
+ const [tsMode, setTsMode] = useState(teamLaunchConfig?.mode || 'sandbox');
1247
1249
 
1248
1250
  useEffect(() => {
1249
1251
  fetchProviders().then((list) => {
@@ -1278,15 +1280,14 @@ function RecommendedTeamCard() {
1278
1280
  async function handleLaunch() {
1279
1281
  setLaunching(true);
1280
1282
  // Save overrides to store so launchRecommendedTeam sends them
1281
- if (tsProvider) {
1282
- useGrooveStore.setState({
1283
- teamLaunchConfig: {
1284
- provider: tsProvider, model: tsModel,
1285
- reasoningEffort: tsReasoning,
1286
- ...(showTemp && { temperature: tsTemp }),
1287
- },
1288
- });
1289
- }
1283
+ useGrooveStore.setState({
1284
+ teamLaunchConfig: {
1285
+ ...(tsProvider && { provider: tsProvider, model: tsModel }),
1286
+ reasoningEffort: tsReasoning,
1287
+ ...(showTemp && { temperature: tsTemp }),
1288
+ mode: tsMode,
1289
+ },
1290
+ });
1290
1291
  try {
1291
1292
  const modified = [...agentEdits, ...phase2];
1292
1293
  await launchRecommendedTeam(modified);
@@ -1362,6 +1363,41 @@ function RecommendedTeamCard() {
1362
1363
  formatValue={(v) => v.toFixed(2)}
1363
1364
  />
1364
1365
  )}
1366
+ {/* Build Mode */}
1367
+ <div className="space-y-1">
1368
+ <label className="text-2xs text-text-3 font-sans">Build Mode</label>
1369
+ <div className="flex rounded-md bg-surface-4 border border-border-subtle p-0.5">
1370
+ <button
1371
+ onClick={() => setTsMode('sandbox')}
1372
+ className={cn(
1373
+ 'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
1374
+ tsMode === 'sandbox'
1375
+ ? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
1376
+ : 'text-text-3 hover:text-text-1',
1377
+ )}
1378
+ >
1379
+ <Box size={11} />
1380
+ Sandbox
1381
+ </button>
1382
+ <button
1383
+ onClick={() => setTsMode('production')}
1384
+ className={cn(
1385
+ 'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
1386
+ tsMode === 'production'
1387
+ ? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
1388
+ : 'text-text-3 hover:text-text-1',
1389
+ )}
1390
+ >
1391
+ <HardDrive size={11} />
1392
+ Production
1393
+ </button>
1394
+ </div>
1395
+ <p className="text-2xs text-text-4 font-sans">
1396
+ {tsMode === 'sandbox'
1397
+ ? 'Files live in a team directory, removable with the team'
1398
+ : 'Files live in the project directory, persist forever'}
1399
+ </p>
1400
+ </div>
1365
1401
  </div>
1366
1402
  )}
1367
1403
  </div>
@@ -1394,7 +1430,7 @@ function RecommendedTeamCard() {
1394
1430
  );
1395
1431
  })}
1396
1432
 
1397
- {recommendedTeam.projectDir && (
1433
+ {recommendedTeam.projectDir && tsMode === 'sandbox' && (
1398
1434
  <div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
1399
1435
  <span className="text-text-4">Project:</span>
1400
1436
  <span className="text-accent">{recommendedTeam.projectDir}/</span>
@@ -73,6 +73,9 @@ function TeamsDashboard() {
73
73
  <div className="flex items-center gap-2">
74
74
  <span className="text-sm font-semibold text-text-0 font-sans">{team.name}</span>
75
75
  {isActive && <Badge variant="accent" className="text-2xs">Active</Badge>}
76
+ <Badge variant={team.mode === 'production' ? 'success' : 'default'} className="text-2xs">
77
+ {team.mode === 'production' ? 'Production' : 'Sandbox'}
78
+ </Badge>
76
79
  </div>
77
80
  {team.workingDir && (
78
81
  <div className="flex items-center gap-1 mt-0.5">
@@ -183,6 +186,7 @@ function TeamsDashboard() {
183
186
  onOpenChange={(open) => !open && setArchiveConfirm(null)}
184
187
  onArchive={archiveTeam}
185
188
  onDeletePermanently={deleteTeamPermanently}
189
+ mode={archiveConfirm?.mode || 'sandbox'}
186
190
  />
187
191
 
188
192
  <PurgeConfirmDialog
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';
4
4
 
5
- const DEFAULT_MODEL = 'sentence-transformers/all-MiniLM-L6-v2';
5
+ const DEFAULT_MODEL = 'BAAI/bge-small-en-v1.5';
6
6
  const DEFAULT_TOP_K = 3;
7
7
 
8
8
  // ~40 domains covering broad technical territory.
@@ -170,8 +170,8 @@ const DOMAIN_TAXONOMY = {
170
170
  description: 'Scientific computing, numerical methods, MATLAB/SciPy/Julia, simulations, optimization, statistics',
171
171
  },
172
172
  planning_strategy: {
173
- keywords: ['plan', 'strategy', 'architect', 'design doc', 'breakdown', 'scope', 'roadmap', 'milestone', 'prioritize', 'tradeoff', 'approach', 'recommend', 'team', 'coordinate', 'delegate', 'assign'],
174
- description: 'Project planning, task breakdown, architecture decisions, team coordination, strategy, roadmaps, scoping, prioritization',
173
+ keywords: ['plan', 'strategy', 'architect', 'design doc', 'breakdown', 'scope', 'roadmap', 'milestone', 'prioritize', 'tradeoff', 'approach', 'recommend', 'team', 'coordinate', 'delegate', 'assign', 'orchestration', 'dispatch', 'routing', 'decompose', 'pipeline', 'inference', 'layer'],
174
+ description: 'Project planning, task decomposition, architecture design, system orchestration, routing and dispatch design, inference pipelines, team coordination, strategy, roadmaps, scoping, prioritization, tradeoff analysis',
175
175
  },
176
176
  conversational_reasoning: {
177
177
  keywords: ['explain', 'why', 'how does', 'what is', 'clarify', 'understand', 'reason', 'think through', 'analyze', 'compare', 'evaluate', 'brainstorm', 'discuss', 'opinion', 'advice'],
@@ -34,6 +34,12 @@ export class TrajectoryCapture {
34
34
  this._offlineRetryTimer = null;
35
35
  this._contexts = new Map();
36
36
  this._shutdown = false;
37
+ this._onEnvelopeSent = null;
38
+ }
39
+
40
+ set onEnvelopeSent(fn) {
41
+ this._onEnvelopeSent = typeof fn === 'function' ? fn : null;
42
+ if (this._transmissionQueue) this._transmissionQueue.onSent = this._onEnvelopeSent;
37
43
  }
38
44
 
39
45
  async init() {
@@ -46,6 +52,7 @@ export class TrajectoryCapture {
46
52
  this._scrubber = new PIIScrubber();
47
53
  this._attestation = new SessionAttestation(this._centralCommandUrl);
48
54
  this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);
55
+ if (this._onEnvelopeSent) this._transmissionQueue.onSent = this._onEnvelopeSent;
49
56
  this._transmissionQueue.start();
50
57
  this._domainTagger = new DomainTagger();
51
58
  await this._domainTagger.init();
@@ -10,6 +10,7 @@ export class TransmissionQueue {
10
10
  this._offlineQueue = [];
11
11
  this._running = false;
12
12
  this._drainPromise = null;
13
+ this._onSent = null;
13
14
 
14
15
  if (process.env.NODE_ENV === 'production' && !centralCommandUrl.startsWith('https://')) {
15
16
  console.warn('[TransmissionQueue] WARNING: centralCommandUrl does not use HTTPS in production');
@@ -20,6 +21,10 @@ export class TransmissionQueue {
20
21
  return this._offlineQueue.length;
21
22
  }
22
23
 
24
+ set onSent(fn) {
25
+ this._onSent = typeof fn === 'function' ? fn : null;
26
+ }
27
+
23
28
  enqueue(signedEnvelope) {
24
29
  if (this._queue.length >= this._maxSize) return;
25
30
  if (signedEnvelope?.attestation?.session_hmac === 'OFFLINE') {
@@ -86,6 +91,7 @@ export class TransmissionQueue {
86
91
  });
87
92
  if (res.ok) {
88
93
  success = true;
94
+ if (this._onSent) this._onSent(envelope);
89
95
  break;
90
96
  }
91
97
  } catch {
@@ -475,7 +475,7 @@ describe('envelope-schema', () => {
475
475
  it('accepts valid session_embedding object', () => {
476
476
  const env = validEnvelope();
477
477
  env.metadata.session_embedding = {
478
- model: 'all-MiniLM-L6-v2',
478
+ model: 'BAAI/bge-small-en-v1.5',
479
479
  vector: [0.0234, -0.0891, 0.1247, 0.0562],
480
480
  source_text: 'Write a Python decorator that caches function results',
481
481
  };
@@ -486,7 +486,7 @@ describe('envelope-schema', () => {
486
486
  it('rejects session_embedding with empty vector', () => {
487
487
  const env = validEnvelope();
488
488
  env.metadata.session_embedding = {
489
- model: 'all-MiniLM-L6-v2',
489
+ model: 'BAAI/bge-small-en-v1.5',
490
490
  vector: [],
491
491
  source_text: 'test',
492
492
  };
@@ -498,7 +498,7 @@ describe('envelope-schema', () => {
498
498
  it('rejects session_embedding with non-numeric vector values', () => {
499
499
  const env = validEnvelope();
500
500
  env.metadata.session_embedding = {
501
- model: 'all-MiniLM-L6-v2',
501
+ model: 'BAAI/bge-small-en-v1.5',
502
502
  vector: [0.1, 'bad', 0.3],
503
503
  source_text: 'test',
504
504
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.117",
3
+ "version": "0.27.119",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.117",
3
+ "version": "0.27.119",
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.117",
3
+ "version": "0.27.119",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -16,7 +16,7 @@ import { OllamaProvider } from './providers/ollama.js';
16
16
  import { ClaudeCodeProvider } from './providers/claude-code.js';
17
17
  import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
18
18
  import { ConsentManager } from '../../../moe-training/client/index.js';
19
- import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from './validate.js';
19
+ import { validateAgentConfig, validateReasoningEffort, validateVerbosity, validateTeamMode } from './validate.js';
20
20
  import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
21
21
 
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -1092,8 +1092,8 @@ export function createApi(app, daemon) {
1092
1092
 
1093
1093
  app.post('/api/teams', (req, res) => {
1094
1094
  try {
1095
- const team = daemon.teams.create(req.body.name, req.body.workingDir);
1096
- daemon.audit.log('team.create', { id: team.id, name: team.name, workingDir: team.workingDir });
1095
+ const team = daemon.teams.create(req.body.name, { mode: req.body.mode });
1096
+ daemon.audit.log('team.create', { id: team.id, name: team.name, mode: team.mode, workingDir: team.workingDir });
1097
1097
  res.status(201).json(team);
1098
1098
  } catch (err) {
1099
1099
  res.status(400).json({ error: err.message });
@@ -3447,9 +3447,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
3447
3447
  }
3448
3448
  const defaultTeamId = launchTeamId || daemon.teams.getDefault()?.id || null;
3449
3449
 
3450
+ // Determine team build mode
3451
+ let launchMode;
3452
+ try { launchMode = validateTeamMode(req.body?.mode || raw.mode); } catch { launchMode = 'sandbox'; }
3453
+
3450
3454
  // If planner specified a project directory, create it and use it as workingDir
3455
+ // Production mode: always use projectDir directly, skip subdirectory creation
3451
3456
  let projectWorkingDir = baseDir;
3452
- if (projectDir) {
3457
+ if (launchMode === 'production') {
3458
+ projectWorkingDir = daemon.projectDir;
3459
+ console.log(`[Groove] Production mode — working in project root: ${projectWorkingDir}`);
3460
+ } else if (projectDir) {
3453
3461
  // Sanitize: kebab-case, no path traversal
3454
3462
  const safeName = String(projectDir).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
3455
3463
  projectWorkingDir = resolve(baseDir, safeName);
@@ -4990,6 +4998,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4990
4998
  'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
4991
4999
  'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
4992
5000
  'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
5001
+ 'dataSharingDismissed',
4993
5002
  ];
4994
5003
  for (const key of Object.keys(req.body)) {
4995
5004
  if (!ALLOWED_KEYS.includes(key)) {
@@ -686,6 +686,10 @@ export class Daemon {
686
686
  centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
687
687
  grooveVersion: version,
688
688
  });
689
+ this.trajectoryCapture.onEnvelopeSent = () => {
690
+ const count = (this.state.get('training_envelopes_sent') || 0) + 1;
691
+ this.state.set('training_envelopes_sent', count);
692
+ };
689
693
  this.trajectoryCapture.init();
690
694
  } catch (e) {
691
695
  // Training capture is never critical
@@ -2,9 +2,9 @@
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
5
- import { resolve, basename } from 'path';
5
+ import { resolve } from 'path';
6
6
  import { randomUUID } from 'crypto';
7
- import { validateTeamName } from './validate.js';
7
+ import { validateTeamName, validateTeamMode } from './validate.js';
8
8
 
9
9
  function slugify(name) {
10
10
  return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64) || 'team';
@@ -44,6 +44,7 @@ export class Teams {
44
44
  id,
45
45
  name: 'Default',
46
46
  isDefault: true,
47
+ mode: 'sandbox',
47
48
  workingDir: defaultDir,
48
49
  createdAt: new Date().toISOString(),
49
50
  };
@@ -64,19 +65,25 @@ export class Teams {
64
65
  /**
65
66
  * Create a team with an auto-managed working directory.
66
67
  */
67
- create(name) {
68
+ create(name, { mode = 'sandbox' } = {}) {
68
69
  validateTeamName(name);
70
+ mode = validateTeamMode(mode);
69
71
  const id = randomUUID().slice(0, 8);
70
- const dirName = slugify(name);
71
- const workingDir = resolve(this.daemon.projectDir, dirName);
72
72
 
73
- // Create the directory
74
- mkdirSync(workingDir, { recursive: true });
73
+ let workingDir;
74
+ if (mode === 'production') {
75
+ workingDir = this.daemon.projectDir;
76
+ } else {
77
+ const dirName = slugify(name);
78
+ workingDir = resolve(this.daemon.projectDir, dirName);
79
+ mkdirSync(workingDir, { recursive: true });
80
+ }
75
81
 
76
82
  const team = {
77
83
  id,
78
84
  name,
79
85
  isDefault: false,
86
+ mode,
80
87
  workingDir,
81
88
  createdAt: new Date().toISOString(),
82
89
  };
@@ -110,8 +117,9 @@ export class Teams {
110
117
  const oldName = team.name;
111
118
  team.name = name;
112
119
 
120
+ // Production teams use the project root — never rename directories
113
121
  // Rename the directory if it was auto-managed (under projectDir)
114
- if (team.workingDir && !team.isDefault) {
122
+ if (team.workingDir && !team.isDefault && team.mode !== 'production') {
115
123
  const newDirName = slugify(name);
116
124
  const newWorkingDir = resolve(this.daemon.projectDir, newDirName);
117
125
  const oldWorkingDir = team.workingDir;
@@ -150,18 +158,30 @@ export class Teams {
150
158
 
151
159
  const agents = this._killAndRemoveAgents(id);
152
160
 
153
- if (
154
- team.workingDir &&
155
- team.workingDir !== this.daemon.projectDir &&
156
- existsSync(team.workingDir)
157
- ) {
158
- try {
159
- const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
160
- mkdirSync(archiveDir, { recursive: true });
161
- const slug = basename(team.workingDir);
162
- const archiveName = `${slug}-${Date.now()}`;
163
- const archivePath = resolve(archiveDir, archiveName);
164
-
161
+ try {
162
+ const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
163
+ mkdirSync(archiveDir, { recursive: true });
164
+ const slug = slugify(team.name);
165
+ const archiveName = `${slug}-${Date.now()}`;
166
+ const archivePath = resolve(archiveDir, archiveName);
167
+
168
+ if (team.mode === 'production') {
169
+ // Production teams: metadata-only archive (no directory move)
170
+ mkdirSync(archivePath, { recursive: true });
171
+ const metadata = {
172
+ originalName: team.name,
173
+ originalId: team.id,
174
+ mode: team.mode,
175
+ deletedAt: new Date().toISOString(),
176
+ agentCount: agents.length,
177
+ originalWorkingDir: team.workingDir,
178
+ };
179
+ writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
180
+ } else if (
181
+ team.workingDir &&
182
+ team.workingDir !== this.daemon.projectDir &&
183
+ existsSync(team.workingDir)
184
+ ) {
165
185
  try {
166
186
  renameSync(team.workingDir, archivePath);
167
187
  } catch (err) {
@@ -176,14 +196,15 @@ export class Teams {
176
196
  const metadata = {
177
197
  originalName: team.name,
178
198
  originalId: team.id,
199
+ mode: team.mode || 'sandbox',
179
200
  deletedAt: new Date().toISOString(),
180
201
  agentCount: agents.length,
181
202
  originalWorkingDir: team.workingDir,
182
203
  };
183
204
  writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
184
- } catch (err) {
185
- console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
186
205
  }
206
+ } catch (err) {
207
+ console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
187
208
  }
188
209
 
189
210
  this._removeTeamAndCleanup(team, id);
@@ -273,32 +294,42 @@ export class Teams {
273
294
  try { meta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* use defaults */ }
274
295
 
275
296
  const name = meta.originalName || archivedId;
276
- let workingDir = meta.originalWorkingDir || resolve(this.daemon.projectDir, slugify(name));
277
-
278
- if (existsSync(workingDir)) {
279
- workingDir = resolve(this.daemon.projectDir, `${slugify(name)}-${Date.now()}`);
280
- }
297
+ const mode = meta.mode || 'sandbox';
298
+
299
+ let workingDir;
300
+ if (mode === 'production') {
301
+ workingDir = this.daemon.projectDir;
302
+ // Production archive is metadata-only — just remove the archive directory
303
+ try { rmSync(archivePath, { recursive: true, force: true }); } catch { /* ignore */ }
304
+ } else {
305
+ workingDir = meta.originalWorkingDir || resolve(this.daemon.projectDir, slugify(name));
306
+
307
+ if (existsSync(workingDir)) {
308
+ workingDir = resolve(this.daemon.projectDir, `${slugify(name)}-${Date.now()}`);
309
+ }
281
310
 
282
- try {
283
- renameSync(archivePath, workingDir);
284
- } catch (err) {
285
- if (err.code === 'EXDEV') {
286
- cpSync(archivePath, workingDir, { recursive: true });
287
- rmSync(archivePath, { recursive: true, force: true });
288
- } else {
289
- throw err;
311
+ try {
312
+ renameSync(archivePath, workingDir);
313
+ } catch (err) {
314
+ if (err.code === 'EXDEV') {
315
+ cpSync(archivePath, workingDir, { recursive: true });
316
+ rmSync(archivePath, { recursive: true, force: true });
317
+ } else {
318
+ throw err;
319
+ }
290
320
  }
291
- }
292
321
 
293
- // Remove the metadata file from the restored directory
294
- const restoredMetaPath = resolve(workingDir, 'metadata.json');
295
- try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
322
+ // Remove the metadata file from the restored directory
323
+ const restoredMetaPath = resolve(workingDir, 'metadata.json');
324
+ try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
325
+ }
296
326
 
297
327
  const id = randomUUID().slice(0, 8);
298
328
  const team = {
299
329
  id,
300
330
  name,
301
331
  isDefault: false,
332
+ mode,
302
333
  workingDir,
303
334
  createdAt: new Date().toISOString(),
304
335
  };
@@ -239,6 +239,16 @@ export function validateVerbosity(value) {
239
239
  return value;
240
240
  }
241
241
 
242
+ const VALID_TEAM_MODES = ['sandbox', 'production'];
243
+
244
+ export function validateTeamMode(mode) {
245
+ if (!mode) return 'sandbox';
246
+ if (!VALID_TEAM_MODES.includes(mode)) {
247
+ throw new Error(`Invalid team mode: must be one of ${VALID_TEAM_MODES.join(', ')}`);
248
+ }
249
+ return mode;
250
+ }
251
+
242
252
  export function escapeMd(text) {
243
253
  if (!text) return '';
244
254
  // Escape markdown special chars that could break table rendering or inject formatting.