groove-dev 0.27.106 → 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 +2 -2
- 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 +37 -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 +40 -6
- package/node_modules/@groove-dev/daemon/src/process.js +5 -5
- package/node_modules/@groove-dev/gui/dist/assets/{index-BN7fQKaF.js → index-CEgtSfbG.js} +1749 -1746
- 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 +12 -1
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +56 -15
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +8 -7
- package/node_modules/moe-training/client/envelope-builder.js +5 -0
- package/node_modules/moe-training/client/scrubber.js +2 -2
- 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 +37 -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 +40 -6
- package/packages/daemon/src/process.js +5 -5
- package/packages/gui/dist/assets/{index-BN7fQKaF.js → index-CEgtSfbG.js} +1749 -1746
- 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 +12 -1
- package/packages/gui/src/views/federation.jsx +56 -15
- package/packages/gui/src/views/settings.jsx +8 -7
- package/ssh/main.js +2253 -0
|
@@ -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
|
{
|
|
@@ -100,7 +100,7 @@ export class PIIScrubber {
|
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
name: 'base64_secret',
|
|
103
|
-
regex: /(?<![A-Za-z0-9
|
|
103
|
+
regex: /(?<![A-Za-z0-9+])[A-Za-z0-9+]{40,}={0,2}(?![A-Za-z0-9+])/g,
|
|
104
104
|
replacement: '[API_KEY]',
|
|
105
105
|
},
|
|
106
106
|
];
|
|
@@ -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;
|
|
@@ -131,6 +131,43 @@ describe('PIIScrubber', () => {
|
|
|
131
131
|
assert.equal(scrubber.scrub(input), 'cd ~');
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
it('does not scrub CSS pseudo-elements as IPv6', () => {
|
|
135
|
+
const input = '.hero-icon::before { content: ""; }';
|
|
136
|
+
assert.equal(scrubber.scrub(input), input);
|
|
137
|
+
});
|
|
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
|
+
|
|
146
|
+
it('still scrubs IPv6 loopback ::1', () => {
|
|
147
|
+
const input = 'listening on ::1 port 3000';
|
|
148
|
+
assert.equal(scrubber.scrub(input), 'listening on [IP] port 3000');
|
|
149
|
+
});
|
|
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
|
+
|
|
157
|
+
it('does not scrub file paths as base64 secrets', () => {
|
|
158
|
+
const input = '/home/user/project/groove/packages/gui/src/views/settings.jsx';
|
|
159
|
+
const result = scrubber.scrub(input);
|
|
160
|
+
assert.ok(!result.includes('[API_KEY]'), `expected no [API_KEY] in: ${result}`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('still scrubs real base64 secrets without slashes', () => {
|
|
164
|
+
const b64 = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODk';
|
|
165
|
+
const input = `key: ${b64} end`;
|
|
166
|
+
const result = scrubber.scrub(input);
|
|
167
|
+
assert.ok(result.includes('[API_KEY]'), `expected [API_KEY] in: ${result}`);
|
|
168
|
+
assert.ok(!result.includes(b64));
|
|
169
|
+
});
|
|
170
|
+
|
|
134
171
|
it('patterns do not interfere with each other', () => {
|
|
135
172
|
const input = 'user@example.com called 555-123-4567 from 192.168.1.1';
|
|
136
173
|
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
|
+
});
|
|
@@ -29,12 +29,17 @@ function makeCtx(overrides = {}) {
|
|
|
29
29
|
session_quality: overrides.quality ?? 80,
|
|
30
30
|
},
|
|
31
31
|
builder: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
_metadata: {},
|
|
33
|
+
updateMetadata(updates) { Object.assign(this._metadata, updates); },
|
|
34
|
+
buildSessionClose: function (outcome) {
|
|
35
|
+
return {
|
|
36
|
+
envelope_id: 'env_test_close',
|
|
37
|
+
session_id: 'sess_test_1',
|
|
38
|
+
type: 'SESSION_CLOSE',
|
|
39
|
+
metadata: { ...this._metadata },
|
|
40
|
+
outcome,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
38
43
|
},
|
|
39
44
|
};
|
|
40
45
|
}
|
|
@@ -439,6 +444,48 @@ describe('TrajectoryCapture — _computeQuality', () => {
|
|
|
439
444
|
});
|
|
440
445
|
});
|
|
441
446
|
|
|
447
|
+
describe('TrajectoryCapture — domain_tags in SESSION_CLOSE', () => {
|
|
448
|
+
it('domain_tags set on ctx.metadata flow into SESSION_CLOSE via updateMetadata', () => {
|
|
449
|
+
const tc = makeTc();
|
|
450
|
+
const captured = [];
|
|
451
|
+
tc._signAndTransmit = (_sid, envelope) => { captured.push(envelope); };
|
|
452
|
+
|
|
453
|
+
const ctx = makeCtx();
|
|
454
|
+
ctx.metadata.domain_tags = {
|
|
455
|
+
primary: { domain: 'react_frontend', confidence: 0.3 },
|
|
456
|
+
secondary: { domain: 'typescript_node', confidence: 0.25 },
|
|
457
|
+
tertiary: { domain: 'python', confidence: 0 },
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
ctx.builder.updateMetadata({
|
|
461
|
+
domain_tags: ctx.metadata.domain_tags,
|
|
462
|
+
session_quality: ctx.metadata.session_quality,
|
|
463
|
+
});
|
|
464
|
+
const close = ctx.builder.buildSessionClose({
|
|
465
|
+
status: 'SUCCESS', total_steps: 10, total_chunks: 1,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
assert.ok(close.metadata, 'SESSION_CLOSE must have metadata');
|
|
469
|
+
assert.deepEqual(close.metadata.domain_tags, ctx.metadata.domain_tags);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('SESSION_CLOSE metadata is absent domain_tags when tagger returns null', () => {
|
|
473
|
+
const ctx = makeCtx();
|
|
474
|
+
ctx.metadata.domain_tags = null;
|
|
475
|
+
|
|
476
|
+
ctx.builder.updateMetadata({
|
|
477
|
+
domain_tags: null,
|
|
478
|
+
session_quality: ctx.metadata.session_quality,
|
|
479
|
+
});
|
|
480
|
+
const close = ctx.builder.buildSessionClose({
|
|
481
|
+
status: 'SUCCESS', total_steps: 5, total_chunks: 1,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
assert.ok(close.metadata, 'SESSION_CLOSE must have metadata');
|
|
485
|
+
assert.equal(close.metadata.domain_tags, null);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
442
489
|
describe('TrajectoryCapture — onParsedOutput', () => {
|
|
443
490
|
function makeEnabledTc() {
|
|
444
491
|
const tc = makeTc();
|
|
@@ -62,12 +62,12 @@ async function _executeApprovalRetry(daemon, approval) {
|
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
if (rp.agentId) {
|
|
65
|
-
await daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved and executed successfully. Result: ${resultText}
|
|
65
|
+
await daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved and executed successfully. Result: ${resultText}`, 'system');
|
|
66
66
|
}
|
|
67
67
|
} catch (err) {
|
|
68
68
|
console.log(`[Groove] Auto-retry for approval ${approval.id} failed: ${err.message}`);
|
|
69
69
|
if (rp.agentId) {
|
|
70
|
-
daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved but execution failed: ${err.message}
|
|
70
|
+
daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved but execution failed: ${err.message}`, 'system').catch(() => {});
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -740,9 +740,10 @@ export function createApi(app, daemon) {
|
|
|
740
740
|
};
|
|
741
741
|
|
|
742
742
|
const proc = spawn('codex', ['login'], {
|
|
743
|
-
stdio: ['
|
|
743
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
744
744
|
shell: true,
|
|
745
745
|
});
|
|
746
|
+
proc.stdin.on('error', () => {});
|
|
746
747
|
let stdout = '';
|
|
747
748
|
let stderr = '';
|
|
748
749
|
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
@@ -751,8 +752,8 @@ export function createApi(app, daemon) {
|
|
|
751
752
|
const timeout = setTimeout(() => {
|
|
752
753
|
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
753
754
|
respond(urlMatch
|
|
754
|
-
? { status: 'pending', url: urlMatch[0] }
|
|
755
|
-
: { status: 'pending', message: 'Login started — check your browser' });
|
|
755
|
+
? { status: 'pending', url: urlMatch[0], browserOpened: true }
|
|
756
|
+
: { status: 'pending', message: 'Login started — check your browser', browserOpened: true });
|
|
756
757
|
}, 5000);
|
|
757
758
|
|
|
758
759
|
proc.on('close', (code) => {
|
|
@@ -4210,6 +4211,33 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4210
4211
|
res.json(daemon.federation.getStatus());
|
|
4211
4212
|
});
|
|
4212
4213
|
|
|
4214
|
+
app.get('/api/federation/test', async (req, res) => {
|
|
4215
|
+
const target = req.query.target;
|
|
4216
|
+
if (!target) return res.status(400).json({ error: 'target required' });
|
|
4217
|
+
const host = target.split(':')[0];
|
|
4218
|
+
const privatePatterns = [
|
|
4219
|
+
/^127\./, /^10\./, /^192\.168\./, /^172\.(1[6-9]|2\d|3[01])\./,
|
|
4220
|
+
/^0\./, /^169\.254\./, /^localhost$/i, /^::1$/, /^\[::1\]$/,
|
|
4221
|
+
/^0\.0\.0\.0$/, /^fc/i, /^fd/i, /^fe80/i,
|
|
4222
|
+
];
|
|
4223
|
+
if (privatePatterns.some(p => p.test(host))) {
|
|
4224
|
+
return res.status(400).json({ error: 'Private/local addresses are not allowed' });
|
|
4225
|
+
}
|
|
4226
|
+
try {
|
|
4227
|
+
const controller = new AbortController();
|
|
4228
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
4229
|
+
const resp = await fetch(`http://${target}/api/health`, { signal: controller.signal });
|
|
4230
|
+
clearTimeout(timeout);
|
|
4231
|
+
if (resp.ok) {
|
|
4232
|
+
const data = await resp.json();
|
|
4233
|
+
return res.json({ reachable: true, version: data.version, peerId: data.daemonId, agents: data.agents });
|
|
4234
|
+
}
|
|
4235
|
+
res.json({ reachable: false });
|
|
4236
|
+
} catch {
|
|
4237
|
+
res.json({ reachable: false });
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
|
|
4213
4241
|
// List peers
|
|
4214
4242
|
app.get('/api/federation/peers', (req, res) => {
|
|
4215
4243
|
res.json(daemon.federation.getPeers());
|
|
@@ -4665,7 +4693,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4665
4693
|
const hasKey = daemon.credentials.hasKey(p.id);
|
|
4666
4694
|
let authStatus = 'not-configured';
|
|
4667
4695
|
if (p.authType === 'subscription') {
|
|
4668
|
-
|
|
4696
|
+
if (!p.installed) {
|
|
4697
|
+
authStatus = 'not-configured';
|
|
4698
|
+
} else {
|
|
4699
|
+
const provObj = getProvider(p.id);
|
|
4700
|
+
const authResult = provObj?.constructor?.isAuthenticated?.();
|
|
4701
|
+
authStatus = authResult?.authenticated ? 'authenticated' : 'not-configured';
|
|
4702
|
+
}
|
|
4669
4703
|
} else if (p.authType === 'api-key') {
|
|
4670
4704
|
authStatus = hasKey ? 'key-set' : 'not-configured';
|
|
4671
4705
|
if (p.authStatus?.authenticated) authStatus = 'authenticated';
|
|
@@ -1434,7 +1434,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1434
1434
|
if (existing && (existing.status === 'running' || existing.status === 'starting')) {
|
|
1435
1435
|
// Agent already active — reuse it instead of spawning a duplicate
|
|
1436
1436
|
if (config.prompt) {
|
|
1437
|
-
this.sendMessage(existing.id, config.prompt).catch((err) => {
|
|
1437
|
+
this.sendMessage(existing.id, config.prompt, 'planner').catch((err) => {
|
|
1438
1438
|
console.error(`[Groove] Phase 2 reuse message failed for ${existing.name}: ${err.message}`);
|
|
1439
1439
|
});
|
|
1440
1440
|
}
|
|
@@ -1527,7 +1527,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1527
1527
|
const message = `Your teammate ${completedAgent.name} (${completedAgent.role}) just finished their work.${fileList}${result ? `\n\nTheir summary:\n${result.slice(0, 2000)}` : ''}\n\nPlease audit their changes: verify correctness, check for bugs, run tests if available, and report any issues.`;
|
|
1528
1528
|
|
|
1529
1529
|
// Send message to the QC agent via the instruct flow
|
|
1530
|
-
this.sendMessage(qc.id, message).catch((err) => {
|
|
1530
|
+
this.sendMessage(qc.id, message, 'system').catch((err) => {
|
|
1531
1531
|
console.error(`[Groove] QC auto-trigger failed: ${err.message}`);
|
|
1532
1532
|
});
|
|
1533
1533
|
|
|
@@ -1651,7 +1651,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1651
1651
|
if (target.status === 'running') {
|
|
1652
1652
|
let sent = false;
|
|
1653
1653
|
if (this.hasAgentLoop(target.id)) {
|
|
1654
|
-
this.sendMessage(target.id, message).catch(() => {});
|
|
1654
|
+
this.sendMessage(target.id, message, 'agent').catch(() => {});
|
|
1655
1655
|
sent = true;
|
|
1656
1656
|
}
|
|
1657
1657
|
if (!sent && this.daemon.journalist) {
|
|
@@ -2189,7 +2189,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
2189
2189
|
* Send a message to a running agent loop.
|
|
2190
2190
|
* Returns true if the message was sent, false if the agent doesn't have an active loop.
|
|
2191
2191
|
*/
|
|
2192
|
-
async sendMessage(agentId, message) {
|
|
2192
|
+
async sendMessage(agentId, message, source = 'user') {
|
|
2193
2193
|
const handle = this.handles.get(agentId);
|
|
2194
2194
|
if (!handle?.loop) return false;
|
|
2195
2195
|
|
|
@@ -2200,7 +2200,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
2200
2200
|
const wrapped = agent ? wrapWithRoleReminder(agent.role, message) : message;
|
|
2201
2201
|
|
|
2202
2202
|
if (this.daemon.trajectoryCapture) {
|
|
2203
|
-
try { this.daemon.trajectoryCapture.onUserMessage(agentId, message); } catch (e) { /* fail silent */ }
|
|
2203
|
+
try { this.daemon.trajectoryCapture.onUserMessage(agentId, message, source); } catch (e) { /* fail silent */ }
|
|
2204
2204
|
}
|
|
2205
2205
|
|
|
2206
2206
|
loop.sendMessage(wrapped).catch(() => {});
|