groove-dev 0.27.107 → 0.27.108
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/TRAINING_DATA.md +9 -0
- package/moe-training/client/envelope-builder.js +5 -0
- package/moe-training/client/scrubber.js +1 -1
- package/moe-training/client/step-classifier.js +22 -6
- package/moe-training/client/trajectory-capture.js +15 -4
- package/moe-training/shared/constants.js +2 -0
- package/moe-training/shared/envelope-schema.js +1 -1
- package/moe-training/test/client/envelope-builder.test.js +32 -0
- package/moe-training/test/client/scrubber.test.js +13 -0
- package/moe-training/test/client/step-classifier.test.js +96 -7
- package/moe-training/test/client/trajectory-capture.test.js +53 -6
- 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/api.js +29 -2
- package/node_modules/@groove-dev/daemon/src/process.js +5 -5
- package/node_modules/@groove-dev/gui/dist/assets/{index-DkAGIluW.js → index-CEgtSfbG.js} +1748 -1745
- package/node_modules/@groove-dev/gui/dist/assets/{index-QwgLRN8B.css → index-_3cJS_UG.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +9 -3
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +14 -2
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +9 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +10 -0
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +56 -15
- package/node_modules/moe-training/client/envelope-builder.js +5 -0
- package/node_modules/moe-training/client/scrubber.js +1 -1
- package/node_modules/moe-training/client/step-classifier.js +22 -6
- package/node_modules/moe-training/client/trajectory-capture.js +15 -4
- package/node_modules/moe-training/shared/constants.js +2 -0
- package/node_modules/moe-training/shared/envelope-schema.js +1 -1
- package/node_modules/moe-training/test/client/envelope-builder.test.js +32 -0
- package/node_modules/moe-training/test/client/scrubber.test.js +13 -0
- package/node_modules/moe-training/test/client/step-classifier.test.js +96 -7
- package/node_modules/moe-training/test/client/trajectory-capture.test.js +53 -6
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +29 -2
- package/packages/daemon/src/process.js +5 -5
- package/packages/gui/dist/assets/{index-DkAGIluW.js → index-CEgtSfbG.js} +1748 -1745
- package/packages/gui/dist/assets/{index-QwgLRN8B.css → index-_3cJS_UG.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/layout/command-palette.jsx +2 -1
- package/packages/gui/src/components/layout/status-bar.jsx +9 -3
- package/packages/gui/src/components/settings/federation-panel.jsx +2 -2
- package/packages/gui/src/components/settings/federation-peers.jsx +14 -2
- package/packages/gui/src/components/settings/quick-connect.jsx +9 -0
- package/packages/gui/src/stores/groove.js +10 -0
- package/packages/gui/src/views/federation.jsx +56 -15
- package/ssh/main.js +2253 -0
|
@@ -4,7 +4,7 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
4
4
|
import {
|
|
5
5
|
Network, Code2, ChartSpline, Puzzle, Users, Plus,
|
|
6
6
|
RotateCw, Skull, MessageSquare, Terminal, Newspaper,
|
|
7
|
-
Search, Radio, ExternalLink, FolderOpen, Globe,
|
|
7
|
+
Search, Radio, ExternalLink, FolderOpen, Globe, Shield,
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { cn } from '../../lib/cn';
|
|
10
10
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
@@ -16,6 +16,7 @@ const STATIC_COMMANDS = [
|
|
|
16
16
|
{ id: 'nav:marketplace', label: 'Go to Marketplace', icon: Puzzle, category: 'Navigation', action: (s) => { s.setActiveView('marketplace'); } },
|
|
17
17
|
{ id: 'nav:teams', label: 'Go to Teams', icon: Users, category: 'Navigation', action: (s) => { s.setActiveView('teams'); } },
|
|
18
18
|
{ id: 'nav:federation', label: 'Go to Federation', icon: Globe, category: 'Navigation', action: (s) => { s.setActiveView('federation'); } },
|
|
19
|
+
{ id: 'action:whitelist', label: 'Add to Federation Whitelist', icon: Shield, category: 'Federation', action: (s) => { s.setActiveView('federation'); } },
|
|
19
20
|
{ id: 'action:spawn', label: 'Spawn Agent', icon: Plus, category: 'Actions', action: (s) => { s.openDetail({ type: 'spawn' }); } },
|
|
20
21
|
{ id: 'action:terminal', label: 'Toggle Terminal', icon: Terminal, category: 'Actions', action: (s) => { s.setTerminalVisible(!s.terminalVisible); }, shortcut: 'Cmd+J' },
|
|
21
22
|
{ id: 'action:journalist', label: 'Toggle Journalist', icon: Newspaper, category: 'Actions', action: (s) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug } from 'lucide-react';
|
|
2
|
+
import { Terminal, BookOpen, Radio, Plug, Globe, ArrowUpCircle, X, Unplug, Shield } from 'lucide-react';
|
|
3
3
|
import { cn } from '../../lib/cn';
|
|
4
4
|
import { StatusDot } from '../ui/status-dot';
|
|
5
5
|
import { Badge } from '../ui/badge';
|
|
@@ -22,7 +22,6 @@ export function StatusBar({
|
|
|
22
22
|
const updateReady = useGrooveStore((s) => s.updateReady);
|
|
23
23
|
const updateProgress = useGrooveStore((s) => s.updateProgress);
|
|
24
24
|
const setUpdateModalOpen = useGrooveStore((s) => s.setUpdateModalOpen);
|
|
25
|
-
const subscription = useGrooveStore((s) => s.subscription);
|
|
26
25
|
const navigate = useGrooveStore((s) => s.setActiveView);
|
|
27
26
|
const activeTunnel = savedTunnels.find((t) => t.active);
|
|
28
27
|
const electron = isElectron();
|
|
@@ -71,6 +70,13 @@ export function StatusBar({
|
|
|
71
70
|
<span className="text-text-4">{activeTunnel.latencyMs}ms</span>
|
|
72
71
|
)}
|
|
73
72
|
</button>
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => useGrooveStore.getState().addToWhitelist(activeTunnel.host)}
|
|
75
|
+
className="p-0.5 text-text-4 hover:text-accent cursor-pointer transition-colors rounded"
|
|
76
|
+
title="Add to Federation Whitelist"
|
|
77
|
+
>
|
|
78
|
+
<Shield size={10} />
|
|
79
|
+
</button>
|
|
74
80
|
<button
|
|
75
81
|
onClick={() => useGrooveStore.getState().disconnectTunnel(activeTunnel.id)}
|
|
76
82
|
className="p-0.5 text-text-4 hover:text-danger cursor-pointer transition-colors rounded"
|
|
@@ -98,7 +104,7 @@ export function StatusBar({
|
|
|
98
104
|
<span>Connect</span>
|
|
99
105
|
</button>
|
|
100
106
|
)}
|
|
101
|
-
{
|
|
107
|
+
{connected && (
|
|
102
108
|
<button
|
|
103
109
|
onClick={() => navigate('federation')}
|
|
104
110
|
className="flex items-center gap-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors"
|
|
@@ -22,7 +22,7 @@ function statusBadge(status) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function WhitelistTab() {
|
|
25
|
+
export function WhitelistTab() {
|
|
26
26
|
const whitelist = useGrooveStore((s) => s.federation.whitelist);
|
|
27
27
|
const addToWhitelist = useGrooveStore((s) => s.addToWhitelist);
|
|
28
28
|
const removeFromWhitelist = useGrooveStore((s) => s.removeFromWhitelist);
|
|
@@ -198,7 +198,7 @@ function WhitelistTab() {
|
|
|
198
198
|
);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
function AmbassadorsTab() {
|
|
201
|
+
export function AmbassadorsTab() {
|
|
202
202
|
const ambassadors = useGrooveStore((s) => s.federation.ambassadors);
|
|
203
203
|
|
|
204
204
|
return (
|
|
@@ -22,6 +22,8 @@ export function FederationPeers({ onOpenWizard }) {
|
|
|
22
22
|
const connections = useGrooveStore((s) => s.federation.connections);
|
|
23
23
|
const peers = useGrooveStore((s) => s.federation.peers);
|
|
24
24
|
const sendPouch = useGrooveStore((s) => s.sendPouch);
|
|
25
|
+
const disconnectPeer = useGrooveStore((s) => s.disconnectPeer);
|
|
26
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
25
27
|
const [sendingTo, setSendingTo] = useState(null);
|
|
26
28
|
|
|
27
29
|
const allPeers = peers.length > 0 ? peers : connections;
|
|
@@ -97,7 +99,12 @@ export function FederationPeers({ onOpenWizard }) {
|
|
|
97
99
|
</div>
|
|
98
100
|
|
|
99
101
|
<div className="flex items-center gap-1.5 mt-3 pt-2.5 border-t border-border-subtle">
|
|
100
|
-
<Button
|
|
102
|
+
<Button
|
|
103
|
+
size="sm"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
className="h-6 text-2xs gap-1 text-text-2"
|
|
106
|
+
onClick={() => addToast('info', name, `ID: ${id}\nIP: ${ip}${peer.port ? `:${peer.port}` : ''}\nLatency: ${latency != null ? `${latency}ms` : 'N/A'}\nState: ${state}`)}
|
|
107
|
+
>
|
|
101
108
|
<Eye size={10} />
|
|
102
109
|
Details
|
|
103
110
|
</Button>
|
|
@@ -111,7 +118,12 @@ export function FederationPeers({ onOpenWizard }) {
|
|
|
111
118
|
<Send size={10} />
|
|
112
119
|
Send Pouch
|
|
113
120
|
</Button>
|
|
114
|
-
<Button
|
|
121
|
+
<Button
|
|
122
|
+
size="sm"
|
|
123
|
+
variant="ghost"
|
|
124
|
+
className="h-6 text-2xs gap-1 text-danger/70 hover:text-danger ml-auto"
|
|
125
|
+
onClick={() => disconnectPeer(id)}
|
|
126
|
+
>
|
|
115
127
|
<Unplug size={10} />
|
|
116
128
|
Disconnect
|
|
117
129
|
</Button>
|
|
@@ -25,6 +25,15 @@ export function QuickConnect() {
|
|
|
25
25
|
setConnectingId(id);
|
|
26
26
|
try {
|
|
27
27
|
await useGrooveStore.getState().connectTunnel(id);
|
|
28
|
+
const tunnel = savedTunnels.find((t) => t.id === id);
|
|
29
|
+
if (tunnel?.host) {
|
|
30
|
+
addToast('info', `Add ${tunnel.host} to Federation Whitelist?`, '', {
|
|
31
|
+
action: {
|
|
32
|
+
label: 'Add',
|
|
33
|
+
onClick: () => useGrooveStore.getState().addToWhitelist(tunnel.host),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
28
37
|
toggle();
|
|
29
38
|
} catch (err) {
|
|
30
39
|
let detail = err?.message || 'Unknown error';
|
|
@@ -2660,6 +2660,16 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2660
2660
|
}
|
|
2661
2661
|
},
|
|
2662
2662
|
|
|
2663
|
+
async disconnectPeer(peerId) {
|
|
2664
|
+
try {
|
|
2665
|
+
await api.delete(`/federation/peers/${encodeURIComponent(peerId)}`);
|
|
2666
|
+
get().addToast('info', 'Peer disconnected');
|
|
2667
|
+
get().fetchFederationStatus();
|
|
2668
|
+
} catch (err) {
|
|
2669
|
+
get().addToast('error', 'Disconnect failed', err.message);
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
|
|
2663
2673
|
// ── Training Data ─────────────────────────────────────────
|
|
2664
2674
|
|
|
2665
2675
|
async setTrainingOptIn(enabled) {
|
|
@@ -1,37 +1,78 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useEffect } from 'react';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../stores/groove';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { FederationPeers } from '../components/settings/federation-peers';
|
|
5
|
+
import { FederationActivity } from '../components/settings/federation-activity';
|
|
6
|
+
import { FederationWizard } from '../components/settings/federation-wizard';
|
|
7
|
+
import { WhitelistTab, AmbassadorsTab } from '../components/settings/federation-panel';
|
|
8
|
+
import { Button } from '../components/ui/button';
|
|
6
9
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
7
|
-
import { Globe } from 'lucide-react';
|
|
10
|
+
import { Globe, Plus } from 'lucide-react';
|
|
8
11
|
|
|
9
12
|
export default function FederationView() {
|
|
10
13
|
const fetchFederationStatus = useGrooveStore((s) => s.fetchFederationStatus);
|
|
14
|
+
const fetchPouchLog = useGrooveStore((s) => s.fetchPouchLog);
|
|
15
|
+
const peers = useGrooveStore((s) => s.federation.peers);
|
|
16
|
+
const connections = useGrooveStore((s) => s.federation.connections);
|
|
17
|
+
const whitelist = useGrooveStore((s) => s.federation.whitelist);
|
|
18
|
+
const [wizardOpen, setWizardOpen] = useState(false);
|
|
19
|
+
|
|
20
|
+
const peerCount = (peers.length > 0 ? peers : connections).length;
|
|
11
21
|
|
|
12
22
|
useEffect(() => {
|
|
13
23
|
fetchFederationStatus();
|
|
24
|
+
fetchPouchLog();
|
|
14
25
|
}, []);
|
|
15
26
|
|
|
16
27
|
return (
|
|
17
28
|
<div className="flex flex-col h-full bg-surface-0">
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
29
|
+
{/* Hero strip */}
|
|
30
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
31
|
+
<div className="flex items-center gap-3">
|
|
32
|
+
<div className="w-8 h-8 rounded-md bg-accent/10 flex items-center justify-center">
|
|
33
|
+
<Globe size={16} className="text-accent" />
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<h1 className="text-sm font-semibold text-text-0 font-sans">Federation</h1>
|
|
37
|
+
<p className="text-2xs text-text-3 font-sans">
|
|
38
|
+
{peerCount} peer{peerCount !== 1 ? 's' : ''} · {whitelist.length} whitelisted
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
25
41
|
</div>
|
|
42
|
+
<Button size="sm" variant="primary" onClick={() => setWizardOpen(true)} className="h-8 text-xs gap-1.5">
|
|
43
|
+
<Plus size={12} />
|
|
44
|
+
Add Peer
|
|
45
|
+
</Button>
|
|
26
46
|
</div>
|
|
27
47
|
|
|
48
|
+
{/* Content */}
|
|
28
49
|
<ScrollArea className="flex-1">
|
|
29
|
-
<div className="
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
<div className="p-6">
|
|
51
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
52
|
+
{/* Left: Peers + Activity (stacked) */}
|
|
53
|
+
<div className="lg:col-span-2 space-y-4">
|
|
54
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4">
|
|
55
|
+
<FederationPeers onOpenWizard={() => setWizardOpen(true)} />
|
|
56
|
+
</div>
|
|
57
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4">
|
|
58
|
+
<FederationActivity />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Right: Whitelist + Ambassadors (stacked) */}
|
|
63
|
+
<div className="space-y-4">
|
|
64
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4">
|
|
65
|
+
<WhitelistTab />
|
|
66
|
+
</div>
|
|
67
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-4">
|
|
68
|
+
<AmbassadorsTab />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
33
72
|
</div>
|
|
34
73
|
</ScrollArea>
|
|
74
|
+
|
|
75
|
+
<FederationWizard open={wizardOpen} onOpenChange={setWizardOpen} />
|
|
35
76
|
</div>
|
|
36
77
|
);
|
|
37
78
|
}
|
|
@@ -31,12 +31,17 @@ export class EnvelopeBuilder {
|
|
|
31
31
|
return this._buildEnvelope();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
updateMetadata(updates) {
|
|
35
|
+
Object.assign(this._metadata, updates);
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
buildSessionClose(outcome) {
|
|
35
39
|
return {
|
|
36
40
|
envelope_id: `env_${randomUUID()}`,
|
|
37
41
|
session_id: this._sessionId,
|
|
38
42
|
type: 'SESSION_CLOSE',
|
|
39
43
|
attestation: { session_hmac: '', sequence: 0, app_version_hash: '' },
|
|
44
|
+
metadata: { ...this._metadata },
|
|
40
45
|
outcome,
|
|
41
46
|
};
|
|
42
47
|
}
|
|
@@ -65,7 +65,7 @@ export class PIIScrubber {
|
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
name: 'ipv6',
|
|
68
|
-
regex: /(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,
|
|
68
|
+
regex: /(?<![\w.#-])(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[fF]{4}:)?(?:\d{1,3}\.){3}\d{1,3}|::1\b)/g,
|
|
69
69
|
replacement: '[IP]',
|
|
70
70
|
},
|
|
71
71
|
{
|
|
@@ -3,23 +3,39 @@
|
|
|
3
3
|
const ERROR_SIGNAL_RE = /\b(?:error|Error|ERROR|exception|Exception|EXCEPTION|failed|FAILED|exit code [1-9]|ENOENT|EACCES|EPERM|TypeError|ReferenceError|SyntaxError|Cannot find|Module not found|Command failed|non-zero exit)\b/;
|
|
4
4
|
const FIX_SIGNAL_RE = /\b(?:fix|correcting|I see the issue|let me fix|the (?:issue|problem|bug) (?:is|was)|instead I should|my mistake)\b/i;
|
|
5
5
|
|
|
6
|
+
const CORRECTION_RE = /\b(?:no[,. ](?:that|not|don't|wrong)|that'?s (?:not|wrong|incorrect)|don'?t do that|stop (?:doing|that)|instead (?:of|do)|undo|revert|go back|try (?:again|differently)|you (?:broke|missed|forgot))\b/i;
|
|
7
|
+
const APPROVAL_RE = /\b(?:looks? good|lgtm|approved?|go ahead|ship it|that'?s (?:right|correct|perfect)|perfect|exactly right|nice work|well done|great job)\b/i;
|
|
8
|
+
const CLARIFICATION_RE = /\b(?:what (?:about|I (?:mean|want))|I meant|to (?:be clear|clarify)|let me (?:rephrase|explain)|clarif(?:y|ication)|more specifically)\b/i;
|
|
9
|
+
|
|
6
10
|
export class StepClassifier {
|
|
7
11
|
constructor() {
|
|
8
12
|
this.hasAgentActed = false;
|
|
9
13
|
this._lastStepType = null;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
classifyUserMessage(text) {
|
|
16
|
+
classifyUserMessage(text, source = 'user') {
|
|
13
17
|
if (!this.hasAgentActed) {
|
|
14
|
-
return
|
|
18
|
+
return {
|
|
19
|
+
type: 'instruction',
|
|
20
|
+
content: text,
|
|
21
|
+
source,
|
|
22
|
+
};
|
|
15
23
|
}
|
|
16
24
|
return {
|
|
17
|
-
type:
|
|
25
|
+
type: StepClassifier.classifyIntent(text),
|
|
18
26
|
content: text,
|
|
19
|
-
source
|
|
27
|
+
source,
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
static classifyIntent(text) {
|
|
32
|
+
if (!text || typeof text !== 'string') return 'instruction';
|
|
33
|
+
if (CORRECTION_RE.test(text)) return 'correction';
|
|
34
|
+
if (APPROVAL_RE.test(text)) return 'approval';
|
|
35
|
+
if (CLARIFICATION_RE.test(text)) return 'clarification';
|
|
36
|
+
return 'instruction';
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
classifyCoordinationEvent(event) {
|
|
24
40
|
return {
|
|
25
41
|
type: 'coordination',
|
|
@@ -42,7 +58,7 @@ export class StepClassifier {
|
|
|
42
58
|
step.type = 'error';
|
|
43
59
|
}
|
|
44
60
|
|
|
45
|
-
if (step.type === 'thought' && this._lastStepType === 'correction' && FIX_SIGNAL_RE.test(content)) {
|
|
61
|
+
if (step.type === 'thought' && (this._lastStepType === 'correction' || this._lastStepType === 'instruction') && FIX_SIGNAL_RE.test(content)) {
|
|
46
62
|
step.correction_context = true;
|
|
47
63
|
}
|
|
48
64
|
|
|
@@ -63,6 +79,6 @@ export class StepClassifier {
|
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
static countUserInterventions(steps) {
|
|
66
|
-
return steps.filter((s) => s.type === 'correction').length;
|
|
82
|
+
return steps.filter((s) => s.type === 'correction' || s.type === 'clarification').length;
|
|
67
83
|
}
|
|
68
84
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
TRAINING_MIN_TOKENS,
|
|
19
19
|
TRAINING_MIN_DURATION,
|
|
20
20
|
TRAINING_EXCLUSION_REASONS,
|
|
21
|
+
USER_MESSAGE_MAX_CHARS,
|
|
21
22
|
} from '../shared/constants.js';
|
|
22
23
|
|
|
23
24
|
const OFFLINE_RETRY_INTERVAL_MS = 60_000;
|
|
@@ -131,15 +132,20 @@ export class TrajectoryCapture {
|
|
|
131
132
|
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
onUserMessage(agentId, text) {
|
|
135
|
+
onUserMessage(agentId, text, source = 'user') {
|
|
135
136
|
if (!this._enabled) return;
|
|
136
137
|
const ctx = this._contexts.get(agentId);
|
|
137
138
|
if (!ctx) return;
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
const capped = (typeof text === 'string' && text.length > USER_MESSAGE_MAX_CHARS)
|
|
141
|
+
? text.slice(0, USER_MESSAGE_MAX_CHARS)
|
|
142
|
+
: text;
|
|
140
143
|
|
|
141
|
-
const classified = ctx.classifier.classifyUserMessage(
|
|
142
|
-
|
|
144
|
+
const classified = ctx.classifier.classifyUserMessage(capped, source);
|
|
145
|
+
|
|
146
|
+
if (classified.type === 'correction' || classified.type === 'clarification') {
|
|
147
|
+
ctx.revisionRounds++;
|
|
148
|
+
}
|
|
143
149
|
|
|
144
150
|
this._processStep(agentId, ctx, classified);
|
|
145
151
|
}
|
|
@@ -301,6 +307,11 @@ export class TrajectoryCapture {
|
|
|
301
307
|
const { tier, reason: tierReason } = this._computeQualityTier(ctx, status, userInterventions);
|
|
302
308
|
const { eligible, exclusionReason } = this._computeTrainingEligibility(ctx, durationSeconds);
|
|
303
309
|
|
|
310
|
+
ctx.builder.updateMetadata({
|
|
311
|
+
domain_tags: ctx.metadata.domain_tags,
|
|
312
|
+
session_quality: ctx.metadata.session_quality,
|
|
313
|
+
});
|
|
314
|
+
|
|
304
315
|
const closeEnvelope = ctx.builder.buildSessionClose({
|
|
305
316
|
status,
|
|
306
317
|
session_quality: ctx.metadata.session_quality,
|
|
@@ -36,4 +36,6 @@ export const TRAINING_MIN_TOKENS = 500;
|
|
|
36
36
|
export const TRAINING_MIN_DURATION = 10;
|
|
37
37
|
export const TRAINING_EXCLUSION_REASONS = ['too_few_steps', 'no_actions', 'no_observations', 'insufficient_tokens', 'too_short'];
|
|
38
38
|
|
|
39
|
+
export const USER_MESSAGE_MAX_CHARS = 2000;
|
|
40
|
+
|
|
39
41
|
export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { SUPPORTED_PROVIDERS, MODEL_TIERS, TRAINING_EXCLUSION_REASONS } from './constants.js';
|
|
4
4
|
|
|
5
|
-
export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination', 'edit'];
|
|
5
|
+
export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination', 'edit', 'instruction', 'clarification', 'approval'];
|
|
6
6
|
const VALID_QUALITY_TIERS = ['TIER_A', 'TIER_B', 'TIER_C'];
|
|
7
7
|
const VALID_FEEDBACK_SIGNALS = ['accepted', 'modified', 'rejected', 'iterated'];
|
|
8
8
|
|
|
@@ -117,6 +117,38 @@ describe('EnvelopeBuilder', () => {
|
|
|
117
117
|
assert.equal(close.outcome.training_exclusion_reason, null);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
it('updateMetadata syncs late-computed fields to builder', () => {
|
|
121
|
+
const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
|
|
122
|
+
const domainTags = {
|
|
123
|
+
primary: { domain: 'react_frontend', confidence: 0.3 },
|
|
124
|
+
secondary: { domain: 'typescript_node', confidence: 0.25 },
|
|
125
|
+
tertiary: { domain: 'python', confidence: 0 },
|
|
126
|
+
};
|
|
127
|
+
builder.updateMetadata({ domain_tags: domainTags, session_quality: 85 });
|
|
128
|
+
|
|
129
|
+
builder.addStep({ step: 1, type: 'thought', timestamp: 123 });
|
|
130
|
+
const envelope = builder.flush();
|
|
131
|
+
assert.deepEqual(envelope.metadata.domain_tags, domainTags);
|
|
132
|
+
assert.equal(envelope.metadata.session_quality, 85);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('SESSION_CLOSE includes metadata with domain_tags', () => {
|
|
136
|
+
const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
|
|
137
|
+
const domainTags = {
|
|
138
|
+
primary: { domain: 'react_frontend', confidence: 0.3 },
|
|
139
|
+
secondary: { domain: 'typescript_node', confidence: 0.25 },
|
|
140
|
+
tertiary: { domain: 'python', confidence: 0 },
|
|
141
|
+
};
|
|
142
|
+
builder.updateMetadata({ domain_tags: domainTags });
|
|
143
|
+
|
|
144
|
+
const close = builder.buildSessionClose({
|
|
145
|
+
status: 'SUCCESS', total_steps: 10, total_chunks: 1,
|
|
146
|
+
});
|
|
147
|
+
assert.ok(close.metadata, 'SESSION_CLOSE must include metadata');
|
|
148
|
+
assert.deepEqual(close.metadata.domain_tags, domainTags);
|
|
149
|
+
assert.equal(close.metadata.agent_role, 'backend');
|
|
150
|
+
});
|
|
151
|
+
|
|
120
152
|
it('chunk sequence increments correctly', () => {
|
|
121
153
|
const builder = new EnvelopeBuilder('sess_1', 'user_1', metadata);
|
|
122
154
|
let first = null;
|
|
@@ -136,11 +136,24 @@ describe('PIIScrubber', () => {
|
|
|
136
136
|
assert.equal(scrubber.scrub(input), input);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
it('does not scrub CSS pseudo-elements with hex-like class names', () => {
|
|
140
|
+
assert.equal(scrubber.scrub('.page::before { content: ""; }'), '.page::before { content: ""; }');
|
|
141
|
+
assert.equal(scrubber.scrub('.page::-webkit-scrollbar { width: 8px; }'), '.page::-webkit-scrollbar { width: 8px; }');
|
|
142
|
+
assert.equal(scrubber.scrub('.cafe::after { display: block; }'), '.cafe::after { display: block; }');
|
|
143
|
+
assert.equal(scrubber.scrub('::placeholder { color: gray; }'), '::placeholder { color: gray; }');
|
|
144
|
+
});
|
|
145
|
+
|
|
139
146
|
it('still scrubs IPv6 loopback ::1', () => {
|
|
140
147
|
const input = 'listening on ::1 port 3000';
|
|
141
148
|
assert.equal(scrubber.scrub(input), 'listening on [IP] port 3000');
|
|
142
149
|
});
|
|
143
150
|
|
|
151
|
+
it('scrubs compressed IPv6 addresses completely', () => {
|
|
152
|
+
assert.equal(scrubber.scrub('addr fe80::1 here'), 'addr [IP] here');
|
|
153
|
+
assert.equal(scrubber.scrub('addr fe80:: here'), 'addr [IP] here');
|
|
154
|
+
assert.equal(scrubber.scrub('addr 2001:db8::1 here'), 'addr [IP] here');
|
|
155
|
+
});
|
|
156
|
+
|
|
144
157
|
it('does not scrub file paths as base64 secrets', () => {
|
|
145
158
|
const input = '/home/user/project/groove/packages/gui/src/views/settings.jsx';
|
|
146
159
|
const result = scrubber.scrub(input);
|
|
@@ -5,18 +5,54 @@ import assert from 'node:assert/strict';
|
|
|
5
5
|
import { StepClassifier } from '../../client/step-classifier.js';
|
|
6
6
|
|
|
7
7
|
describe('StepClassifier', () => {
|
|
8
|
-
it('user message before any action is
|
|
8
|
+
it('user message before any action is classified as instruction', () => {
|
|
9
9
|
const classifier = new StepClassifier();
|
|
10
10
|
const result = classifier.classifyUserMessage('fix the bug');
|
|
11
|
-
assert.equal(result,
|
|
11
|
+
assert.equal(result.type, 'instruction');
|
|
12
|
+
assert.equal(result.content, 'fix the bug');
|
|
13
|
+
assert.equal(result.source, 'user');
|
|
12
14
|
});
|
|
13
15
|
|
|
14
|
-
it('user
|
|
16
|
+
it('user correction after action is classified as correction', () => {
|
|
15
17
|
const classifier = new StepClassifier();
|
|
16
18
|
classifier.onStep({ type: 'action' });
|
|
17
|
-
const result = classifier.classifyUserMessage('no, use exponential backoff');
|
|
19
|
+
const result = classifier.classifyUserMessage('no, that\'s wrong, use exponential backoff');
|
|
18
20
|
assert.equal(result.type, 'correction');
|
|
19
|
-
assert.equal(result.content, 'no, use exponential backoff');
|
|
21
|
+
assert.equal(result.content, 'no, that\'s wrong, use exponential backoff');
|
|
22
|
+
assert.equal(result.source, 'user');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('user approval after action is classified as approval', () => {
|
|
26
|
+
const classifier = new StepClassifier();
|
|
27
|
+
classifier.onStep({ type: 'action' });
|
|
28
|
+
const result = classifier.classifyUserMessage('looks good, ship it');
|
|
29
|
+
assert.equal(result.type, 'approval');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('user clarification after action is classified as clarification', () => {
|
|
33
|
+
const classifier = new StepClassifier();
|
|
34
|
+
classifier.onStep({ type: 'action' });
|
|
35
|
+
const result = classifier.classifyUserMessage('to clarify, I meant the sidebar component');
|
|
36
|
+
assert.equal(result.type, 'clarification');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('new instruction after action defaults to instruction', () => {
|
|
40
|
+
const classifier = new StepClassifier();
|
|
41
|
+
classifier.onStep({ type: 'action' });
|
|
42
|
+
const result = classifier.classifyUserMessage('now add pagination to the list view');
|
|
43
|
+
assert.equal(result.type, 'instruction');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('passes source through from caller', () => {
|
|
47
|
+
const classifier = new StepClassifier();
|
|
48
|
+
const result = classifier.classifyUserMessage('deploy the backend', 'planner');
|
|
49
|
+
assert.equal(result.source, 'planner');
|
|
50
|
+
assert.equal(result.type, 'instruction');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('defaults source to user', () => {
|
|
54
|
+
const classifier = new StepClassifier();
|
|
55
|
+
const result = classifier.classifyUserMessage('do the thing');
|
|
20
56
|
assert.equal(result.source, 'user');
|
|
21
57
|
});
|
|
22
58
|
|
|
@@ -66,17 +102,27 @@ describe('StepClassifier', () => {
|
|
|
66
102
|
assert.equal(StepClassifier.detectErrorRecovery(steps), false);
|
|
67
103
|
});
|
|
68
104
|
|
|
69
|
-
it('counts
|
|
105
|
+
it('counts corrections and clarifications as interventions', () => {
|
|
70
106
|
const steps = [
|
|
71
107
|
{ type: 'thought' },
|
|
72
108
|
{ type: 'correction' },
|
|
73
109
|
{ type: 'action' },
|
|
74
|
-
{ type: '
|
|
110
|
+
{ type: 'clarification' },
|
|
75
111
|
{ type: 'resolution' },
|
|
76
112
|
];
|
|
77
113
|
assert.equal(StepClassifier.countUserInterventions(steps), 2);
|
|
78
114
|
});
|
|
79
115
|
|
|
116
|
+
it('does not count instruction or approval as interventions', () => {
|
|
117
|
+
const steps = [
|
|
118
|
+
{ type: 'instruction' },
|
|
119
|
+
{ type: 'action' },
|
|
120
|
+
{ type: 'approval' },
|
|
121
|
+
{ type: 'resolution' },
|
|
122
|
+
];
|
|
123
|
+
assert.equal(StepClassifier.countUserInterventions(steps), 0);
|
|
124
|
+
});
|
|
125
|
+
|
|
80
126
|
it('counts zero interventions when none present', () => {
|
|
81
127
|
const steps = [
|
|
82
128
|
{ type: 'thought' },
|
|
@@ -117,6 +163,14 @@ describe('StepClassifier', () => {
|
|
|
117
163
|
assert.equal(result.correction_context, true);
|
|
118
164
|
});
|
|
119
165
|
|
|
166
|
+
it('marks thought after instruction as correction_context when fix signal present', () => {
|
|
167
|
+
const classifier = new StepClassifier();
|
|
168
|
+
classifier.onStep({ type: 'instruction', content: 'fix the login page' });
|
|
169
|
+
const step = { type: 'thought', content: 'I see the issue, let me fix the validation' };
|
|
170
|
+
const result = classifier.onStep(step);
|
|
171
|
+
assert.equal(result.correction_context, true);
|
|
172
|
+
});
|
|
173
|
+
|
|
120
174
|
it('does not mark thought as correction_context without prior correction', () => {
|
|
121
175
|
const classifier = new StepClassifier();
|
|
122
176
|
classifier.onStep({ type: 'action', content: 'running test' });
|
|
@@ -175,3 +229,38 @@ describe('StepClassifier', () => {
|
|
|
175
229
|
assert.equal(result.type, 'error');
|
|
176
230
|
});
|
|
177
231
|
});
|
|
232
|
+
|
|
233
|
+
describe('StepClassifier.classifyIntent', () => {
|
|
234
|
+
it('classifies corrections', () => {
|
|
235
|
+
assert.equal(StepClassifier.classifyIntent("no, that's wrong"), 'correction');
|
|
236
|
+
assert.equal(StepClassifier.classifyIntent("that's not what I wanted"), 'correction');
|
|
237
|
+
assert.equal(StepClassifier.classifyIntent('undo that change'), 'correction');
|
|
238
|
+
assert.equal(StepClassifier.classifyIntent('revert the last edit'), 'correction');
|
|
239
|
+
assert.equal(StepClassifier.classifyIntent('you missed the edge case'), 'correction');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('classifies approvals', () => {
|
|
243
|
+
assert.equal(StepClassifier.classifyIntent('looks good'), 'approval');
|
|
244
|
+
assert.equal(StepClassifier.classifyIntent('lgtm, ship it'), 'approval');
|
|
245
|
+
assert.equal(StepClassifier.classifyIntent("that's correct"), 'approval');
|
|
246
|
+
assert.equal(StepClassifier.classifyIntent('go ahead with that approach'), 'approval');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('classifies clarifications', () => {
|
|
250
|
+
assert.equal(StepClassifier.classifyIntent('to clarify, I meant the sidebar'), 'clarification');
|
|
251
|
+
assert.equal(StepClassifier.classifyIntent('what I want is the mobile layout'), 'clarification');
|
|
252
|
+
assert.equal(StepClassifier.classifyIntent('let me rephrase — update the header'), 'clarification');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('defaults to instruction for new directions', () => {
|
|
256
|
+
assert.equal(StepClassifier.classifyIntent('now add pagination to the list'), 'instruction');
|
|
257
|
+
assert.equal(StepClassifier.classifyIntent('also update the README'), 'instruction');
|
|
258
|
+
assert.equal(StepClassifier.classifyIntent('can you refactor the auth module'), 'instruction');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns instruction for null/empty input', () => {
|
|
262
|
+
assert.equal(StepClassifier.classifyIntent(null), 'instruction');
|
|
263
|
+
assert.equal(StepClassifier.classifyIntent(''), 'instruction');
|
|
264
|
+
assert.equal(StepClassifier.classifyIntent(undefined), 'instruction');
|
|
265
|
+
});
|
|
266
|
+
});
|