groove-dev 0.27.119 → 0.27.121

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/moe-training/client/trajectory-capture.js +55 -0
  2. package/moe-training/test/client/trajectory-capture.test.js +63 -0
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/cli/src/commands/start.js +2 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/api.js +30 -10
  7. package/node_modules/@groove-dev/daemon/src/conversations.js +54 -32
  8. package/node_modules/@groove-dev/daemon/src/introducer.js +45 -20
  9. package/node_modules/@groove-dev/daemon/src/process.js +47 -1
  10. package/node_modules/@groove-dev/daemon/src/teams.js +33 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/{index-DT6Jbf_q.css → index-BLd3MON8.css} +1 -1
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-BxPCaxlC.js → index-bmkBX18f.js} +1721 -1721
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +3 -41
  16. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +4 -43
  17. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +8 -10
  18. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +8 -23
  19. package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
  20. package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +7 -57
  21. package/node_modules/@groove-dev/gui/src/stores/groove.js +13 -0
  22. package/node_modules/@groove-dev/gui/src/views/settings.jsx +50 -84
  23. package/node_modules/@groove-dev/gui/src/views/teams.jsx +61 -1
  24. package/node_modules/moe-training/client/trajectory-capture.js +55 -0
  25. package/node_modules/moe-training/test/client/trajectory-capture.test.js +63 -0
  26. package/package.json +1 -1
  27. package/packages/cli/package.json +1 -1
  28. package/packages/cli/src/commands/start.js +2 -1
  29. package/packages/daemon/package.json +1 -1
  30. package/packages/daemon/src/api.js +30 -10
  31. package/packages/daemon/src/conversations.js +54 -32
  32. package/packages/daemon/src/introducer.js +45 -20
  33. package/packages/daemon/src/process.js +47 -1
  34. package/packages/daemon/src/teams.js +33 -0
  35. package/packages/gui/dist/assets/{index-DT6Jbf_q.css → index-BLd3MON8.css} +1 -1
  36. package/packages/gui/dist/assets/{index-BxPCaxlC.js → index-bmkBX18f.js} +1721 -1721
  37. package/packages/gui/dist/index.html +2 -2
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/agent-config.jsx +3 -41
  40. package/packages/gui/src/components/agents/spawn-wizard.jsx +4 -43
  41. package/packages/gui/src/components/layout/status-bar.jsx +8 -10
  42. package/packages/gui/src/components/onboarding/setup-wizard.jsx +8 -23
  43. package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
  44. package/packages/gui/src/components/ui/data-sharing-modal.jsx +7 -57
  45. package/packages/gui/src/stores/groove.js +13 -0
  46. package/packages/gui/src/views/settings.jsx +50 -84
  47. package/packages/gui/src/views/teams.jsx +61 -1
@@ -62,10 +62,8 @@ function ProviderCard({ provider, onKeyChange }) {
62
62
  const [customPathOpen, setCustomPathOpen] = useState(false);
63
63
  const [customPath, setCustomPath] = useState('');
64
64
  const [savingPath, setSavingPath] = useState(false);
65
- const [loginPending, setLoginPending] = useState(false);
66
65
  const addToast = useGrooveStore((s) => s.addToast);
67
66
  const installProgress = useGrooveStore((s) => s.providerInstallProgress[provider.id]);
68
- const loginProvider = useGrooveStore((s) => s.loginProvider);
69
67
  const setProviderPath = useGrooveStore((s) => s.setProviderPath);
70
68
  const verifyProvider = useGrooveStore((s) => s.verifyProvider);
71
69
  const installProvider = useGrooveStore((s) => s.installProvider);
@@ -100,15 +98,6 @@ function ProviderCard({ provider, onKeyChange }) {
100
98
  }
101
99
  }
102
100
 
