groove-dev 0.27.175 → 0.27.180

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 (71) hide show
  1. package/AUTONOMOUS_AGENT_FRAMEWORK.md +654 -0
  2. package/innerchat/Screenshot_2026-06-09_at_2.19.42_PM.png +0 -0
  3. package/moe-training/client/envelope-builder.js +22 -5
  4. package/moe-training/shared/constants.js +11 -1
  5. package/moe-training/shared/envelope-schema.js +11 -3
  6. package/moe-training/test/client/envelope-builder.test.js +42 -6
  7. package/moe-training/test/shared/envelope-schema.test.js +11 -4
  8. package/node_modules/@groove-dev/cli/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/package.json +1 -1
  10. package/node_modules/@groove-dev/daemon/src/api.js +3 -1
  11. package/node_modules/@groove-dev/daemon/src/autostate.js +198 -0
  12. package/node_modules/@groove-dev/daemon/src/index.js +6 -0
  13. package/node_modules/@groove-dev/daemon/src/orchestrator.js +585 -0
  14. package/node_modules/@groove-dev/daemon/src/process.js +27 -2
  15. package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
  16. package/node_modules/@groove-dev/daemon/src/routes/auto-agents.js +264 -0
  17. package/node_modules/@groove-dev/daemon/src/validate.js +6 -0
  18. package/node_modules/@groove-dev/gui/dist/assets/index-CTer01Vg.js +1065 -0
  19. package/node_modules/@groove-dev/gui/dist/assets/index-DTFtRtkx.css +1 -0
  20. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  21. package/node_modules/@groove-dev/gui/package.json +1 -1
  22. package/node_modules/@groove-dev/gui/src/App.jsx +2 -0
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +53 -0
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +95 -2
  25. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +50 -1
  26. package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
  27. package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
  28. package/node_modules/@groove-dev/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
  29. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  30. package/node_modules/@groove-dev/gui/src/stores/groove.js +14 -0
  31. package/node_modules/@groove-dev/gui/src/stores/slices/auto-agents-slice.js +149 -0
  32. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +14 -2
  33. package/node_modules/@groove-dev/gui/src/views/auto-agents.jsx +70 -0
  34. package/node_modules/@groove-dev/gui/src/views/settings.jsx +13 -6
  35. package/node_modules/moe-training/client/envelope-builder.js +22 -5
  36. package/node_modules/moe-training/shared/constants.js +11 -1
  37. package/node_modules/moe-training/shared/envelope-schema.js +11 -3
  38. package/node_modules/moe-training/test/client/envelope-builder.test.js +42 -6
  39. package/node_modules/moe-training/test/shared/envelope-schema.test.js +11 -4
  40. package/package.json +1 -1
  41. package/packages/cli/package.json +1 -1
  42. package/packages/daemon/package.json +1 -1
  43. package/packages/daemon/src/api.js +3 -1
  44. package/packages/daemon/src/autostate.js +198 -0
  45. package/packages/daemon/src/index.js +6 -0
  46. package/packages/daemon/src/orchestrator.js +585 -0
  47. package/packages/daemon/src/process.js +27 -2
  48. package/packages/daemon/src/registry.js +2 -1
  49. package/packages/daemon/src/routes/auto-agents.js +264 -0
  50. package/packages/daemon/src/validate.js +6 -0
  51. package/packages/gui/dist/assets/index-CTer01Vg.js +1065 -0
  52. package/packages/gui/dist/assets/index-DTFtRtkx.css +1 -0
  53. package/packages/gui/dist/index.html +2 -2
  54. package/packages/gui/package.json +1 -1
  55. package/packages/gui/src/App.jsx +2 -0
  56. package/packages/gui/src/components/agents/agent-config.jsx +53 -0
  57. package/packages/gui/src/components/agents/agent-panel.jsx +95 -2
  58. package/packages/gui/src/components/agents/spawn-wizard.jsx +50 -1
  59. package/packages/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
  60. package/packages/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
  61. package/packages/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
  62. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  63. package/packages/gui/src/stores/groove.js +14 -0
  64. package/packages/gui/src/stores/slices/auto-agents-slice.js +149 -0
  65. package/packages/gui/src/stores/slices/ui-slice.js +14 -2
  66. package/packages/gui/src/views/auto-agents.jsx +70 -0
  67. package/packages/gui/src/views/settings.jsx +13 -6
  68. package/node_modules/@groove-dev/gui/dist/assets/index-BMaxZVeX.js +0 -1035
  69. package/node_modules/@groove-dev/gui/dist/assets/index-BvJwMNAX.css +0 -1
  70. package/packages/gui/dist/assets/index-BMaxZVeX.js +0 -1035
  71. package/packages/gui/dist/assets/index-BvJwMNAX.css +0 -1
