groove-dev 0.27.156 → 0.27.159
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/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/journalist.js +61 -16
- package/node_modules/@groove-dev/daemon/src/process.js +130 -2
- package/node_modules/@groove-dev/daemon/src/rotator.js +2 -1
- package/node_modules/@groove-dev/daemon/src/routes/files.js +28 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +89 -71
- package/node_modules/@groove-dev/gui/dist/assets/index-Bij9o_dc.js +1020 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dzofq3wS.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -2
- package/node_modules/@groove-dev/gui/src/app.css +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +11 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +8 -1
- package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +18 -6
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +122 -17
- package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +69 -38
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +121 -49
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/journalist.js +61 -16
- package/packages/daemon/src/process.js +130 -2
- package/packages/daemon/src/rotator.js +2 -1
- package/packages/daemon/src/routes/files.js +28 -6
- package/packages/daemon/src/tunnel-manager.js +89 -71
- package/packages/gui/dist/assets/index-Bij9o_dc.js +1020 -0
- package/packages/gui/dist/assets/index-Dzofq3wS.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -2
- package/packages/gui/src/app.css +2 -2
- package/packages/gui/src/components/agents/agent-feed.jsx +8 -8
- package/packages/gui/src/components/agents/diff-viewer.jsx +2 -2
- package/packages/gui/src/components/agents/workspace-mode.jsx +11 -2
- package/packages/gui/src/components/dashboard/cache-ring.jsx +2 -2
- package/packages/gui/src/components/dashboard/token-chart.jsx +2 -2
- package/packages/gui/src/components/editor/terminal.jsx +1 -1
- package/packages/gui/src/components/layout/welcome-splash.jsx +8 -1
- package/packages/gui/src/components/network/activity-chart.jsx +4 -4
- package/packages/gui/src/components/network/performance-dashboard.jsx +1 -1
- package/packages/gui/src/components/settings/quick-connect.jsx +18 -6
- package/packages/gui/src/components/settings/ssh-wizard.jsx +122 -17
- package/packages/gui/src/stores/groove.js +9 -1
- package/packages/gui/src/stores/slices/agents-slice.js +69 -38
- package/packages/gui/src/views/memory.jsx +121 -49
- package/ssh/error.png +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/CHANGELOG.md +0 -2
- package/node_modules/@fontsource-variable/jetbrains-mono/LICENSE +0 -93
- package/node_modules/@fontsource-variable/jetbrains-mono/README.md +0 -48
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-ext-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-ext-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-greek-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-greek-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/index.css +0 -59
- package/node_modules/@fontsource-variable/jetbrains-mono/metadata.json +0 -29
- package/node_modules/@fontsource-variable/jetbrains-mono/package.json +0 -47
- package/node_modules/@fontsource-variable/jetbrains-mono/scss/metadata.scss +0 -46
- package/node_modules/@fontsource-variable/jetbrains-mono/scss/mixins.scss +0 -193
- package/node_modules/@fontsource-variable/jetbrains-mono/unicode.json +0 -8
- package/node_modules/@fontsource-variable/jetbrains-mono/wght-italic.css +0 -59
- package/node_modules/@fontsource-variable/jetbrains-mono/wght.css +0 -59
- package/node_modules/@groove-dev/gui/dist/assets/index-COQYX12F.js +0 -1015
- package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/packages/gui/dist/assets/index-COQYX12F.js +0 -1015
- package/packages/gui/dist/assets/index-Diw6wDPU.css +0 -1
- package/packages/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
|
@@ -4,7 +4,7 @@ import { useGrooveStore } from '../stores/groove';
|
|
|
4
4
|
import { Button } from '../components/ui/button';
|
|
5
5
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
6
6
|
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
7
|
-
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical } from 'lucide-react';
|
|
7
|
+
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical, CornerLeftUp } from 'lucide-react';
|
|
8
8
|
|
|
9
9
|
const COMMANDS = [
|
|
10
10
|
{ cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
|
|
@@ -114,7 +114,7 @@ function MemoryCard({ item, onEdit, onDelete }) {
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
117
|
+
function EditorModal({ open, onOpenChange, editing, onSave, onRename }) {
|
|
118
118
|
const [tag, setTag] = useState('');
|
|
119
119
|
const [content, setContent] = useState('');
|
|
120
120
|
const textareaRef = useRef(null);
|
|
@@ -132,9 +132,15 @@ function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
|
132
132
|
}
|
|
133
133
|
}, [open]);
|
|
134
134
|
|
|
135
|
-
const handleSave = () => {
|
|
135
|
+
const handleSave = async () => {
|
|
136
136
|
if (!tag.trim() || editing?.readOnly) return;
|
|
137
|
-
|
|
137
|
+
const originalTag = editing?.tag || '';
|
|
138
|
+
const newTag = tag.trim();
|
|
139
|
+
if (!editing?.isNew && newTag !== originalTag) {
|
|
140
|
+
await onRename(originalTag, newTag, content);
|
|
141
|
+
} else {
|
|
142
|
+
onSave(newTag, content);
|
|
143
|
+
}
|
|
138
144
|
onOpenChange(false);
|
|
139
145
|
};
|
|
140
146
|
|
|
@@ -148,27 +154,30 @@ function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
|
148
154
|
const isNew = editing?.isNew;
|
|
149
155
|
const readOnly = editing?.readOnly;
|
|
150
156
|
const title = readOnly ? `#${editing?.tag || ''}` : isNew ? 'New Memory' : `Edit #${editing?.tag || ''}`;
|
|
157
|
+
const tagChanged = !isNew && tag.trim() !== (editing?.tag || '');
|
|
151
158
|
|
|
152
159
|
return (
|
|
153
160
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
154
161
|
<DialogContent title={title} description="Memory content" className="max-w-2xl">
|
|
155
162
|
<div className="p-5 space-y-4" onKeyDown={handleKeyDown}>
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
<div>
|
|
164
|
+
<label className="block text-xs font-medium text-text-2 mb-1.5">
|
|
165
|
+
{isNew ? 'Tag' : 'Title'}
|
|
166
|
+
{tagChanged && <span className="ml-2 text-2xs text-warning">(will rename)</span>}
|
|
167
|
+
</label>
|
|
168
|
+
<div className="flex items-center gap-1">
|
|
169
|
+
<span className="text-sm text-text-3">#</span>
|
|
170
|
+
<input
|
|
171
|
+
type="text"
|
|
172
|
+
value={tag}
|
|
173
|
+
onChange={(e) => !readOnly && setTag(e.target.value.replace(/[^a-zA-Z0-9/_-]/g, '').toLowerCase())}
|
|
174
|
+
readOnly={readOnly}
|
|
175
|
+
placeholder="project/feature-name"
|
|
176
|
+
className="flex-1 px-2 py-1.5 text-sm font-mono rounded-md bg-surface-0 border border-border text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
177
|
+
/>
|
|
170
178
|
</div>
|
|
171
|
-
|
|
179
|
+
{isNew && <p className="text-2xs text-text-4 mt-1">Use / for hierarchy: groove/memory-system</p>}
|
|
180
|
+
</div>
|
|
172
181
|
<div>
|
|
173
182
|
{!readOnly && <label className="block text-xs font-medium text-text-2 mb-1.5">Content</label>}
|
|
174
183
|
<textarea
|
|
@@ -196,7 +205,7 @@ function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
|
196
205
|
{!readOnly && (
|
|
197
206
|
<Button variant="primary" size="sm" onClick={handleSave} disabled={!tag.trim()}>
|
|
198
207
|
<Save size={14} />
|
|
199
|
-
Save
|
|
208
|
+
{tagChanged ? 'Rename & Save' : 'Save'}
|
|
200
209
|
</Button>
|
|
201
210
|
)}
|
|
202
211
|
</div>
|
|
@@ -249,7 +258,7 @@ function InstructModal({ open, onOpenChange }) {
|
|
|
249
258
|
);
|
|
250
259
|
}
|
|
251
260
|
|
|
252
|
-
function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
261
|
+
function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onEdit, onDelete, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
253
262
|
return (
|
|
254
263
|
<div
|
|
255
264
|
draggable
|
|
@@ -257,19 +266,34 @@ function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onDragStart
|
|
|
257
266
|
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; onDragOver?.(tag); }}
|
|
258
267
|
onDragLeave={() => onDragLeave?.()}
|
|
259
268
|
onDrop={(e) => { e.preventDefault(); onDrop?.(e.dataTransfer.getData('text/plain'), tag); }}
|
|
260
|
-
onClick={() => onSelect({ tag })}
|
|
261
269
|
className={`flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs transition-colors cursor-pointer group ${isDragOver ? 'bg-accent/15 border border-accent/30 border-dashed' : 'hover:bg-surface-2'}`}
|
|
262
270
|
style={indent ? { paddingLeft: `${8 + indent * 16}px` } : undefined}
|
|
263
271
|
>
|
|
264
272
|
<GripVertical size={10} className="text-text-4 opacity-0 group-hover:opacity-50 flex-shrink-0 cursor-grab" />
|
|
265
273
|
<Hash size={11} className="text-text-4 flex-shrink-0" />
|
|
266
|
-
<span className="font-medium text-text-2 truncate">{label}</span>
|
|
274
|
+
<span className="font-medium text-text-2 truncate flex-1" onClick={() => onSelect({ tag })}>{label}</span>
|
|
267
275
|
{isDoc && <Sparkles size={9} className="text-purple flex-shrink-0" />}
|
|
276
|
+
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
|
277
|
+
<button
|
|
278
|
+
onClick={(e) => { e.stopPropagation(); onEdit?.({ tag }); }}
|
|
279
|
+
className="p-1 rounded text-text-4 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
|
|
280
|
+
title="Edit"
|
|
281
|
+
>
|
|
282
|
+
<Pencil size={11} />
|
|
283
|
+
</button>
|
|
284
|
+
<button
|
|
285
|
+
onClick={(e) => { e.stopPropagation(); onDelete?.(tag); }}
|
|
286
|
+
className="p-1 rounded text-text-4 hover:text-danger hover:bg-danger/10 transition-colors cursor-pointer"
|
|
287
|
+
title="Delete"
|
|
288
|
+
>
|
|
289
|
+
<Trash2 size={11} />
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
268
292
|
</div>
|
|
269
293
|
);
|
|
270
294
|
}
|
|
271
295
|
|
|
272
|
-
function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
296
|
+
function TreeGroup({ node, onSelect, onEdit, onDelete, dragOverTag, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
273
297
|
const [expanded, setExpanded] = useState(true);
|
|
274
298
|
const hasChildren = node.children && node.children.length > 0;
|
|
275
299
|
|
|
@@ -278,7 +302,8 @@ function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDra
|
|
|
278
302
|
<TreeItem
|
|
279
303
|
tag={node.tag} label={node.tag} isDoc={node.type === 'doc'}
|
|
280
304
|
isDragOver={dragOverTag === node.tag}
|
|
281
|
-
onSelect={onSelect}
|
|
305
|
+
onSelect={onSelect} onEdit={onEdit} onDelete={onDelete}
|
|
306
|
+
onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
282
307
|
/>
|
|
283
308
|
);
|
|
284
309
|
}
|
|
@@ -305,14 +330,16 @@ function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDra
|
|
|
305
330
|
<TreeItem
|
|
306
331
|
tag={node.tag} label={node.tag} isDoc={node.type === 'doc'} indent={1}
|
|
307
332
|
isDragOver={false}
|
|
308
|
-
onSelect={onSelect}
|
|
333
|
+
onSelect={onSelect} onEdit={onEdit} onDelete={onDelete}
|
|
334
|
+
onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
309
335
|
/>
|
|
310
336
|
)}
|
|
311
337
|
{node.children.map((child) => (
|
|
312
338
|
<TreeItem
|
|
313
339
|
key={child.tag} tag={child.tag} label={child.tag.split('/').pop()} isDoc={child.type === 'doc'} indent={1}
|
|
314
340
|
isDragOver={dragOverTag === child.tag}
|
|
315
|
-
onSelect={onSelect}
|
|
341
|
+
onSelect={onSelect} onEdit={onEdit} onDelete={onDelete}
|
|
342
|
+
onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
316
343
|
/>
|
|
317
344
|
))}
|
|
318
345
|
</div>
|
|
@@ -335,7 +362,7 @@ export default function MemoryView() {
|
|
|
335
362
|
const setKeeperEditing = useGrooveStore((s) => s.setKeeperEditing);
|
|
336
363
|
|
|
337
364
|
const [search, setSearch] = useState('');
|
|
338
|
-
const [viewMode, setViewMode] = useState('
|
|
365
|
+
const [viewMode, setViewMode] = useState('tree');
|
|
339
366
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
340
367
|
const [dragOverTag, setDragOverTag] = useState(null);
|
|
341
368
|
const [draggingTag, setDraggingTag] = useState(null);
|
|
@@ -346,10 +373,23 @@ export default function MemoryView() {
|
|
|
346
373
|
if (keeperEditing) setEditorOpen(true);
|
|
347
374
|
}, [keeperEditing]);
|
|
348
375
|
|
|
349
|
-
const
|
|
350
|
-
|
|
376
|
+
const searchLower = search.toLowerCase();
|
|
377
|
+
const filtered = searchLower
|
|
378
|
+
? keeperItems.filter((item) => item.tag.includes(searchLower))
|
|
351
379
|
: keeperItems;
|
|
352
380
|
|
|
381
|
+
const filteredTree = searchLower
|
|
382
|
+
? keeperTree
|
|
383
|
+
.map((node) => {
|
|
384
|
+
const tagMatch = node.tag.includes(searchLower);
|
|
385
|
+
const matchingChildren = (node.children || []).filter((c) => c.tag.includes(searchLower));
|
|
386
|
+
if (tagMatch) return node;
|
|
387
|
+
if (matchingChildren.length > 0) return { ...node, children: matchingChildren };
|
|
388
|
+
return null;
|
|
389
|
+
})
|
|
390
|
+
.filter(Boolean)
|
|
391
|
+
: keeperTree;
|
|
392
|
+
|
|
353
393
|
const handleNew = () => {
|
|
354
394
|
setKeeperEditing({ tag: '', content: '', isNew: true });
|
|
355
395
|
setEditorOpen(true);
|
|
@@ -370,6 +410,12 @@ export default function MemoryView() {
|
|
|
370
410
|
setKeeperEditing(null);
|
|
371
411
|
};
|
|
372
412
|
|
|
413
|
+
const handleRename = async (oldTag, newTag, content) => {
|
|
414
|
+
await moveKeeperItem(oldTag, newTag);
|
|
415
|
+
await updateKeeperItem(newTag, content);
|
|
416
|
+
setKeeperEditing(null);
|
|
417
|
+
};
|
|
418
|
+
|
|
373
419
|
const handleEditorClose = (open) => {
|
|
374
420
|
setEditorOpen(open);
|
|
375
421
|
if (!open) setKeeperEditing(null);
|
|
@@ -384,10 +430,9 @@ export default function MemoryView() {
|
|
|
384
430
|
setDragOverTag(null);
|
|
385
431
|
setDraggingTag(null);
|
|
386
432
|
if (!sourceTag || !targetTag || sourceTag === targetTag) return;
|
|
387
|
-
// Don't drop onto self or own children
|
|
388
433
|
if (targetTag.startsWith(sourceTag + '/')) return;
|
|
389
434
|
const sourceName = sourceTag.split('/').pop();
|
|
390
|
-
const newTag = targetTag + '/' + sourceName;
|
|
435
|
+
const newTag = targetTag === '__root__' ? sourceName : targetTag + '/' + sourceName;
|
|
391
436
|
if (sourceTag === newTag) return;
|
|
392
437
|
try {
|
|
393
438
|
await moveKeeperItem(sourceTag, newTag);
|
|
@@ -464,24 +509,50 @@ export default function MemoryView() {
|
|
|
464
509
|
</div>
|
|
465
510
|
</div>
|
|
466
511
|
) : viewMode === 'tree' ? (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
512
|
+
filteredTree.length === 0 ? (
|
|
513
|
+
<div className="flex flex-col items-center justify-center h-32 gap-2">
|
|
514
|
+
<Search size={18} className="text-text-4" />
|
|
515
|
+
<p className="text-xs text-text-3">No memories matching “{search}”</p>
|
|
516
|
+
</div>
|
|
517
|
+
) : (
|
|
518
|
+
<div className="p-3 space-y-0.5" onDragOver={(e) => e.preventDefault()}>
|
|
519
|
+
{filteredTree.map((node) => (
|
|
520
|
+
<TreeGroup
|
|
521
|
+
key={node.tag} node={node} onSelect={handleTreeSelect}
|
|
522
|
+
onEdit={handleEdit} onDelete={(tag) => deleteKeeperItem(tag)}
|
|
523
|
+
dragOverTag={dragOverTag}
|
|
524
|
+
onDragStart={(tag) => setDraggingTag(tag)}
|
|
525
|
+
onDragOver={(tag) => { if (tag !== draggingTag) setDragOverTag(tag); }}
|
|
526
|
+
onDragLeave={() => setDragOverTag(null)}
|
|
527
|
+
onDrop={handleDrop}
|
|
528
|
+
/>
|
|
529
|
+
))}
|
|
530
|
+
{draggingTag && draggingTag.includes('/') && (
|
|
531
|
+
<div
|
|
532
|
+
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverTag('__root__'); }}
|
|
533
|
+
onDragLeave={() => setDragOverTag(null)}
|
|
534
|
+
onDrop={(e) => { e.preventDefault(); handleDrop(e.dataTransfer.getData('text/plain'), '__root__'); }}
|
|
535
|
+
className={`flex items-center gap-2 px-3 py-2 mt-2 rounded-md border border-dashed text-xs transition-colors ${dragOverTag === '__root__' ? 'border-accent/50 bg-accent/10 text-accent' : 'border-border text-text-4'}`}
|
|
536
|
+
>
|
|
537
|
+
<CornerLeftUp size={12} />
|
|
538
|
+
<span>Drop here to move to root</span>
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
)
|
|
479
543
|
) : (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
<
|
|
483
|
-
|
|
484
|
-
|
|
544
|
+
filtered.length === 0 ? (
|
|
545
|
+
<div className="flex flex-col items-center justify-center h-32 gap-2">
|
|
546
|
+
<Search size={18} className="text-text-4" />
|
|
547
|
+
<p className="text-xs text-text-3">No memories matching “{search}”</p>
|
|
548
|
+
</div>
|
|
549
|
+
) : (
|
|
550
|
+
<div className="p-3 space-y-2">
|
|
551
|
+
{filtered.map((item) => (
|
|
552
|
+
<MemoryCard key={item.tag} item={item} onEdit={handleEdit} onDelete={(tag) => deleteKeeperItem(tag)} />
|
|
553
|
+
))}
|
|
554
|
+
</div>
|
|
555
|
+
)
|
|
485
556
|
)}
|
|
486
557
|
</ScrollArea>
|
|
487
558
|
|
|
@@ -491,6 +562,7 @@ export default function MemoryView() {
|
|
|
491
562
|
onOpenChange={handleEditorClose}
|
|
492
563
|
editing={keeperEditing}
|
|
493
564
|
onSave={handleSave}
|
|
565
|
+
onRename={handleRename}
|
|
494
566
|
/>
|
|
495
567
|
|
|
496
568
|
{/* Instruct Modal (command reference) */}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.159",
|
|
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)",
|
|
@@ -483,6 +483,9 @@ export class Journalist {
|
|
|
483
483
|
'Be specific. Name files, functions, and line numbers. Do not summarize vaguely.',
|
|
484
484
|
'Preserve the investigation narrative — the next agent needs to understand the',
|
|
485
485
|
'journey, not just the destination.',
|
|
486
|
+
'IMPORTANT: Focus on what the user ASKED the agent to do (the original task).',
|
|
487
|
+
'Do NOT include observations about unrelated code issues, potential improvements,',
|
|
488
|
+
'or things the agent noticed but did not act on. The next agent must stay on task.',
|
|
486
489
|
'Keep your response under 4000 characters.',
|
|
487
490
|
'',
|
|
488
491
|
'---',
|
|
@@ -1024,7 +1027,13 @@ export class Journalist {
|
|
|
1024
1027
|
originalTask ? `## Original Task\n\n${originalTask}\n` : '',
|
|
1025
1028
|
``,
|
|
1026
1029
|
agent.role === 'planner' ? 'CRITICAL: You are a PLANNING ONLY agent. Do NOT implement code. Route all work to your team via .groove/recommended-team.json.\n' : '',
|
|
1027
|
-
|
|
1030
|
+
`## Instructions`,
|
|
1031
|
+
``,
|
|
1032
|
+
`Continue and finish the in-progress task — deliver the output. Stay focused on that specific task only.`,
|
|
1033
|
+
`- Do NOT explore the codebase looking for other things to fix or improve`,
|
|
1034
|
+
`- Do NOT start new work outside the original task scope`,
|
|
1035
|
+
`- Do NOT act on TODO comments, code quality issues, or improvements you notice in passing`,
|
|
1036
|
+
`- If the task is already complete, report what was accomplished and STOP — await new instructions from the user`,
|
|
1028
1037
|
].filter(Boolean).join('\n');
|
|
1029
1038
|
|
|
1030
1039
|
// Hard cap: 16000 chars — investigation context needs room to preserve the full narrative
|
|
@@ -1142,34 +1151,60 @@ export class Journalist {
|
|
|
1142
1151
|
* Build a full context-resume prompt that preserves the conversation
|
|
1143
1152
|
* thread so a fresh agent picks up where the previous session left off.
|
|
1144
1153
|
*/
|
|
1145
|
-
buildConversationResumePrompt(agent, userMessage) {
|
|
1154
|
+
buildConversationResumePrompt(agent, userMessage, { isRotation = false, reason } = {}) {
|
|
1146
1155
|
const thread = this.extractConversationThread(agent);
|
|
1147
1156
|
if (!thread) return null;
|
|
1148
1157
|
|
|
1149
1158
|
const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
|
|
1150
1159
|
const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000, agent.scope, agent.teamId) || '';
|
|
1151
1160
|
|
|
1161
|
+
// Extract the user's original task from the conversation — the first substantial
|
|
1162
|
+
// user message is almost always the task assignment. This anchors the new agent.
|
|
1163
|
+
const originalTask = this._extractOriginalTask(thread);
|
|
1164
|
+
|
|
1165
|
+
// Rotation and idle-resume need very different framing. During rotation the agent
|
|
1166
|
+
// has no new user message — it must continue the exact in-progress task without
|
|
1167
|
+
// drifting. During idle-resume the user explicitly sent a message to continue.
|
|
1168
|
+
const isIdleResume = !isRotation && userMessage && userMessage.trim().length > 0;
|
|
1169
|
+
|
|
1170
|
+
const taskFocusBlock = isRotation ? [
|
|
1171
|
+
`## CRITICAL: Task Focus`,
|
|
1172
|
+
``,
|
|
1173
|
+
`You were auto-rotated (reason: ${reason || 'context_management'}) — this is a routine context refresh, NOT a new assignment.`,
|
|
1174
|
+
originalTask ? `Your task: ${originalTask}` : '',
|
|
1175
|
+
``,
|
|
1176
|
+
`Rules:`,
|
|
1177
|
+
`- Continue ONLY the task described in the conversation below`,
|
|
1178
|
+
`- Do NOT explore the codebase looking for other things to fix or improve`,
|
|
1179
|
+
`- Do NOT start new work that was not part of the original task`,
|
|
1180
|
+
`- Do NOT act on TODO comments, code quality issues, or improvements you notice in passing`,
|
|
1181
|
+
`- If the task is complete, report what was done and STOP — await new instructions from the user`,
|
|
1182
|
+
`- If the task is in progress, pick up exactly where the previous session left off`,
|
|
1183
|
+
``,
|
|
1184
|
+
] : [];
|
|
1185
|
+
|
|
1152
1186
|
let prompt = [
|
|
1153
1187
|
`# Session Context Resume`,
|
|
1154
1188
|
``,
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1189
|
+
isRotation
|
|
1190
|
+
? `You are continuing a session after an automatic context rotation. Your context was refreshed but your task has NOT changed. The conversation below is your previous session — continue the same work.`
|
|
1191
|
+
: `You are continuing a session that went idle. Below is the full conversation from your previous session — your actual exchanges with the user. Pick up exactly where you left off. The user's new message follows at the end.`,
|
|
1158
1192
|
``,
|
|
1159
1193
|
`Role: ${agent.role} | Provider: ${agent.provider} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
|
|
1160
1194
|
agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
|
|
1161
1195
|
``,
|
|
1196
|
+
...taskFocusBlock,
|
|
1162
1197
|
constraints ? `## Project Constraints\n\n${constraints}\n` : '',
|
|
1163
1198
|
discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
|
|
1164
1199
|
`## Previous Conversation\n\n${thread}`,
|
|
1165
1200
|
``,
|
|
1166
1201
|
`---`,
|
|
1167
1202
|
``,
|
|
1168
|
-
`## New Message From User
|
|
1169
|
-
``,
|
|
1170
|
-
userMessage,
|
|
1203
|
+
isIdleResume ? `## New Message From User\n\n${userMessage}` : '',
|
|
1171
1204
|
``,
|
|
1172
|
-
|
|
1205
|
+
isRotation
|
|
1206
|
+
? `Continue the in-progress task from the conversation above. Stay focused on that task only. Do not ask the user to repeat anything. If the task was already completed, state that and wait for new instructions.`
|
|
1207
|
+
: `Continue seamlessly from the conversation above. You have the full context of what was discussed, what was tried, what worked and what didn't. Do not ask the user to repeat anything.`,
|
|
1173
1208
|
].filter(Boolean).join('\n');
|
|
1174
1209
|
|
|
1175
1210
|
// Hard cap at 80K chars (~20K tokens) to leave plenty of room in context window
|
|
@@ -1180,23 +1215,24 @@ export class Journalist {
|
|
|
1180
1215
|
prompt = [
|
|
1181
1216
|
`# Session Context Resume`,
|
|
1182
1217
|
``,
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1218
|
+
isRotation
|
|
1219
|
+
? `You are continuing after an automatic context rotation. Your task has NOT changed.`
|
|
1220
|
+
: `You are continuing a session that went idle (older turns summarized to fit). Pick up exactly where you left off.`,
|
|
1186
1221
|
``,
|
|
1187
1222
|
`Role: ${agent.role} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
|
|
1188
1223
|
agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
|
|
1189
1224
|
``,
|
|
1225
|
+
...taskFocusBlock,
|
|
1190
1226
|
constraints ? `## Project Constraints\n\n${constraints}\n` : '',
|
|
1191
1227
|
`## Previous Conversation\n\n${smallerThread}`,
|
|
1192
1228
|
``,
|
|
1193
1229
|
`---`,
|
|
1194
1230
|
``,
|
|
1195
|
-
`## New Message From User
|
|
1196
|
-
``,
|
|
1197
|
-
userMessage,
|
|
1231
|
+
isIdleResume ? `## New Message From User\n\n${userMessage}` : '',
|
|
1198
1232
|
``,
|
|
1199
|
-
|
|
1233
|
+
isRotation
|
|
1234
|
+
? `Continue the in-progress task only. Do not explore or start new work. If done, state that and wait.`
|
|
1235
|
+
: `Continue seamlessly. Do not ask the user to repeat anything.`,
|
|
1200
1236
|
].filter(Boolean).join('\n');
|
|
1201
1237
|
}
|
|
1202
1238
|
}
|
|
@@ -1204,6 +1240,15 @@ export class Journalist {
|
|
|
1204
1240
|
return prompt;
|
|
1205
1241
|
}
|
|
1206
1242
|
|
|
1243
|
+
_extractOriginalTask(thread) {
|
|
1244
|
+
// Find the first substantial USER message in the thread — that's the task.
|
|
1245
|
+
const match = thread.match(/\[USER\]:\n([\s\S]*?)(?=\n\n---|\n\[CLAUDE\]:|$)/);
|
|
1246
|
+
if (!match) return '';
|
|
1247
|
+
const firstMsg = match[1].trim();
|
|
1248
|
+
if (firstMsg.length < 10) return '';
|
|
1249
|
+
return firstMsg.length > 500 ? firstMsg.slice(0, 500) + '...' : firstMsg;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1207
1252
|
// --- Workspace Grouping ---
|
|
1208
1253
|
|
|
1209
1254
|
/**
|
|
@@ -507,6 +507,7 @@ export class ProcessManager {
|
|
|
507
507
|
|
|
508
508
|
if (finalStatus === 'completed' && agent.role === 'planner') {
|
|
509
509
|
this._extractRecommendedTeam(agent, logPath);
|
|
510
|
+
this._consumeRecommendedTeamAutonomous(agent);
|
|
510
511
|
this._handleReviewComplete(agent);
|
|
511
512
|
}
|
|
512
513
|
|
|
@@ -534,7 +535,7 @@ export class ProcessManager {
|
|
|
534
535
|
|
|
535
536
|
this._checkPhase2(agent.id);
|
|
536
537
|
|
|
537
|
-
if (finalStatus === 'completed' && agent.role === 'fullstack' && agent.teamId) {
|
|
538
|
+
if (finalStatus === 'completed' && agent.role === 'fullstack' && agent.teamId && agent.metadata?.isQcPhase2) {
|
|
538
539
|
this._triggerReview(agent);
|
|
539
540
|
}
|
|
540
541
|
|
|
@@ -1755,6 +1756,114 @@ For normal file edits within your scope, proceed without review.
|
|
|
1755
1756
|
} catch { /* best effort */ }
|
|
1756
1757
|
}
|
|
1757
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Daemon-autonomous consumption of recommended-team.json.
|
|
1761
|
+
* If the file exists and the GUI hasn't already consumed it (no _pendingPhase2
|
|
1762
|
+
* for this team), broadcast a notification so the GUI picks it up on next tick.
|
|
1763
|
+
* This closes the race where GUI polling stops before the file is written.
|
|
1764
|
+
*/
|
|
1765
|
+
_consumeRecommendedTeamAutonomous(agent) {
|
|
1766
|
+
try {
|
|
1767
|
+
const workDir = agent.workingDir || this.daemon.projectDir;
|
|
1768
|
+
const targetPath = resolve(workDir, '.groove', 'recommended-team.json');
|
|
1769
|
+
if (!existsSync(targetPath)) return;
|
|
1770
|
+
|
|
1771
|
+
const teamId = agent.teamId || null;
|
|
1772
|
+
|
|
1773
|
+
// If phase 2 is already pending for this team, GUI already consumed it
|
|
1774
|
+
const pending = this.daemon._pendingPhase2 || [];
|
|
1775
|
+
if (teamId && pending.some(g => g.agents.some(a => a.teamId === teamId))) return;
|
|
1776
|
+
|
|
1777
|
+
// Broadcast so the GUI knows to fetch — even if its polling interval was cleared
|
|
1778
|
+
this.daemon.broadcast({
|
|
1779
|
+
type: 'recommended-team:ready',
|
|
1780
|
+
teamId,
|
|
1781
|
+
agentId: agent.id,
|
|
1782
|
+
agentName: agent.name,
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
// Delayed self-consumption: if the GUI doesn't consume within 5s, daemon does it directly
|
|
1786
|
+
setTimeout(() => {
|
|
1787
|
+
if (!existsSync(targetPath)) return; // GUI consumed it
|
|
1788
|
+
try {
|
|
1789
|
+
const raw = JSON.parse(readFileSync(targetPath, 'utf8'));
|
|
1790
|
+
delete raw._meta;
|
|
1791
|
+
const agentConfigs = Array.isArray(raw) ? raw : (raw.agents || []);
|
|
1792
|
+
if (agentConfigs.length === 0) return;
|
|
1793
|
+
|
|
1794
|
+
const phase1 = agentConfigs.filter(a => !a.phase || a.phase === 1);
|
|
1795
|
+
const phase2 = agentConfigs.filter(a => a.phase === 2);
|
|
1796
|
+
if (phase1.length === 0) return;
|
|
1797
|
+
|
|
1798
|
+
// Check again — GUI may have consumed during the timeout
|
|
1799
|
+
const currentPending = this.daemon._pendingPhase2 || [];
|
|
1800
|
+
if (teamId && currentPending.some(g => g.agents.some(a => a.teamId === teamId))) return;
|
|
1801
|
+
|
|
1802
|
+
const baseDir = agent.workingDir || this.daemon.projectDir;
|
|
1803
|
+
const projectDir = raw.projectDir || null;
|
|
1804
|
+
let projectWorkingDir = baseDir;
|
|
1805
|
+
if (projectDir) {
|
|
1806
|
+
const safeName = String(projectDir).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
|
|
1807
|
+
projectWorkingDir = resolve(baseDir, safeName);
|
|
1808
|
+
mkdirSync(projectWorkingDir, { recursive: true });
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const defaultTeamId = teamId || this.daemon.teams.getDefault()?.id || null;
|
|
1812
|
+
const phase1Ids = [];
|
|
1813
|
+
|
|
1814
|
+
// Spawn phase 1 agents
|
|
1815
|
+
const spawnPromises = phase1.map(async (config) => {
|
|
1816
|
+
try {
|
|
1817
|
+
const validated = validateAgentConfig({
|
|
1818
|
+
role: config.role,
|
|
1819
|
+
scope: config.scope || [],
|
|
1820
|
+
prompt: config.prompt || '',
|
|
1821
|
+
provider: config.provider || agent.provider || this.daemon.config?.defaultProvider,
|
|
1822
|
+
model: config.model || agent.model || this.daemon.config?.defaultModel || 'auto',
|
|
1823
|
+
permission: config.permission || 'auto',
|
|
1824
|
+
workingDir: config.workingDir || projectWorkingDir,
|
|
1825
|
+
name: config.name || undefined,
|
|
1826
|
+
});
|
|
1827
|
+
validated.teamId = defaultTeamId;
|
|
1828
|
+
const spawned = await this.spawn(validated);
|
|
1829
|
+
phase1Ids.push(spawned.id);
|
|
1830
|
+
return spawned;
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
console.error(`[Groove] Autonomous team launch: failed to spawn ${config.role}: ${err.message}`);
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
Promise.all(spawnPromises).then(() => {
|
|
1838
|
+
// Set up phase 2 pending
|
|
1839
|
+
if (phase2.length > 0 && phase1Ids.length > 0) {
|
|
1840
|
+
this.daemon._pendingPhase2 = this.daemon._pendingPhase2 || [];
|
|
1841
|
+
this.daemon._pendingPhase2.push({
|
|
1842
|
+
waitFor: phase1Ids,
|
|
1843
|
+
agents: phase2.map(c => ({
|
|
1844
|
+
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
1845
|
+
provider: c.provider || agent.provider || this.daemon.config?.defaultProvider,
|
|
1846
|
+
model: c.model || agent.model || this.daemon.config?.defaultModel || 'auto',
|
|
1847
|
+
permission: c.permission || 'auto',
|
|
1848
|
+
workingDir: c.workingDir || projectWorkingDir,
|
|
1849
|
+
name: c.name || undefined,
|
|
1850
|
+
teamId: defaultTeamId,
|
|
1851
|
+
})),
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Clean up the file
|
|
1856
|
+
try { unlinkSync(targetPath); } catch { /* */ }
|
|
1857
|
+
this.daemon.audit?.log('team.autonomousLaunch', { teamId: defaultTeamId, phase1: phase1Ids.length, phase2: phase2.length });
|
|
1858
|
+
console.log(`[Groove] Autonomous team launch: ${phase1Ids.length} phase 1 agents spawned for team ${defaultTeamId}`);
|
|
1859
|
+
});
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
console.error(`[Groove] Autonomous team consumption failed: ${err.message}`);
|
|
1862
|
+
}
|
|
1863
|
+
}, 5000);
|
|
1864
|
+
} catch { /* best effort */ }
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1758
1867
|
/**
|
|
1759
1868
|
* Check if a completed/crashed agent was the last phase 1 agent in a team.
|
|
1760
1869
|
* If so, auto-spawn the phase 2 (QC/finisher) agents.
|
|
@@ -1834,13 +1943,26 @@ For normal file edits within your scope, proceed without review.
|
|
|
1834
1943
|
try {
|
|
1835
1944
|
const validated = validateAgentConfig(config);
|
|
1836
1945
|
if (!validated.teamId) validated.teamId = this.daemon.teams.getDefault()?.id || null;
|
|
1946
|
+
validated.metadata = { ...(validated.metadata || {}), isQcPhase2: true };
|
|
1947
|
+
const existingId = existing?.id || null;
|
|
1837
1948
|
const p = this.spawn(validated).then((agent) => {
|
|
1949
|
+
registry.update(agent.id, { metadata: { ...(agent.metadata || {}), isQcPhase2: true } });
|
|
1838
1950
|
this.daemon.broadcast({
|
|
1839
1951
|
type: 'phase2:spawned',
|
|
1840
1952
|
agentId: agent.id,
|
|
1953
|
+
oldAgentId: existingId,
|
|
1841
1954
|
name: agent.name,
|
|
1842
1955
|
role: agent.role,
|
|
1843
1956
|
});
|
|
1957
|
+
if (existingId) {
|
|
1958
|
+
this.daemon.broadcast({
|
|
1959
|
+
type: 'rotation:complete',
|
|
1960
|
+
agentId: agent.id,
|
|
1961
|
+
oldAgentId: existingId,
|
|
1962
|
+
agentName: agent.name,
|
|
1963
|
+
reason: 'phase2_respawn',
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1844
1966
|
this.daemon.audit.log('phase2.autoSpawn', { id: agent.id, name: agent.name, role: agent.role });
|
|
1845
1967
|
}).catch((err) => {
|
|
1846
1968
|
console.error(`[Groove] Phase 2 spawn failed for ${config.role}: ${err.message}`);
|
|
@@ -1884,6 +2006,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1884
2006
|
const teamId = agent.teamId;
|
|
1885
2007
|
if (!teamId) return;
|
|
1886
2008
|
if (this._reviewTriggered.has(teamId)) return;
|
|
2009
|
+
this._reviewTriggered.add(teamId);
|
|
1887
2010
|
|
|
1888
2011
|
const registry = this.daemon.registry;
|
|
1889
2012
|
const teamAgents = registry.getAll().filter(a => a.teamId === teamId);
|
|
@@ -1898,7 +2021,6 @@ For normal file edits within your scope, proceed without review.
|
|
|
1898
2021
|
a.role !== 'planner' && (a.status === 'running' || a.status === 'starting'));
|
|
1899
2022
|
if (hasRunning) return;
|
|
1900
2023
|
|
|
1901
|
-
this._reviewTriggered.add(teamId);
|
|
1902
2024
|
this._reviewPending.add(teamId);
|
|
1903
2025
|
|
|
1904
2026
|
const journalist = this.daemon.journalist;
|
|
@@ -1912,6 +2034,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
1912
2034
|
for (const f of (journalist?.getAgentFiles(a) || [])) allFiles.add(f);
|
|
1913
2035
|
}
|
|
1914
2036
|
|
|
2037
|
+
if (!originalSpec.trim() && !plannerResult.trim() && allFiles.size === 0) {
|
|
2038
|
+
console.log(`[Groove] Review skipped for team ${teamId}: no spec, plan, or files to review`);
|
|
2039
|
+
this._reviewPending.delete(teamId);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1915
2043
|
const reviewPrompt = `You are reviewing a completed team build against the original specification.
|
|
1916
2044
|
|
|
1917
2045
|
## Original Task
|