103
- async function handleLogin(body) {
104
- try {
105
- setLoginPending(true);
106
- await loginProvider(provider.id, body);
107
- } catch {
108
- setLoginPending(false);
109
- }
110
- }
111
-
112
101
  async function handleSavePath() {
113
102
  if (!customPath.trim()) return;
114
103
  setSavingPath(true);
@@ -321,15 +310,30 @@ function ProviderCard({ provider, onKeyChange }) {
321
310
  {effectivelyInstalled && !isReady && !settingKey && !isInstalling && (
322
311
  <div className="flex flex-col gap-3 flex-1">
323
312
  {/* ── Claude Code auth ── */}
324
- {provider.id === 'claude-code' && !loginPending && (
313
+ {provider.id === 'claude-code' && (
325
314
  <>
326
- <div className="space-y-1.5">
327
- <p className="text-xs text-text-1 font-sans font-medium">Sign in with your Claude account</p>
328
- <p className="text-2xs text-text-3 font-sans">A browser window will open where you can sign in with your existing Anthropic account or Claude subscription.</p>
315
+ <div className="space-y-2">
316
+ <p className="text-xs text-text-1 font-sans font-medium">Sign in via terminal</p>
317
+ <div className="space-y-1.5">
318
+ <div className="flex items-start gap-2">
319
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
320
+ <p className="text-2xs text-text-2 font-sans flex items-center gap-1">
321
+ <Terminal size={10} className="text-text-3 flex-shrink-0" />
322
+ Open the Groove terminal below
323
+ </p>
324
+ </div>
325
+ <div className="flex items-start gap-2">
326
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
327
+ <p className="text-2xs text-text-2 font-sans">
328
+ Run: <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">claude</code>
329
+ </p>
330
+ </div>
331
+ <div className="flex items-start gap-2">
332
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
333
+ <p className="text-2xs text-text-2 font-sans">Follow the prompts to sign in with your Anthropic account</p>
334
+ </div>
335
+ </div>
329
336
  </div>
330
- <Button variant="primary" size="sm" onClick={() => handleLogin()} className="w-full h-9 text-xs gap-1.5">
331
- <ExternalLink size={12} /> Sign In
332
- </Button>
333
337
  <button
334
338
  onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
335
339
  className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
@@ -340,15 +344,36 @@ function ProviderCard({ provider, onKeyChange }) {
340
344
  )}
341
345
 
342
346
  {/* ── Codex auth ── */}
343
- {provider.id === 'codex' && !loginPending && (
347
+ {provider.id === 'codex' && (
344
348
  <>
345
- <div className="space-y-1.5">
346
- <p className="text-xs text-text-1 font-sans font-medium">Sign in with your ChatGPT account</p>
347
- <p className="text-2xs text-text-3 font-sans">A browser window will open where you can sign in with your ChatGPT Plus or Teams subscription.</p>
349
+ <div className="space-y-2">
350
+ <p className="text-xs text-text-1 font-sans font-medium">Sign in via terminal</p>
351
+ <div className="space-y-1.5">
352
+ <div className="flex items-start gap-2">
353
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
354
+ <p className="text-2xs text-text-2 font-sans flex items-center gap-1">
355
+ <Terminal size={10} className="text-text-3 flex-shrink-0" />
356
+ Open the Groove terminal below
357
+ </p>
358
+ </div>
359
+ <div className="flex items-start gap-2">
360
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
361
+ <p className="text-2xs text-text-2 font-sans">
362
+ Run: <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">npm i -g @openai/codex</code> (if not installed)
363
+ </p>
364
+ </div>
365
+ <div className="flex items-start gap-2">
366
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
367
+ <p className="text-2xs text-text-2 font-sans">
368
+ Run: <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">codex login</code>
369
+ </p>
370
+ </div>
371
+ <div className="flex items-start gap-2">
372
+ <span className="text-2xs font-bold text-accent font-mono mt-0.5">4</span>
373
+ <p className="text-2xs text-text-2 font-sans">Follow the prompts to authenticate</p>
374
+ </div>
375
+ </div>
348
376
  </div>
349
- <Button variant="primary" size="sm" onClick={() => handleLogin({ method: 'chatgpt-plus' })} className="w-full h-9 text-xs gap-1.5">
350
- <ExternalLink size={12} /> Sign In
351
- </Button>
352
377
  <button
353
378
  onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
354
379
  className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
@@ -442,65 +467,6 @@ function ProviderCard({ provider, onKeyChange }) {
442
467
  </>
443
468
  )}
444
469
 
445
- {/* ── Any provider: login pending state ── */}
446
- {(provider.id === 'claude-code' || provider.id === 'codex') && loginPending && (
447
- <div className="flex flex-col gap-3">
448
- <div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
449
- <Loader2 size={14} className="text-accent animate-spin" />
450
- <div>
451
- <p className="text-xs text-accent font-sans font-medium">Check your browser</p>
452
- <p className="text-2xs text-text-3 font-sans">Complete the sign-in in the browser window that opened.</p>
453
- </div>
454
- </div>
455
- <Button
456
- variant="primary"
457
- size="sm"
458
- disabled={checking}
459
- onClick={async () => {
460
- if (provider.id === 'codex') {
461
- setChecking(true);
462
- try {
463
- const res = await api.post(`/providers/codex/verify`);
464
- if (res.authenticated) {
465
- setLoginPending(false);
466
- if (onKeyChange) onKeyChange();
467
- } else {
468
- addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
469
- }
470
- } catch {
471
- addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
472
- } finally {
473
- setChecking(false);
474
- }
475
- } else {
476
- setChecking(true);
477
- try {
478
- const res = await api.post(`/providers/claude-code/verify`);
479
- if (res.authenticated) {
480
- setLoginPending(false);
481
- if (onKeyChange) onKeyChange();
482
- } else {
483
- addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
484
- }
485
- } catch {
486
- addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
487
- } finally {
488
- setChecking(false);
489
- }
490
- }
491
- }}
492
- className="w-full h-8 text-xs gap-1.5"
493
- >
494
- {checking ? <Loader2 size={12} className="animate-spin" /> : <Check size={12} />} I've signed in
495
- </Button>
496
- <button
497
- onClick={() => setLoginPending(false)}
498
- className="text-2xs text-text-4 hover:text-text-2 cursor-pointer font-sans text-center"
499
- >
500
- Cancel
501
- </button>
502
- </div>
503
- )}
504
470
  </div>
505
471
  )}
506
472
 
@@ -9,10 +9,11 @@ import { api } from '../lib/api';
9
9
  import { useToast } from '../lib/hooks/use-toast';
10
10
  import { fmtNum, fmtDollar, timeAgo, fmtUptime } from '../lib/format';
11
11
  import { cn } from '../lib/cn';
12
+ import { Dialog, DialogContent } from '../components/ui/dialog';
12
13
  import {
13
14
  Clock, CheckCircle, XCircle, AlertTriangle, ShieldCheck, ShieldX,
14
15
  Users, Folder, Cpu, Trash2, Play, Pause, LayoutDashboard, ListChecks, Calendar,
15
- Archive, RotateCcw, ChevronRight,
16
+ Archive, RotateCcw, ChevronRight, ArrowUpCircle,
16
17
  } from 'lucide-react';
17
18
  import { TeamRemovalDialog, PurgeConfirmDialog } from '../components/teams/team-removal-dialog';
18
19
 
@@ -28,9 +29,11 @@ function TeamsDashboard() {
28
29
  const fetchArchivedTeams = useGrooveStore((s) => s.fetchArchivedTeams);
29
30
  const restoreTeam = useGrooveStore((s) => s.restoreTeam);
30
31
  const purgeTeam = useGrooveStore((s) => s.purgeTeam);
32
+ const promoteTeam = useGrooveStore((s) => s.promoteTeam);
31
33
 
32
34
  const [archiveConfirm, setArchiveConfirm] = useState(null);
33
35
  const [purgeConfirm, setPurgeConfirm] = useState(null);
36
+ const [promoteConfirm, setPromoteConfirm] = useState(null);
34
37
  const [archivedOpen, setArchivedOpen] = useState(false);
35
38
 
36
39
  useEffect(() => { fetchArchivedTeams(); }, []);
@@ -84,6 +87,15 @@ function TeamsDashboard() {
84
87
  </div>
85
88
  )}
86
89
  </div>
90
+ {team.mode !== 'production' && (
91
+ <button
92
+ onClick={() => setPromoteConfirm(team)}
93
+ className="p-1.5 text-text-4 hover:text-success rounded transition-colors cursor-pointer"
94
+ title="Promote to production"
95
+ >
96
+ <ArrowUpCircle size={13} />
97
+ </button>
98
+ )}
87
99
  <button
88
100
  onClick={() => {
89
101
  if (teamAgents.some((a) => a.status === 'running' || a.status === 'starting')) {
@@ -195,10 +207,58 @@ function TeamsDashboard() {
195
207
  onOpenChange={(open) => !open && setPurgeConfirm(null)}
196
208
  onPurge={purgeTeam}
197
209
  />
210
+
211
+ <PromoteConfirmDialog
212
+ team={promoteConfirm}
213
+ open={!!promoteConfirm}
214
+ onOpenChange={(open) => !open && setPromoteConfirm(null)}
215
+ onPromote={promoteTeam}
216
+ />
198
217
  </div>
199
218
  );
200
219
  }
201
220
 
221
+ function PromoteConfirmDialog({ team, open, onOpenChange, onPromote }) {
222
+ const [promoting, setPromoting] = useState(false);
223
+
224
+ useEffect(() => {
225
+ if (!open) setPromoting(false);
226
+ }, [open]);
227
+
228
+ async function handleConfirm() {
229
+ setPromoting(true);
230
+ try {
231
+ await onPromote(team?.id);
232
+ onOpenChange(false);
233
+ } catch {
234
+ setPromoting(false);
235
+ }
236
+ }
237
+
238
+ return (
239
+ <Dialog open={open} onOpenChange={onOpenChange}>
240
+ <DialogContent title="Promote to Production" description="Promote this team to production mode">
241
+ <div className="px-5 py-4 space-y-3">
242
+ <p className="text-sm text-text-1 font-sans">
243
+ Promote <span className="font-semibold text-text-0">{team?.name}</span> to production?
244
+ </p>
245
+ <p className="text-xs text-text-3 font-sans">
246
+ This will move files from the team directory into the project directory.
247
+ The team will switch to production mode and files will persist when the team is removed.
248
+ </p>
249
+ </div>
250
+ <div className="px-5 py-3 border-t border-border-subtle flex justify-end gap-2">
251
+ <Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>Cancel</Button>
252
+ <Button variant="primary" size="sm" disabled={promoting} onClick={handleConfirm} className="gap-1.5">
253
+ <ArrowUpCircle size={12} />
254
+ {promoting ? 'Promoting...' : 'Promote'}
255
+ </Button>
256
+ </div>
257
+ </DialogContent>
258
+ </Dialog>
259
+ );
260
+ }
261
+
202
262
  function Stat({ label, value, color }) {
203
263
  return (
204
264
  <div className="text-center">
@@ -128,6 +128,61 @@ export class TrajectoryCapture {
128
128
  await this._attestation.openSession(sessionId, metadata);
129
129
  }
130
130
 
131
+ onChatTurnStart(conversationId, provider, model, message) {
132
+ if (!this._enabled) return null;
133
+
134
+ const agentId = `chat-api-${conversationId}-${Date.now()}`;
135
+ const sessionId = `sess_${randomUUID()}`;
136
+ const contributorId = ConsentManager.getOrCreateUserId();
137
+ const metadata = {
138
+ model_engine: model,
139
+ provider,
140
+ agent_role: 'chat',
141
+ agent_id: agentId,
142
+ task_complexity: 'medium',
143
+ team_size: 1,
144
+ session_quality: 0,
145
+ groove_version: this._grooveVersion,
146
+ leaf_context: null,
147
+ };
148
+
149
+ const builder = new EnvelopeBuilder(sessionId, contributorId, metadata);
150
+ const classifier = new StepClassifier();
151
+
152
+ const ctx = {
153
+ sessionId,
154
+ parser: null,
155
+ builder,
156
+ classifier,
157
+ metadata,
158
+ stepCount: 0,
159
+ chunkCount: 0,
160
+ totalTokens: 0,
161
+ errorsEncountered: 0,
162
+ errorsRecovered: 0,
163
+ filesModified: 0,
164
+ coordinationEvents: 0,
165
+ startTime: Date.now(),
166
+ chunkTimer: null,
167
+ allSteps: [],
168
+ revisionRounds: 0,
169
+ };
170
+
171
+ this._contexts.set(agentId, ctx);
172
+
173
+ if (message && typeof message === 'string' && message.trim()) {
174
+ this._processStep(agentId, ctx, {
175
+ type: 'instruction',
176
+ content: message.slice(0, USER_MESSAGE_MAX_CHARS),
177
+ source: 'user',
178
+ });
179
+ }
180
+
181
+ this._attestation.openSession(sessionId, metadata).catch(() => {});
182
+
183
+ return agentId;
184
+ }
185
+
131
186
  onStdoutLine(agentId, jsonLine) {
132
187
  if (!this._enabled) return;
133
188
  const ctx = this._contexts.get(agentId);
@@ -354,6 +354,69 @@ describe('TrajectoryCapture — planner/conversational eligibility', () => {
354
354
  });
355
355
  });
356
356
 
357
+ describe('TrajectoryCapture — API chat capture via onChatTurnStart', () => {
358
+ function makeChatTc() {
359
+ const tc = makeTc();
360
+ tc._enabled = true;
361
+ tc._scrubber = { scrub: (s) => s };
362
+ tc._attestation = { openSession: async () => {}, closeSession: async () => {}, signEnvelope: (sid, e) => e };
363
+ tc._transmissionQueue = { enqueue: () => {}, waitForDrain: async () => {} };
364
+ tc._domainTagger = null;
365
+ return tc;
366
+ }
367
+
368
+ it('returns a synthetic agent ID and creates context', () => {
369
+ const tc = makeChatTc();
370
+ const agentId = tc.onChatTurnStart('conv-123', 'claude-code', 'opus', 'What is React?');
371
+ assert.ok(agentId);
372
+ assert.ok(agentId.startsWith('chat-api-conv-123-'));
373
+ const ctx = tc._contexts.get(agentId);
374
+ assert.ok(ctx);
375
+ assert.equal(ctx.metadata.agent_role, 'chat');
376
+ assert.equal(ctx.metadata.provider, 'claude-code');
377
+ assert.equal(ctx.metadata.model_engine, 'opus');
378
+ });
379
+
380
+ it('records the user message as an instruction step', () => {
381
+ const tc = makeChatTc();
382
+ const agentId = tc.onChatTurnStart('conv-456', 'claude-code', 'opus', 'Explain hooks');
383
+ const ctx = tc._contexts.get(agentId);
384
+ assert.equal(ctx.stepCount, 1);
385
+ assert.equal(ctx.allSteps[0].type, 'instruction');
386
+ assert.ok(ctx.allSteps[0].content.includes('Explain hooks'));
387
+ });
388
+
389
+ it('works with onParsedOutput and onAgentComplete', async () => {
390
+ const tc = makeChatTc();
391
+ const agentId = tc.onChatTurnStart('conv-789', 'claude-code', 'opus', 'Tell me about React');
392
+
393
+ tc.onParsedOutput(agentId, { type: 'activity', subtype: 'assistant', data: 'React is a UI library' });
394
+ tc.onParsedOutput(agentId, { type: 'result', data: 'React is a UI library' });
395
+
396
+ const ctx = tc._contexts.get(agentId);
397
+ assert.equal(ctx.stepCount, 3);
398
+ assert.equal(ctx.allSteps[1].type, 'thought');
399
+ assert.equal(ctx.allSteps[2].type, 'resolution');
400
+
401
+ await tc.onAgentComplete(agentId, { status: 'SUCCESS' });
402
+ assert.equal(tc._contexts.has(agentId), false);
403
+ });
404
+
405
+ it('returns null when disabled', () => {
406
+ const tc = makeChatTc();
407
+ tc._enabled = false;
408
+ const agentId = tc.onChatTurnStart('conv-000', 'claude-code', 'opus', 'Hello');
409
+ assert.equal(agentId, null);
410
+ });
411
+
412
+ it('context has no parser (not needed for API chat)', () => {
413
+ const tc = makeChatTc();
414
+ const agentId = tc.onChatTurnStart('conv-nop', 'claude-code', 'opus', 'Hello');
415
+ const ctx = tc._contexts.get(agentId);
416
+ assert.equal(ctx.parser, null);
417
+ });
418
+ });
419
+
357
420
  describe('TrajectoryCapture — initial prompt capture', () => {
358
421
  function makeSpawnTc() {
359
422
  const tc = makeTc();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.119",
3
+ "version": "0.27.121",
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.119",
3
+ "version": "0.27.121",
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",
@@ -3,12 +3,13 @@
3
3
 
4
4
  import { existsSync } from 'fs';
5
5
  import { resolve } from 'path';
6
+ import { homedir } from 'os';
6
7
  import { Daemon } from '@groove-dev/daemon';
7
8
  import chalk from 'chalk';
8
9
  import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
9
10
 
10
11
  export async function start(options) {
11
- const grooveDir = process.env.GROOVE_DIR || resolve(process.cwd(), '.groove');
12
+ const grooveDir = process.env.GROOVE_DIR || resolve(homedir(), '.groove');
12
13
  const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
13
14
 
14
15
  // ── First-run interactive wizard ────────────────────────────
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.119",
3
+ "version": "0.27.121",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1147,6 +1147,16 @@ export function createApi(app, daemon) {
1147
1147
  }
1148
1148
  });
1149
1149
 
1150
+ app.post('/api/teams/:id/promote', (req, res) => {
1151
+ try {
1152
+ const team = daemon.teams.promote(req.params.id);
1153
+ daemon.audit.log('team.promote', { id: team.id, name: team.name });
1154
+ res.json(team);
1155
+ } catch (err) {
1156
+ res.status(400).json({ error: err.message });
1157
+ }
1158
+ });
1159
+
1150
1160
  // --- Conversations ---
1151
1161
 
1152
1162
  app.get('/api/conversations', (req, res) => {
@@ -2957,8 +2967,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
2957
2967
 
2958
2968
  try {
2959
2969
  const stat = statSync(result.fullPath);
2960
- if (stat.size > 5 * 1024 * 1024) {
2961
- return res.status(400).json({ error: 'File too large (>5MB)' });
2970
+ if (stat.size > 50 * 1024 * 1024) {
2971
+ return res.status(400).json({ error: 'File too large (>50MB)' });
2962
2972
  }
2963
2973
 
2964
2974
  // Binary detection: check first 8KB for null bytes
@@ -2990,8 +3000,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
2990
3000
  if (typeof content !== 'string') {
2991
3001
  return res.status(400).json({ error: 'content must be a string' });
2992
3002
  }
2993
- if (content.length > 5 * 1024 * 1024) {
2994
- return res.status(400).json({ error: 'Content too large (>5MB)' });
3003
+ if (content.length > 50 * 1024 * 1024) {
3004
+ return res.status(400).json({ error: 'Content too large (>50MB)' });
2995
3005
  }
2996
3006
 
2997
3007
  try {
@@ -3435,6 +3445,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3435
3445
  const plannerAgent = found.agentId ? daemon.registry.get(found.agentId) : null;
3436
3446
  const baseDir = plannerAgent?.workingDir || daemon.config?.defaultWorkingDir || daemon.projectDir;
3437
3447
  const plannerProvider = plannerAgent?.provider || undefined;
3448
+ const plannerModel = plannerAgent?.model || undefined;
3438
3449
 
3439
3450
  // Use the planner's teamId so launched agents join the correct team.
3440
3451
  // Priority: explicit from frontend > agent that wrote the file > most recent planner > default
@@ -3477,6 +3488,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
3477
3488
  });
3478
3489
  }
3479
3490
 
3491
+ function resolveProviderAndModel(cfgProvider, cfgModel, fallbackProvider, fallbackModel) {
3492
+ const provider = cfgProvider || plannerProvider || daemon.config?.defaultProvider || fallbackProvider || undefined;
3493
+ if (cfgModel) return { provider, model: cfgModel };
3494
+ if (!cfgProvider && plannerProvider && plannerProvider !== daemon.config?.defaultProvider) {
3495
+ return { provider, model: plannerModel || 'auto' };
3496
+ }
3497
+ return { provider, model: daemon.config?.defaultModel || fallbackModel || 'auto' };
3498
+ }
3499
+
3480
3500
  // Team-level overrides from the pre-planner config panel
3481
3501
  const teamProvider = req.body?.teamProvider || undefined;
3482
3502
  const teamModel = req.body?.teamModel || undefined;
@@ -3500,10 +3520,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
3500
3520
 
3501
3521
  // Safety net: if planner forgot the QC agent, auto-add one
3502
3522
  if (phase2.length === 0 && phase1.length >= 2) {
3523
+ const { provider: qcProvider, model: qcModel } = resolveProviderAndModel(teamProvider, teamModel);
3503
3524
  phase2 = [{
3504
3525
  name: 'qc-agent',
3505
3526
  role: 'fullstack', phase: 2, scope: [],
3506
- provider: teamProvider || plannerProvider || daemon.config?.defaultProvider || undefined,
3527
+ provider: qcProvider,
3528
+ model: qcModel,
3507
3529
  prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
3508
3530
  }];
3509
3531
  }
@@ -3556,8 +3578,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3556
3578
  role: existing.role,
3557
3579
  scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
3558
3580
  prompt,
3559
- provider: config.provider || plannerProvider || daemon.config?.defaultProvider || existing.provider || undefined,
3560
- model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
3581
+ ...resolveProviderAndModel(config.provider, config.model, existing.provider, existing.model),
3561
3582
  permission: config.permission || existing.permission || 'auto',
3562
3583
  workingDir: existing.workingDir || projectWorkingDir,
3563
3584
  name: existing.name,
@@ -3581,8 +3602,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3581
3602
  role: config.role,
3582
3603
  scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
3583
3604
  prompt,
3584
- provider: config.provider || plannerProvider || daemon.config?.defaultProvider || undefined,
3585
- model: config.model || daemon.config?.defaultModel || 'auto',
3605
+ ...resolveProviderAndModel(config.provider, config.model),
3586
3606
  permission: config.permission || 'auto',
3587
3607
  workingDir: config.workingDir || projectWorkingDir,
3588
3608
  name: config.name || undefined,
@@ -3624,7 +3644,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3624
3644
  waitFor: phase1Ids,
3625
3645
  agents: phase2.map((c) => ({
3626
3646
  role: c.role, scope: c.scope || [], prompt: c.prompt || '',
3627
- provider: c.provider || plannerProvider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
3647
+ ...resolveProviderAndModel(c.provider, c.model),
3628
3648
  permission: c.permission || 'auto',
3629
3649
  reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
3630
3650
  workingDir: c.workingDir || projectWorkingDir,