groove-dev 0.27.112 → 0.27.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/TRAINING_DATA_v3.md +11 -0
  2. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +44 -0
  3. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +1 -0
  4. package/codex-test/offroad-nitro-racer/dist/index.html +23 -0
  5. package/codex-test/offroad-nitro-racer/index.html +21 -0
  6. package/codex-test/offroad-nitro-racer/package-lock.json +841 -0
  7. package/codex-test/offroad-nitro-racer/package.json +15 -0
  8. package/codex-test/offroad-nitro-racer/src/game/AI.ts +28 -0
  9. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +63 -0
  10. package/codex-test/offroad-nitro-racer/src/game/Car.ts +247 -0
  11. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +62 -0
  12. package/codex-test/offroad-nitro-racer/src/game/Game.ts +229 -0
  13. package/codex-test/offroad-nitro-racer/src/game/Input.ts +45 -0
  14. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +224 -0
  15. package/codex-test/offroad-nitro-racer/src/game/Track.ts +158 -0
  16. package/codex-test/offroad-nitro-racer/src/game/UI.ts +96 -0
  17. package/codex-test/offroad-nitro-racer/src/game/math.ts +42 -0
  18. package/codex-test/offroad-nitro-racer/src/main.ts +24 -0
  19. package/codex-test/offroad-nitro-racer/src/style.css +291 -0
  20. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +1 -0
  21. package/codex-test/offroad-nitro-racer/tsconfig.json +18 -0
  22. package/codex-test/offroad-nitro-racer/vite.config.ts +7 -0
  23. package/moe-training/client/parsers/codex.js +3 -3
  24. package/moe-training/client/parsers/gemini.js +2 -2
  25. package/moe-training/client/step-classifier.js +2 -2
  26. package/moe-training/test/client/step-classifier.test.js +63 -7
  27. package/node_modules/@groove-dev/cli/package.json +1 -1
  28. package/node_modules/@groove-dev/daemon/package.json +1 -1
  29. package/node_modules/@groove-dev/daemon/src/api.js +51 -15
  30. package/node_modules/@groove-dev/daemon/src/index.js +22 -8
  31. package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
  32. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +3 -1
  35. package/node_modules/@groove-dev/gui/src/stores/groove.js +15 -0
  36. package/node_modules/moe-training/client/parsers/codex.js +3 -3
  37. package/node_modules/moe-training/client/parsers/gemini.js +2 -2
  38. package/node_modules/moe-training/client/step-classifier.js +2 -2
  39. package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
  40. package/package.json +1 -1
  41. package/packages/cli/package.json +1 -1
  42. package/packages/daemon/package.json +1 -1
  43. package/packages/daemon/src/api.js +51 -15
  44. package/packages/daemon/src/index.js +22 -8
  45. package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
  46. package/packages/gui/dist/index.html +1 -1
  47. package/packages/gui/package.json +1 -1
  48. package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
  49. package/packages/gui/src/stores/groove.js +15 -0
  50. package/TRAINING_DATA_v2.md +0 -9