@@ -0,0 +1,284 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useRef, useEffect, useCallback } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { Button } from '../ui/button';
5
+ import { Badge } from '../ui/badge';
6
+ import { Sheet, SheetContent } from '../ui/sheet';
7
+ import { cn } from '../../lib/cn';
8
+ import { api } from '../../lib/api';
9
+ import {
10
+ Bot, Send, ArrowRight, Sparkles, Clock, Shield, FileText,
11
+ ChevronDown, ChevronUp, Edit3, Check, X,
12
+ } from 'lucide-react';
13
+
14
+ const CADENCE_PRESETS = [
15
+ { label: 'Every 15 min', value: '*/15 * * * *' },
16
+ { label: 'Every 30 min', value: '*/30 * * * *' },
17
+ { label: 'Every hour', value: '0 * * * *' },
18
+ { label: 'Every 2 hours', value: '0 */2 * * *' },
19
+ { label: 'Every 6 hours', value: '0 */6 * * *' },
20
+ { label: 'Daily 9 AM', value: '0 9 * * *' },
21
+ { label: 'Weekdays 9 AM', value: '0 9 * * 1-5' },
22
+ ];
23
+
24
+ const SETUP_SYSTEM_PROMPT = `You are a setup assistant for creating autonomous AI agents in Groove.
25
+ Your job is to interview the user about what they want their auto agent to do, then generate a complete configuration.
26
+
27
+ Ask 3-5 focused questions to understand:
28
+ 1. What is the agent's goal/objective?
29
+ 2. What tools, APIs, or commands will it need?
30
+ 3. What should it never do? (guardrails)
31
+ 4. How often should it check in? (cadence)
32
+ 5. What does success look like? (milestones/roadmap)
33
+
34
+ After gathering enough info, generate a JSON config block wrapped in \`\`\`json ... \`\`\` with this exact shape:
35
+ {
36
+ "ready": true,
37
+ "name": "short-kebab-name",
38
+ "description": "One-line description",
39
+ "cadence": "0 * * * *",
40
+ "role": "fullstack",
41
+ "prompt": "Full system prompt for the agent...",
42
+ "roadmap": "## Stage 1: ...\\n## Stage 2: ...",
43
+ "guardrails": ["Never do X", "Always do Y"],
44
+ "maxIterations": null
45
+ }
46
+
47
+ Make the prompt comprehensive — include identity, philosophy, available tools, failure handling, and the never-park directive.
48
+ Make the roadmap have 2-4 stages with clear graduation criteria.
49
+ Keep the conversation friendly and efficient. Ask one question at a time.`;
50
+
51
+ function ChatMessage({ msg }) {
52
+ const isUser = msg.role === 'user';
53
+ return (
54
+ <div className={cn('flex gap-2', isUser ? 'justify-end' : 'justify-start')}>
55
+ {!isUser && (
56
+ <div className="w-6 h-6 rounded-full bg-accent/10 flex items-center justify-center flex-shrink-0 mt-0.5">
57
+ <Sparkles size={12} className="text-accent" />
58
+ </div>
59
+ )}
60
+ <div className={cn(
61
+ 'max-w-[85%] rounded-lg px-3 py-2 text-xs font-sans',
62
+ isUser
63
+ ? 'bg-accent text-surface-0 rounded-br-sm'
64
+ : 'bg-surface-2 text-text-1 rounded-bl-sm',
65
+ )}>
66
+ <p className="whitespace-pre-wrap">{msg.content}</p>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ function ConfigPreview({ config, onDeploy, onEdit, deploying }) {
73
+ const [expanded, setExpanded] = useState(false);
74
+
75
+ return (
76
+ <div className="rounded-lg border border-accent/30 bg-accent/5 p-4 space-y-3">
77
+ <div className="flex items-center gap-2">
78
+ <Bot size={16} className="text-accent" />
79
+ <span className="text-sm font-semibold text-text-0 font-sans">Ready to Deploy</span>
80
+ </div>
81
+
82
+ <div className="grid grid-cols-2 gap-2 text-xs font-sans">
83
+ <div>
84
+ <span className="text-text-4 text-2xs">Name</span>
85
+ <p className="text-text-0">{config.name}</p>
86
+ </div>
87
+ <div>
88
+ <span className="text-text-4 text-2xs">Cadence</span>
89
+ <p className="text-text-0">{CADENCE_PRESETS.find(p => p.value === config.cadence)?.label || config.cadence}</p>
90
+ </div>
91
+ </div>
92
+
93
+ {config.description && (
94
+ <p className="text-xs text-text-2 font-sans">{config.description}</p>
95
+ )}
96
+
97
+ {config.guardrails?.length > 0 && (
98
+ <div>
99
+ <span className="text-2xs text-text-4 font-sans flex items-center gap-1">
100
+ <Shield size={10} /> Guardrails
101
+ </span>
102
+ <ul className="text-2xs text-text-2 font-sans mt-1 space-y-0.5">
103
+ {config.guardrails.map((g, i) => <li key={i}>• {g}</li>)}
104
+ </ul>
105
+ </div>
106
+ )}
107
+
108
+ <button
109
+ onClick={() => setExpanded(!expanded)}
110
+ className="text-2xs text-accent font-sans flex items-center gap-1 cursor-pointer hover:underline"
111
+ >
112
+ {expanded ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
113
+ {expanded ? 'Hide' : 'Show'} full config
114
+ </button>
115
+
116
+ {expanded && (
117
+ <pre className="text-2xs font-mono text-text-3 bg-surface-0 rounded p-2 overflow-x-auto max-h-48 overflow-y-auto border border-border-subtle">
118
+ {JSON.stringify(config, null, 2)}
119
+ </pre>
120
+ )}
121
+
122
+ <div className="flex gap-2 pt-1">
123
+ <Button variant="primary" size="sm" onClick={onDeploy} disabled={deploying} className="flex-1 gap-1.5">
124
+ {deploying ? 'Deploying...' : <><Check size={12} /> Deploy Agent</>}
125
+ </Button>
126
+ <Button variant="outline" size="sm" onClick={onEdit} className="gap-1.5">
127
+ <Edit3 size={12} /> Edit
128
+ </Button>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ export function AutoAgentSetupWizard() {
135
+ const open = useGrooveStore((s) => s.autoAgentWizardOpen);
136
+ const closeWizard = useGrooveStore((s) => s.closeAutoAgentWizard);
137
+ const createAutoAgent = useGrooveStore((s) => s.createAutoAgent);
138
+ const setActiveView = useGrooveStore((s) => s.setActiveView);
139
+
140
+ const [messages, setMessages] = useState([]);
141
+ const [input, setInput] = useState('');
142
+ const [loading, setLoading] = useState(false);
143
+ const [config, setConfig] = useState(null);
144
+ const [deploying, setDeploying] = useState(false);
145
+ const scrollRef = useRef(null);
146
+ const inputRef = useRef(null);
147
+
148
+ useEffect(() => {
149
+ if (open && messages.length === 0) {
150
+ setMessages([{
151
+ role: 'assistant',
152
+ content: "Hey! I'll help you set up an autonomous agent. What task do you want it to handle? Tell me what you're trying to accomplish and I'll help configure everything.",
153
+ }]);
154
+ setConfig(null);
155
+ }
156
+ }, [open]);
157
+
158
+ useEffect(() => {
159
+ if (scrollRef.current) {
160
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
161
+ }
162
+ }, [messages]);
163
+
164
+ const sendMessage = useCallback(async () => {
165
+ if (!input.trim() || loading) return;
166
+ const userMsg = { role: 'user', content: input.trim() };
167
+ const newMessages = [...messages, userMsg];
168
+ setMessages(newMessages);
169
+ setInput('');
170
+ setLoading(true);
171
+
172
+ try {
173
+ const chatHistory = newMessages.map(m => ({ role: m.role, content: m.content }));
174
+ const result = await api.post('/auto-agents/setup-chat', {
175
+ messages: chatHistory,
176
+ systemPrompt: SETUP_SYSTEM_PROMPT,
177
+ });
178
+
179
+ const assistantMsg = { role: 'assistant', content: result.response };
180
+ setMessages(prev => [...prev, assistantMsg]);
181
+
182
+ if (result.config) {
183
+ setConfig(result.config);
184
+ }
185
+ } catch (err) {
186
+ setMessages(prev => [...prev, {
187
+ role: 'assistant',
188
+ content: `Sorry, I hit an error: ${err.message}. Let me try again — could you rephrase that?`,
189
+ }]);
190
+ } finally {
191
+ setLoading(false);
192
+ }
193
+ }, [input, messages, loading]);
194
+
195
+ const handleDeploy = useCallback(async () => {
196
+ if (!config) return;
197
+ setDeploying(true);
198
+ try {
199
+ const result = await createAutoAgent(config);
200
+ if (result) {
201
+ setActiveView('auto-agents');
202
+ }
203
+ } finally {
204
+ setDeploying(false);
205
+ }
206
+ }, [config]);
207
+
208
+ const handleClose = () => {
209
+ closeWizard();
210
+ setMessages([]);
211
+ setConfig(null);
212
+ setInput('');
213
+ };
214
+
215
+ return (
216
+ <Sheet open={open} onOpenChange={(o) => { if (!o) handleClose(); }}>
217
+ <SheetContent title="New Auto Agent" width={520} onClose={handleClose}>
218
+ <div className="flex flex-col h-full">
219
+ {/* Chat area */}
220
+ <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 space-y-3">
221
+ {messages.map((msg, i) => <ChatMessage key={i} msg={msg} />)}
222
+
223
+ {loading && (
224
+ <div className="flex gap-2">
225
+ <div className="w-6 h-6 rounded-full bg-accent/10 flex items-center justify-center flex-shrink-0">
226
+ <Sparkles size={12} className="text-accent animate-pulse" />
227
+ </div>
228
+ <div className="bg-surface-2 rounded-lg px-3 py-2 rounded-bl-sm">
229
+ <span className="text-xs text-text-3 font-sans animate-pulse">Thinking...</span>
230
+ </div>
231
+ </div>
232
+ )}
233
+
234
+ {config && (
235
+ <ConfigPreview
236
+ config={config}
237
+ onDeploy={handleDeploy}
238
+ onEdit={() => setConfig(null)}
239
+ deploying={deploying}
240
+ />
241
+ )}
242
+ </div>
243
+
244
+ {/* Input area */}
245
+ <div className="border-t border-border-subtle px-4 py-3">
246
+ <div className="flex items-center gap-2">
247
+ <input
248
+ ref={inputRef}
249
+ type="text"
250
+ value={input}
251
+ onChange={(e) => setInput(e.target.value)}
252
+ onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }}
253
+ placeholder={config ? 'Ask to adjust the config...' : 'Describe what you want the agent to do...'}
254
+ className="flex-1 bg-surface-0 border border-border-subtle rounded-md px-3 py-2 text-xs text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:border-accent"
255
+ disabled={loading}
256
+ />
257
+ <Button
258
+ variant="primary"
259
+ size="icon-sm"
260
+ onClick={sendMessage}
261
+ disabled={!input.trim() || loading}
262
+ >
263
+ <Send size={14} />
264
+ </Button>
265
+ </div>
266
+ {!config && (
267
+ <div className="flex flex-wrap gap-1.5 mt-2">
268
+ {['ML training pipeline', 'Content generation', 'Data monitoring', 'Code review'].map(s => (
269
+ <button
270
+ key={s}
271
+ onClick={() => { setInput(s); inputRef.current?.focus(); }}
272
+ className="px-2 py-0.5 text-2xs font-sans text-text-3 bg-surface-2 rounded-full hover:bg-surface-3 cursor-pointer transition-colors"
273
+ >
274
+ {s}
275
+ </button>
276
+ ))}
277
+ </div>
278
+ )}
279
+ </div>
280
+ </div>
281
+ </SheetContent>
282
+ </Sheet>
283
+ );
284
+ }
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, BookOpen, LayoutList } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, BookOpen, LayoutList, Bot } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
5
  import { useGrooveStore } from '../../stores/groove';
@@ -12,6 +12,7 @@ const BASE_NAV_ITEMS = [
12
12
  { id: 'editor', icon: Code2, label: 'Editor' },
13
13
  { id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
14
14
  { id: 'memory', icon: BookOpen, label: 'Memory' },
15
+ { id: 'auto-agents', icon: Bot, label: 'Auto Agents' },
15
16
  { id: 'teams', icon: Users, label: 'Teams' },
16
17
  { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
17
18
  { id: 'models', icon: Box, label: 'Models' },
@@ -14,6 +14,7 @@ import { createNetworkSlice } from './slices/network-slice.js';
14
14
  import { createPreviewSlice } from './slices/preview-slice.js';
15
15
  import { createMarketplaceSlice } from './slices/marketplace-slice.js';
16
16
  import { createAutomationsSlice } from './slices/automations-slice.js';
17
+ import { createAutoAgentsSlice } from './slices/auto-agents-slice.js';
17
18
 
18
19
  const WS_URL = `ws://${window.location.hostname}:${window.location.port || 31415}`;
19
20
 
@@ -39,6 +40,7 @@ export const useGrooveStore = create((set, get) => ({
39
40
  ...createPreviewSlice(set, get),
40
41
  ...createMarketplaceSlice(set, get),
41
42
  ...createAutomationsSlice(set, get),
43
+ ...createAutoAgentsSlice(set, get),
42
44
 
43
45
  // ── Connection ────────────────────────────────────────────
44
46
  connected: false,
@@ -654,6 +656,18 @@ export const useGrooveStore = create((set, get) => ({
654
656
  get().fetchAutomations();
655
657
  break;
656
658
 
659
+ case 'auto-agent:created':
660
+ case 'auto-agent:updated':
661
+ case 'auto-agent:deleted':
662
+ case 'auto-agent:paused':
663
+ case 'auto-agent:resumed':
664
+ case 'auto-agent:cycle-complete':
665
+ case 'auto-agent:auto-paused':
666
+ case 'auto-agent:state-updated':
667
+ case 'auto-agent:iteration-started':
668
+ get().fetchAutoAgents();
669
+ break;
670
+
657
671
  case 'gateway:status':
658
672
  set({ gateways: msg.data || [] });
659
673
  break;
@@ -0,0 +1,149 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { api } from '../../lib/api';
4
+
5
+ export const createAutoAgentsSlice = (set, get) => ({
6
+ autoAgents: [],
7
+ autoAgentWizardOpen: false,
8
+ autoAgentWizardStep: 'chat',
9
+ autoAgentSetupMessages: [],
10
+ autoAgentSetupConfig: null,
11
+ selectedAutoAgentId: null,
12
+
13
+ async fetchAutoAgents() {
14
+ try {
15
+ const data = await api.get('/auto-agents');
16
+ set({ autoAgents: Array.isArray(data) ? data : [] });
17
+ } catch { /* ignore */ }
18
+ },
19
+
20
+ async createAutoAgent(config) {
21
+ try {
22
+ const result = await api.post('/auto-agents', config);
23
+ get().addToast('success', 'Auto agent created');
24
+ get().fetchAutoAgents();
25
+ set({ autoAgentWizardOpen: false, autoAgentSetupMessages: [], autoAgentSetupConfig: null });
26
+ return result;
27
+ } catch (err) {
28
+ get().addToast('error', 'Failed to create auto agent', err.message);
29
+ }
30
+ },
31
+
32
+ async updateAutoAgent(id, updates) {
33
+ try {
34
+ await api.patch(`/auto-agents/${encodeURIComponent(id)}`, updates);
35
+ get().fetchAutoAgents();
36
+ } catch (err) {
37
+ get().addToast('error', 'Failed to update', err.message);
38
+ }
39
+ },
40
+
41
+ async deleteAutoAgent(id) {
42
+ try {
43
+ await api.delete(`/auto-agents/${encodeURIComponent(id)}`);
44
+ get().addToast('info', 'Auto agent deleted');
45
+ get().fetchAutoAgents();
46
+ if (get().selectedAutoAgentId === id) set({ selectedAutoAgentId: null });
47
+ } catch (err) {
48
+ get().addToast('error', 'Failed to delete', err.message);
49
+ }
50
+ },
51
+
52
+ async pauseAutoAgent(id) {
53
+ try {
54
+ await api.post(`/auto-agents/${encodeURIComponent(id)}/pause`);
55
+ get().fetchAutoAgents();
56
+ } catch (err) {
57
+ get().addToast('error', 'Failed to pause', err.message);
58
+ }
59
+ },
60
+
61
+ async resumeAutoAgent(id) {
62
+ try {
63
+ await api.post(`/auto-agents/${encodeURIComponent(id)}/resume`);
64
+ get().fetchAutoAgents();
65
+ } catch (err) {
66
+ get().addToast('error', 'Failed to resume', err.message);
67
+ }
68
+ },
69
+
70
+ async triggerAutoAgent(id) {
71
+ try {
72
+ const result = await api.post(`/auto-agents/${encodeURIComponent(id)}/trigger`);
73
+ get().addToast('success', 'Iteration triggered');
74
+ get().fetchAutoAgents();
75
+ return result;
76
+ } catch (err) {
77
+ get().addToast('error', 'Failed to trigger', err.message);
78
+ }
79
+ },
80
+
81
+ async fetchAutoAgentDetail(id) {
82
+ try {
83
+ return await api.get(`/auto-agents/${encodeURIComponent(id)}`);
84
+ } catch { return null; }
85
+ },
86
+
87
+ async fetchAutoAgentJournal(id) {
88
+ try {
89
+ return await api.get(`/auto-agents/${encodeURIComponent(id)}/journal?limit=100`);
90
+ } catch { return []; }
91
+ },
92
+
93
+ async fetchAutoAgentRuns(id) {
94
+ try {
95
+ return await api.get(`/auto-agents/${encodeURIComponent(id)}/runs?limit=30`);
96
+ } catch { return []; }
97
+ },
98
+
99
+ async fetchAutoAgentRoadmap(id) {
100
+ try {
101
+ const res = await fetch(`/api/auto-agents/${encodeURIComponent(id)}/roadmap`);
102
+ return await res.text();
103
+ } catch { return ''; }
104
+ },
105
+
106
+ async fetchAutoAgentPrompt(id) {
107
+ try {
108
+ const res = await fetch(`/api/auto-agents/${encodeURIComponent(id)}/prompt`);
109
+ return await res.text();
110
+ } catch { return ''; }
111
+ },
112
+
113
+ async updateAutoAgentPrompt(id, content) {
114
+ try {
115
+ await api.put(`/auto-agents/${encodeURIComponent(id)}/prompt`, { content });
116
+ get().addToast('success', 'Prompt updated');
117
+ } catch (err) {
118
+ get().addToast('error', 'Failed to update prompt', err.message);
119
+ }
120
+ },
121
+
122
+ async updateAutoAgentRoadmap(id, content) {
123
+ try {
124
+ await api.put(`/auto-agents/${encodeURIComponent(id)}/roadmap`, { content });
125
+ get().addToast('success', 'Roadmap updated');
126
+ } catch (err) {
127
+ get().addToast('error', 'Failed to update roadmap', err.message);
128
+ }
129
+ },
130
+
131
+ openAutoAgentWizard() {
132
+ set({
133
+ autoAgentWizardOpen: true,
134
+ autoAgentWizardStep: 'chat',
135
+ autoAgentSetupMessages: [],
136
+ autoAgentSetupConfig: null,
137
+ });
138
+ },
139
+ closeAutoAgentWizard() {
140
+ set({ autoAgentWizardOpen: false, autoAgentSetupMessages: [], autoAgentSetupConfig: null });
141
+ },
142
+ selectAutoAgent(id) { set({ selectedAutoAgentId: id }); },
143
+
144
+ addAutoAgentSetupMessage(msg) {
145
+ set((s) => ({ autoAgentSetupMessages: [...s.autoAgentSetupMessages, msg] }));
146
+ },
147
+ setAutoAgentSetupConfig(config) { set({ autoAgentSetupConfig: config }); },
148
+ setAutoAgentWizardStep(step) { set({ autoAgentWizardStep: step }); },
149
+ });
@@ -6,7 +6,7 @@ let toastCounter = 0;
6
6
 
7
7
  export const createUiSlice = (set, get) => ({
8
8
  // ── Navigation ────────────────────────────────────────────
9
- activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings'
9
+ activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'auto-agents' | 'settings'
10
10
  detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
11
11
  teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
12
12
  commandPaletteOpen: false,
@@ -91,7 +91,19 @@ export const createUiSlice = (set, get) => ({
91
91
  localStorage.setItem('groove:detailWidth', String(w));
92
92
  },
93
93
  setTerminalVisible(v) {
94
- set({ terminalVisible: v });
94
+ const updates = { terminalVisible: v };
95
+ // In fleet view, opening the terminal auto-opens the agent panel so the
96
+ // terminal is constrained to the left column rather than spanning full width.
97
+ if (v && get().activeView === 'fleet') {
98
+ const selectedId = get().fleetSelectedAgents[0];
99
+ if (selectedId && !get().detailPanel) {
100
+ const tid = get().activeTeamId;
101
+ const panel = { type: 'agent', agentId: selectedId };
102
+ updates.detailPanel = panel;
103
+ updates.teamDetailPanels = { ...get().teamDetailPanels, [tid]: panel };
104
+ }
105
+ }
106
+ set(updates);
95
107
  localStorage.setItem('groove:terminalVisible', String(v));
96
108
  },
97
109
  setTerminalHeight(h) {
@@ -0,0 +1,70 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useEffect } from 'react';
3
+ import { useGrooveStore } from '../stores/groove';
4
+ import { Button } from '../components/ui/button';
5
+ import { AutoAgentCard } from '../components/auto-agents/auto-agent-card';
6
+ import { AutoAgentDetail } from '../components/auto-agents/auto-agent-detail';
7
+ import { AutoAgentSetupWizard } from '../components/auto-agents/setup-wizard';
8
+ import { Plus, Bot } from 'lucide-react';
9
+
10
+ export default function AutoAgentsView() {
11
+ const autoAgents = useGrooveStore((s) => s.autoAgents);
12
+ const fetchAutoAgents = useGrooveStore((s) => s.fetchAutoAgents);
13
+ const openWizard = useGrooveStore((s) => s.openAutoAgentWizard);
14
+ const selectedId = useGrooveStore((s) => s.selectedAutoAgentId);
15
+ const selectAutoAgent = useGrooveStore((s) => s.selectAutoAgent);
16
+
17
+ useEffect(() => {
18
+ fetchAutoAgents();
19
+ const interval = setInterval(fetchAutoAgents, 10000);
20
+ return () => clearInterval(interval);
21
+ }, []);
22
+
23
+ // Detail view
24
+ if (selectedId) {
25
+ return (
26
+ <div className="flex flex-col h-full">
27
+ <AutoAgentDetail agentId={selectedId} onBack={() => selectAutoAgent(null)} />
28
+ <AutoAgentSetupWizard />
29
+ </div>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <div className="flex flex-col h-full">
35
+ {/* Header */}
36
+ <div className="flex items-center justify-between px-5 py-4 border-b border-border-subtle bg-surface-1">
37
+ <h2 className="text-sm font-semibold text-text-0 font-sans uppercase tracking-wide">Auto Agents</h2>
38
+ <Button variant="primary" size="sm" onClick={openWizard} className="gap-1.5">
39
+ <Plus size={12} /> New Auto Agent
40
+ </Button>
41
+ </div>
42
+
43
+ {/* Content */}
44
+ <div className="flex-1 overflow-y-auto">
45
+ {autoAgents.length === 0 ? (
46
+ <div className="flex-1 flex items-center justify-center h-full">
47
+ <div className="text-center space-y-3 py-20">
48
+ <Bot size={28} className="mx-auto text-text-4" />
49
+ <p className="text-xs font-sans text-text-3">No auto agents yet</p>
50
+ <p className="text-2xs font-sans text-text-4 max-w-xs mx-auto">
51
+ Auto agents run autonomously on a schedule — iterating, learning, and making progress on complex tasks 24/7.
52
+ </p>
53
+ <Button variant="primary" size="sm" onClick={openWizard} className="gap-1.5 mt-2">
54
+ <Plus size={12} /> Create Auto Agent
55
+ </Button>
56
+ </div>
57
+ </div>
58
+ ) : (
59
+ <div className="p-4 space-y-3">
60
+ {autoAgents.map((a) => (
61
+ <AutoAgentCard key={a.id} agent={a} />
62
+ ))}
63
+ </div>
64
+ )}
65
+ </div>
66
+
67
+ <AutoAgentSetupWizard />
68
+ </div>
69
+ );
70
+ }
@@ -418,12 +418,19 @@ function ProviderCard({ provider, onKeyChange }) {
418
418
 
419
419
  {/* Connected state */}
420
420
  {provider.hasKey && !settingKey && (
421
- <div className="flex items-center gap-2 mb-3">
422
- <div className="flex-1 flex items-center gap-1.5 h-8 px-2.5 bg-success/8 border border-success/20 rounded-md text-2xs font-sans text-success">
423
- <Check size={10} /> API Connected
421
+ <div className="space-y-1.5 mb-3">
422
+ {isSubscription && isReady && (
423
+ <div className="flex items-center gap-1.5 h-8 px-2.5 bg-accent/8 border border-accent/20 rounded-md text-2xs font-sans text-accent">
424
+ <Check size={10} /> Subscription active
425
+ </div>
426
+ )}
427
+ <div className="flex items-center gap-2">
428
+ <div className="flex-1 flex items-center gap-1.5 h-8 px-2.5 bg-success/8 border border-success/20 rounded-md text-2xs font-sans text-success">
429
+ <Check size={10} /> {isSubscription ? 'API key stored (usage credits)' : 'API Connected'}
430
+ </div>
431
+ <button onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans">Edit</button>
432
+ <button onClick={handleDeleteKey} className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans">Remove</button>
424
433
  </div>
425
- <button onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }} className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans">Edit</button>
426
- <button onClick={handleDeleteKey} className="text-2xs text-text-4 hover:text-danger cursor-pointer font-sans">Remove</button>
427
434
  </div>
428
435
  )}
429
436
 
@@ -492,7 +499,7 @@ function ProviderCard({ provider, onKeyChange }) {
492
499
  className="w-full h-8 text-2xs gap-1.5 mt-2"
493
500
  >
494
501
  <Key size={11} />
495
- Add API key for headless mode
502
+ {provider.id === 'claude-code' ? 'Add API key (usage credits + headless)' : 'Add API key for headless mode'}
496
503
  </Button>
497
504
  )}
498
505
  </div>
@@ -1,7 +1,12 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
 
3
3
  import { randomUUID } from 'node:crypto';
4
- import { CHUNK_SIZE } from '../shared/constants.js';
4
+ import { CHUNK_SIZE, MAX_STEP_CONTENT_CHARS, MAX_TOKEN_COUNT } from '../shared/constants.js';
5
+
6
+ function estimateTokens(text) {
7
+ if (!text) return 0;
8
+ return Math.ceil(text.length / 4);
9
+ }
5
10
 
6
11
  export class EnvelopeBuilder {
7
12
  constructor(sessionId, contributorId, metadata) {
@@ -13,11 +18,23 @@ export class EnvelopeBuilder {
13
18
  }
14
19
 
15
20
  addStep(step) {
16
- if (step.content && typeof step.content === 'string' && step.content.length > 10_000) {
17
- step.content = step.content.slice(0, 10_000);
21
+ // Last-resort trim so the envelope passes ingest validation (an oversized
22
+ // step.content is rejected, which drops the whole session). Parsers already
23
+ // truncate observations to OBSERVATION_TOKEN_LIMIT and flag it; this only
24
+ // fires for content that slipped past them. When it does fire it MUST record
25
+ // the loss — a silent trim looks like complete data to downstream training.
26
+ if (step.content && typeof step.content === 'string' && step.content.length > MAX_STEP_CONTENT_CHARS) {
27
+ const originalTokens = estimateTokens(step.content);
28
+ step.content = step.content.slice(0, MAX_STEP_CONTENT_CHARS);
29
+ step.truncated = true;
30
+ // Preserve the parser's count if it already trimmed — that one reflects
31
+ // the true original size, ours only sees what survived the first pass.
32
+ if (typeof step.original_token_count !== 'number') {
33
+ step.original_token_count = Math.min(originalTokens, MAX_TOKEN_COUNT);
34
+ }
18
35
  }
19
- if (typeof step.token_count === 'number' && step.token_count > 100_000) {
20
- step.token_count = 100_000;
36
+ if (typeof step.token_count === 'number' && step.token_count > MAX_TOKEN_COUNT) {
37
+ step.token_count = MAX_TOKEN_COUNT;
21
38
  }
22
39
  this._buffer.push(step);
23
40
  if (this._buffer.length >= CHUNK_SIZE) {