@@ -0,0 +1,291 @@
1
+ :root {
2
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
3
+ color: #fff7ed;
4
+ background: #140c08;
5
+ font-synthesis: none;
6
+ text-rendering: optimizeLegibility;
7
+ -webkit-font-smoothing: antialiased;
8
+ --amber: #f6b23c;
9
+ --cyan: #32f5ff;
10
+ --red: #f43f5e;
11
+ --panel: rgba(30, 18, 12, 0.82);
12
+ --panel-border: rgba(255, 240, 210, 0.22);
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ html,
20
+ body,
21
+ #app {
22
+ width: 100%;
23
+ height: 100%;
24
+ margin: 0;
25
+ overflow: hidden;
26
+ }
27
+
28
+ body {
29
+ min-width: 320px;
30
+ }
31
+
32
+ button {
33
+ font: inherit;
34
+ }
35
+
36
+ .shell {
37
+ position: relative;
38
+ width: 100vw;
39
+ height: 100vh;
40
+ isolation: isolate;
41
+ background: radial-gradient(circle at 50% 50%, #a7642c 0%, #3b2115 72%);
42
+ }
43
+
44
+ canvas {
45
+ position: absolute;
46
+ inset: 0;
47
+ width: 100%;
48
+ height: 100%;
49
+ image-rendering: auto;
50
+ }
51
+
52
+ .overlay {
53
+ position: absolute;
54
+ inset: 0;
55
+ z-index: 2;
56
+ pointer-events: none;
57
+ }
58
+
59
+ .overlay--active {
60
+ display: grid;
61
+ place-items: center;
62
+ padding: 24px;
63
+ pointer-events: auto;
64
+ background:
65
+ radial-gradient(circle at center, rgba(255, 170, 70, 0.12), transparent 38%),
66
+ linear-gradient(135deg, rgba(13, 8, 5, 0.52), rgba(13, 8, 5, 0.78));
67
+ backdrop-filter: blur(3px);
68
+ }
69
+
70
+ .panel {
71
+ width: min(620px, 94vw);
72
+ border: 2px solid var(--panel-border);
73
+ border-radius: 28px;
74
+ padding: 28px;
75
+ background: var(--panel);
76
+ box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.18);
77
+ text-align: center;
78
+ }
79
+
80
+ .panel.hero {
81
+ transform: rotate(-1deg);
82
+ }
83
+
84
+ .panel.compact {
85
+ width: min(420px, 92vw);
86
+ }
87
+
88
+ .eyebrow {
89
+ margin: 0 0 10px;
90
+ color: var(--cyan);
91
+ font-size: 12px;
92
+ font-weight: 900;
93
+ letter-spacing: 0.22em;
94
+ text-transform: uppercase;
95
+ }
96
+
97
+ h1,
98
+ h2 {
99
+ margin: 0;
100
+ line-height: 0.9;
101
+ text-transform: uppercase;
102
+ text-shadow: 0 5px 0 #5a230f, 0 12px 30px rgba(0, 0, 0, 0.55);
103
+ }
104
+
105
+ h1 {
106
+ font-size: clamp(48px, 9vw, 92px);
107
+ }
108
+
109
+ h2 {
110
+ font-size: clamp(38px, 7vw, 64px);
111
+ }
112
+
113
+ .lede,
114
+ .panel p:not(.eyebrow) {
115
+ color: #ffe7c5;
116
+ font-size: 16px;
117
+ line-height: 1.6;
118
+ }
119
+
120
+ .controls-grid {
121
+ display: grid;
122
+ grid-template-columns: 1fr 1.2fr;
123
+ gap: 10px 16px;
124
+ margin: 24px auto;
125
+ max-width: 390px;
126
+ text-align: left;
127
+ }
128
+
129
+ .controls-grid span,
130
+ .result-row span {
131
+ color: #ffd8a3;
132
+ font-size: 12px;
133
+ font-weight: 800;
134
+ letter-spacing: 0.12em;
135
+ text-transform: uppercase;
136
+ }
137
+
138
+ .controls-grid strong,
139
+ .result-row strong {
140
+ color: #fff;
141
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
142
+ }
143
+
144
+ .primary {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ min-height: 48px;
149
+ padding: 0 22px;
150
+ border: 0;
151
+ border-radius: 999px;
152
+ color: #241006;
153
+ background: linear-gradient(180deg, #ffe08a, var(--amber));
154
+ box-shadow: 0 7px 0 #8b4314, 0 18px 30px rgba(0, 0, 0, 0.3);
155
+ font-weight: 1000;
156
+ letter-spacing: 0.06em;
157
+ text-transform: uppercase;
158
+ }
159
+
160
+ .result-row {
161
+ display: flex;
162
+ justify-content: space-between;
163
+ gap: 20px;
164
+ margin: 16px auto;
165
+ max-width: 340px;
166
+ padding: 14px 16px;
167
+ border: 1px solid rgba(255, 255, 255, 0.14);
168
+ border-radius: 16px;
169
+ background: rgba(0, 0, 0, 0.2);
170
+ }
171
+
172
+ .hud {
173
+ position: absolute;
174
+ left: 18px;
175
+ right: 18px;
176
+ display: flex;
177
+ gap: 10px;
178
+ align-items: center;
179
+ justify-content: center;
180
+ }
181
+
182
+ .hud--top {
183
+ top: 16px;
184
+ }
185
+
186
+ .hud-card {
187
+ min-width: 112px;
188
+ padding: 10px 12px;
189
+ border: 1px solid rgba(255, 235, 204, 0.24);
190
+ border-radius: 16px;
191
+ background: rgba(21, 12, 8, 0.68);
192
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.22);
193
+ backdrop-filter: blur(8px);
194
+ }
195
+
196
+ .hud-card span {
197
+ display: block;
198
+ color: #ffd8a3;
199
+ font-size: 10px;
200
+ font-weight: 900;
201
+ letter-spacing: 0.14em;
202
+ text-transform: uppercase;
203
+ }
204
+
205
+ .hud-card strong {
206
+ display: block;
207
+ margin-top: 2px;
208
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
209
+ font-size: clamp(16px, 3vw, 22px);
210
+ }
211
+
212
+ .nitro-stack {
213
+ position: absolute;
214
+ right: 22px;
215
+ bottom: 22px;
216
+ display: flex;
217
+ gap: 8px;
218
+ padding: 10px;
219
+ border-radius: 999px;
220
+ background: rgba(14, 10, 8, 0.68);
221
+ border: 1px solid rgba(255, 255, 255, 0.16);
222
+ }
223
+
224
+ .nitro-stack i {
225
+ width: 48px;
226
+ height: 16px;
227
+ border: 2px solid rgba(255, 255, 255, 0.35);
228
+ border-radius: 999px;
229
+ background: rgba(255, 255, 255, 0.12);
230
+ }
231
+
232
+ .nitro-stack i.full {
233
+ border-color: #a5f3fc;
234
+ background: linear-gradient(90deg, #0891b2, var(--cyan));
235
+ box-shadow: 0 0 18px rgba(50, 245, 255, 0.75);
236
+ }
237
+
238
+ .countdown {
239
+ position: absolute;
240
+ inset: 0;
241
+ display: grid;
242
+ place-items: center;
243
+ color: #fff;
244
+ font-size: clamp(84px, 20vw, 210px);
245
+ font-weight: 1000;
246
+ text-shadow: 0 10px 0 #6c2c10, 0 0 60px rgba(255, 181, 67, 0.8);
247
+ animation: pop 0.45s ease both;
248
+ }
249
+
250
+ .toast {
251
+ position: absolute;
252
+ left: 50%;
253
+ bottom: 34px;
254
+ transform: translateX(-50%);
255
+ padding: 12px 18px;
256
+ border-radius: 999px;
257
+ color: #261208;
258
+ background: #ffe08a;
259
+ box-shadow: 0 8px 0 #8b4314;
260
+ font-weight: 1000;
261
+ text-transform: uppercase;
262
+ }
263
+
264
+ @keyframes pop {
265
+ from {
266
+ transform: scale(0.7) rotate(-4deg);
267
+ opacity: 0;
268
+ }
269
+ to {
270
+ transform: scale(1) rotate(0deg);
271
+ opacity: 1;
272
+ }
273
+ }
274
+
275
+ @media (max-width: 720px) {
276
+ .hud {
277
+ left: 8px;
278
+ right: 8px;
279
+ flex-wrap: wrap;
280
+ justify-content: flex-start;
281
+ }
282
+
283
+ .hud-card {
284
+ min-width: calc(50% - 6px);
285
+ padding: 8px 10px;
286
+ }
287
+
288
+ .nitro-stack i {
289
+ width: 34px;
290
+ }
291
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "strict": true,
14
+ "noUnusedLocals": false,
15
+ "noUnusedParameters": false
16
+ },
17
+ "include": ["src"]
18
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ const previewBase = process.env.GROOVE_PREVIEW_BASE || './';
4
+
5
+ export default defineConfig({
6
+ base: previewBase,
7
+ });
@@ -57,15 +57,15 @@ export class CodexParser {
57
57
  if (item.type === 'command_execution') {
58
58
  const rawOutput = item.aggregated_output || '';
59
59
  if (item.exit_code !== 0) {
60
- return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
60
+ return { type: 'error', is_error: true, content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
61
61
  }
62
62
  const obs = truncateObservation(rawOutput);
63
- return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
63
+ return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
64
64
  }
65
65
  if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
66
66
  const rawOutput = item.output || item.content || '';
67
67
  const obs = truncateObservation(rawOutput);
68
- return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
68
+ return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
69
69
  }
70
70
  return null;
71
71
  }
@@ -64,11 +64,11 @@ export class GeminiParser {
64
64
  const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
65
65
  const rawText = contentParts.map((p) => p.text || '').join('');
66
66
  const obs = truncateObservation(rawText);
67
- return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
67
+ return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
68
68
  }
69
69
 
70
70
  case 'error': {
71
- return { type: 'error', content: jsonEvent.message || 'Unknown error' };
71
+ return { type: 'error', is_error: true, content: jsonEvent.message || 'Unknown error' };
72
72
  }
73
73
 
74
74
  case 'agent_end': {
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
 
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/;
3
+ const ERROR_SIGNAL_RE = /(?:^|\n)\s*(?:error[ :\[]\S|Error[ :\[]\S|ERROR[ :\[]\S)|(?:exit code [1-9]|non-zero exit|ENOENT|EACCES|EPERM|Command failed)|(?:(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError):\s)|(?:Cannot find module|Module not found|ModuleNotFoundError|ImportError:)|(?:FATAL|PANIC|Traceback \(most recent)|(?:error TS\d{4}:)/;
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
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;
@@ -54,7 +54,7 @@ export class StepClassifier {
54
54
 
55
55
  const content = step.content || '';
56
56
 
57
- if ((step.type === 'action' || step.type === 'observation') && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
57
+ if (step.type === 'observation' && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
58
58
  step.type = 'error';
59
59
  }
60
60
 
@@ -132,11 +132,11 @@ describe('StepClassifier', () => {
132
132
  assert.equal(StepClassifier.countUserInterventions(steps), 0);
133
133
  });
134
134
 
135
- it('reclassifies action with error content to error', () => {
135
+ it('never reclassifies action to error', () => {
136
136
  const classifier = new StepClassifier();
137
137
  const step = { type: 'action', content: 'Command failed with exit code 1' };
138
138
  const result = classifier.onStep(step);
139
- assert.equal(result.type, 'error');
139
+ assert.equal(result.type, 'action');
140
140
  });
141
141
 
142
142
  it('reclassifies observation with error content to error', () => {
@@ -215,19 +215,75 @@ describe('StepClassifier', () => {
215
215
  assert.equal(result.type, 'error');
216
216
  });
217
217
 
218
- it('preserves action type when is_error is false', () => {
218
+ it('does not reclassify observation containing bare word "error" in source code', () => {
219
219
  const classifier = new StepClassifier();
220
- const step = { type: 'action', content: 'Command failed with exit code 1', is_error: false };
220
+ const step = { type: 'observation', content: 'function handleError(err) { console.error(err); }' };
221
221
  const result = classifier.onStep(step);
222
- assert.equal(result.type, 'action');
222
+ assert.equal(result.type, 'observation');
223
223
  });
224
224
 
225
- it('still reclassifies action to error when is_error is not set', () => {
225
+ it('does not reclassify observation with "0 errors" or "found 0 vulnerabilities"', () => {
226
226
  const classifier = new StepClassifier();
227
- const step = { type: 'action', content: 'Command failed with exit code 1' };
227
+ const step = { type: 'observation', content: 'Build succeeded\n0 errors, 0 warnings\nfound 0 vulnerabilities' };
228
+ const result = classifier.onStep(step);
229
+ assert.equal(result.type, 'observation');
230
+ });
231
+
232
+ it('does not reclassify observation reading a file that mentions exceptions', () => {
233
+ const classifier = new StepClassifier();
234
+ const step = { type: 'observation', content: '{"scripts": {"build": "tsc && vite build"}, "name": "my-app"}' };
235
+ const result = classifier.onStep(step);
236
+ assert.equal(result.type, 'observation');
237
+ });
238
+
239
+ it('reclassifies observation with real TypeScript build error', () => {
240
+ const classifier = new StepClassifier();
241
+ const step = { type: 'observation', content: 'src/main.ts(1,8): error TS2882: Cannot find module' };
242
+ const result = classifier.onStep(step);
243
+ assert.equal(result.type, 'error');
244
+ });
245
+
246
+ it('reclassifies observation with Python traceback', () => {
247
+ const classifier = new StepClassifier();
248
+ const step = { type: 'observation', content: 'Traceback (most recent call last):\n File "main.py", line 5' };
249
+ const result = classifier.onStep(step);
250
+ assert.equal(result.type, 'error');
251
+ });
252
+
253
+ it('reclassifies observation with actual TypeError message', () => {
254
+ const classifier = new StepClassifier();
255
+ const step = { type: 'observation', content: 'TypeError: Cannot read properties of undefined (reading "map")' };
256
+ const result = classifier.onStep(step);
257
+ assert.equal(result.type, 'error');
258
+ });
259
+
260
+ it('reclassifies observation with exit code failure', () => {
261
+ const classifier = new StepClassifier();
262
+ const step = { type: 'observation', content: 'Process exited with exit code 1' };
263
+ const result = classifier.onStep(step);
264
+ assert.equal(result.type, 'error');
265
+ });
266
+
267
+ it('reclassifies observation with ModuleNotFoundError', () => {
268
+ const classifier = new StepClassifier();
269
+ const step = { type: 'observation', content: 'ModuleNotFoundError: No module named requests' };
228
270
  const result = classifier.onStep(step);
229
271
  assert.equal(result.type, 'error');
230
272
  });
273
+
274
+ it('preserves action type even with error keywords', () => {
275
+ const classifier = new StepClassifier();
276
+ const step = { type: 'action', content: 'Command failed with exit code 1' };
277
+ const result = classifier.onStep(step);
278
+ assert.equal(result.type, 'action');
279
+ });
280
+
281
+ it('preserves action type regardless of is_error flag', () => {
282
+ const classifier = new StepClassifier();
283
+ const step = { type: 'action', content: 'ENOENT: no such file', is_error: true };
284
+ const result = classifier.onStep(step);
285
+ assert.equal(result.type, 'action');
286
+ });
231
287
  });
232
288
 
233
289
  describe('StepClassifier.classifyIntent', () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.112",
3
+ "version": "0.27.113",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.112",
3
+ "version": "0.27.113",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3855,6 +3855,48 @@ Keep responses concise. Help them think, don't lecture them about the system the
3855
3855
  // --- Preview Proxy (same-origin iframe support) ---
3856
3856
  // Forwards HTTP requests to the dev server so the GUI can iframe the preview
3857
3857
  // without cross-origin issues. WebSocket upgrade for HMR is handled in index.js.
3858
+
3859
+ function rewriteAbsoluteUrls(body, proxyBase) {
3860
+ let out = body;
3861
+ // HTML attributes: src, href, action, poster
3862
+ out = out.replace(/((?:src|href|action|poster)\s*=\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
3863
+ // JS imports: from '/' and import('/')
3864
+ out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
3865
+ out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
3866
+ // CSS url()
3867
+ out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
3868
+ return out;
3869
+ }
3870
+
3871
+ const REWRITABLE_TYPES = ['text/html', 'application/javascript', 'text/javascript', 'text/css'];
3872
+
3873
+ function handleProxyResponse(proxyRes, res, proxyBase) {
3874
+ const fwdHeaders = { ...proxyRes.headers };
3875
+ delete fwdHeaders['content-security-policy'];
3876
+ delete fwdHeaders['x-frame-options'];
3877
+
3878
+ const ct = (fwdHeaders['content-type'] || '').toLowerCase();
3879
+ const shouldRewrite = REWRITABLE_TYPES.some((t) => ct.includes(t));
3880
+
3881
+ if (!shouldRewrite) {
3882
+ res.writeHead(proxyRes.statusCode, fwdHeaders);
3883
+ proxyRes.pipe(res);
3884
+ return;
3885
+ }
3886
+
3887
+ const chunks = [];
3888
+ proxyRes.on('data', (c) => chunks.push(c));
3889
+ proxyRes.on('end', () => {
3890
+ let body = Buffer.concat(chunks).toString('utf8');
3891
+ body = rewriteAbsoluteUrls(body, proxyBase);
3892
+ const buf = Buffer.from(body, 'utf8');
3893
+ fwdHeaders['content-length'] = buf.length;
3894
+ delete fwdHeaders['transfer-encoding'];
3895
+ res.writeHead(proxyRes.statusCode, fwdHeaders);
3896
+ res.end(buf);
3897
+ });
3898
+ }
3899
+
3858
3900
  app.all('/api/preview/:teamId/proxy/*', (req, res) => {
3859
3901
  const entry = daemon.preview?.get(req.params.teamId);
3860
3902
  if (!entry || !entry.url) return res.status(404).json({ error: 'No active preview for this team' });
@@ -3865,9 +3907,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
3865
3907
  const proxyPath = req.params[0] || '';
3866
3908
  const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
3867
3909
  const fullPath = '/' + proxyPath + search;
3910
+ const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
3868
3911
 
3869
3912
  const headers = { ...req.headers };
3870
3913
  delete headers.host;
3914
+ delete headers['accept-encoding'];
3871
3915
  headers.host = targetUrl.host;
3872
3916
 
3873
3917
  const proxyReq = httpRequest({
@@ -3876,13 +3920,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3876
3920
  path: fullPath,
3877
3921
  method: req.method,
3878
3922
  headers,
3879
- }, (proxyRes) => {
3880
- const fwdHeaders = { ...proxyRes.headers };
3881
- delete fwdHeaders['content-security-policy'];
3882
- delete fwdHeaders['x-frame-options'];
3883
- res.writeHead(proxyRes.statusCode, fwdHeaders);
3884
- proxyRes.pipe(res);
3885
- });
3923
+ }, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
3886
3924
 
3887
3925
  proxyReq.on('error', (err) => {
3888
3926
  if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
@@ -3899,9 +3937,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
3899
3937
  try { targetUrl = new URL(entry.devUrl || entry.url); } catch { return res.status(500).json({ error: 'Invalid preview URL' }); }
3900
3938
 
3901
3939
  const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
3940
+ const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
3902
3941
 
3903
3942
  const headers = { ...req.headers };
3904
3943
  delete headers.host;
3944
+ delete headers['accept-encoding'];
3905
3945
  headers.host = targetUrl.host;
3906
3946
 
3907
3947
  const proxyReq = httpRequest({
@@ -3910,13 +3950,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3910
3950
  path: '/' + search,
3911
3951
  method: req.method,
3912
3952
  headers,
3913
- }, (proxyRes) => {
3914
- const fwdHeaders = { ...proxyRes.headers };
3915
- delete fwdHeaders['content-security-policy'];
3916
- delete fwdHeaders['x-frame-options'];
3917
- res.writeHead(proxyRes.statusCode, fwdHeaders);
3918
- proxyRes.pipe(res);
3919
- });
3953
+ }, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
3920
3954
 
3921
3955
  proxyReq.on('error', (err) => {
3922
3956
  if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
@@ -6183,13 +6217,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
6183
6217
  stdio: ['ignore', 'pipe', 'pipe'],
6184
6218
  env: tagEnv,
6185
6219
  });
6220
+ daemon._networkCheckProc = proc;
6186
6221
  let stdout = '';
6187
6222
  let stderr = '';
6188
6223
  proc.stdout.on('data', (c) => { stdout += c.toString(); });
6189
6224
  proc.stderr.on('data', (c) => { stderr += c.toString(); });
6190
6225
  const timeout = setTimeout(() => { safeKill(proc, 'SIGTERM'); }, 10_000);
6191
- proc.on('error', () => { clearTimeout(timeout); resolvePromise(null); });
6226
+ proc.on('error', () => { clearTimeout(timeout); daemon._networkCheckProc = null; resolvePromise(null); });
6192
6227
  proc.on('close', (code) => {
6228
+ daemon._networkCheckProc = null;
6193
6229
  clearTimeout(timeout);
6194
6230
  if (code !== 0) return resolvePromise(null);
6195
6231
  const tags = [];
@@ -290,11 +290,11 @@ export class Daemon {
290
290
  });
291
291
 
292
292
  // Debounced file I/O for registry changes (at most once per 2s)
293
- let _registryIoTimer = null;
293
+ this._registryIoTimer = null;
294
294
  const _debouncedRegistryIo = () => {
295
- if (_registryIoTimer) return;
296
- _registryIoTimer = setTimeout(() => {
297
- _registryIoTimer = null;
295
+ if (this._registryIoTimer) return;
296
+ this._registryIoTimer = setTimeout(() => {
297
+ this._registryIoTimer = null;
298
298
  this.introducer.writeRegistryFile(this.projectDir);
299
299
  this.introducer.injectGrooveSection(this.projectDir);
300
300
  }, 2000);
@@ -779,6 +779,11 @@ export class Daemon {
779
779
  if (this._stateSaveInterval) clearInterval(this._stateSaveInterval);
780
780
  if (this._classifierInterval) clearInterval(this._classifierInterval);
781
781
  if (this._subscriptionPollInterval) clearInterval(this._subscriptionPollInterval);
782
+ if (this._registryIoTimer) clearTimeout(this._registryIoTimer);
783
+ if (this._networkCheckProc) {
784
+ try { this._networkCheckProc.kill(); } catch { /* already exited */ }
785
+ this._networkCheckProc = null;
786
+ }
782
787
 
783
788
  // Clean up file watchers and terminal sessions
784
789
  this.fileWatcher.unwatchAll();
@@ -823,10 +828,19 @@ export class Daemon {
823
828
 
824
829
  // Close server
825
830
  return new Promise((resolvePromise) => {
826
- this.wss.close(() => {
827
- this.server.close(() => {
828
- console.log('GROOVE daemon stopped.');
829
- resolvePromise();
831
+ this.federationWss.close(() => {
832
+ this.wss.close(() => {
833
+ this.server.close(() => {
834
+ // Unref lingering handles (idle fetch/undici TLS pool connections,
835
+ // closed servers) so they don't prevent process exit in tests.
836
+ for (const h of process._getActiveHandles()) {
837
+ if (typeof h.unref === 'function' && h !== process.stdout && h !== process.stderr) {
838
+ h.unref();
839
+ }
840
+ }
841
+ console.log('GROOVE daemon stopped.');
842
+ resolvePromise();
843
+ });
830
844
  });
831
845
  });
832
846
  });