groove-dev 0.27.113 → 0.27.115
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/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +3 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +24 -0
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -2
- package/node_modules/@groove-dev/daemon/src/teams.js +100 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
- package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
- package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
- 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/agents/workspace-mode.jsx +0 -22
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +42 -8
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +43 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +24 -0
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/teams.js +100 -6
- package/packages/daemon/src/tunnel-manager.js +75 -43
- package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
- package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/packages/gui/src/components/layout/status-bar.jsx +43 -45
- package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/stores/groove.js +42 -8
- package/packages/gui/src/views/agents.jsx +31 -3
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +106 -3
- package/TRAINING_DATA_v3.md +0 -11
- package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
- package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
- package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
- package/codex-test/offroad-nitro-racer/index.html +0 -21
- package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
- package/codex-test/offroad-nitro-racer/package.json +0 -15
- package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
- package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
- package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
- package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
- package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
- package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
- package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
- package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
- package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
- package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
- package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
- package/codex-test/offroad-nitro-racer/src/style.css +0 -291
- package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
- package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
- package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Embedding Service Diagnostic — Central Command Rebuild
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
After the Central Command rebuild, `session_embedding` is `null` on all SESSION_CLOSE envelopes and `domain_tags` show keyword-mode confidence (~0.067) instead of embedding-mode confidence (~0.3-0.5). Embeddings were working correctly before the rebuild (TRAINING_DATA_v3 had full 384-dim vectors on every session).
|
|
6
|
+
|
|
7
|
+
The endpoint responds to direct curl tests from a developer machine, but the Groove client is falling back to keyword mode during live sessions.
|
|
8
|
+
|
|
9
|
+
## What "working" looked like (v3 data)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
domain_tags: primary=react_frontend (0.4932)
|
|
13
|
+
session_embedding: 384d, model=sentence-transformers/all-MiniLM-L6-v2, nonzero=377/384
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What's happening now (v4 data)
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
domain_tags: primary=nlp_llm (0.0769) ← keyword mode
|
|
20
|
+
session_embedding: null ← embed() returns null in keyword mode
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How the client decides between keyword and embedding mode
|
|
24
|
+
|
|
25
|
+
On every agent spawn, the `DomainTagger.init()` runs this sequence:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
1. POST to EMBEDDING_SERVICE_URL with {"input": "health check", "model": "sentence-transformers/all-MiniLM-L6-v2"}
|
|
29
|
+
2. If response is 200 OK:
|
|
30
|
+
a. Switch to "http" mode
|
|
31
|
+
b. Call _buildCentroids() — embeds all 40 domain descriptions (40 sequential POST calls)
|
|
32
|
+
c. If all succeeds → ready, embedding mode active
|
|
33
|
+
3. If ANYTHING fails (network error, non-200, timeout, _buildCentroids() error):
|
|
34
|
+
→ Fall back to "keyword" mode silently
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The client **never logs** why it fell back — it catches all errors silently. So the failure is invisible.
|
|
38
|
+
|
|
39
|
+
## Likely failure points (in order of probability)
|
|
40
|
+
|
|
41
|
+
### 1. `_buildCentroids()` is failing
|
|
42
|
+
|
|
43
|
+
Even if the health probe succeeds, `init()` then makes **40 sequential embed calls** to build domain centroids. If any single one fails, the whole init throws and falls back to keyword mode. The entire block is wrapped in a try/catch:
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(this._serviceUrl, { ... }); // health probe
|
|
48
|
+
if (res.ok) {
|
|
49
|
+
this._mode = this._leafRegistry?.length ? 'registry' : 'http';
|
|
50
|
+
await this._buildCentroids(); // ← 40 more calls, if this throws → keyword mode
|
|
51
|
+
this._ready = true;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// silently falls through to keyword mode
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**To check:** Look at Central Command logs during a Groove session. You should see ~41 POST requests to `/v1/embed` in quick succession (1 health probe + 40 centroid builds). If you see fewer, one is failing.
|
|
60
|
+
|
|
61
|
+
### 2. Timeout under load
|
|
62
|
+
|
|
63
|
+
The health probe has a **5-second timeout**. The centroid embed calls have a **10-second timeout** each. If the ONNX model is cold or the server is under load, individual calls might be timing out.
|
|
64
|
+
|
|
65
|
+
**To check:** Add timing logs to the `/v1/embed` endpoint:
|
|
66
|
+
```javascript
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
const vector = await embed(text);
|
|
69
|
+
console.log(`[embed] ${Date.now() - start}ms "${text.slice(0, 50)}..."`);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Response format mismatch
|
|
73
|
+
|
|
74
|
+
The client parses the response as:
|
|
75
|
+
```javascript
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
const embedding = data?.data?.[0]?.embedding;
|
|
78
|
+
if (!Array.isArray(embedding)) {
|
|
79
|
+
// returns null — "Invalid embedding response format"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The expected response is **exactly**:
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"data": [
|
|
87
|
+
{
|
|
88
|
+
"embedding": [0.0123, -0.0456, ...384 floats],
|
|
89
|
+
"index": 0
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"model": "sentence-transformers/all-MiniLM-L6-v2"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**To check:** Verify the response structure. Common mistakes:
|
|
97
|
+
- Returning `{ "embedding": [...] }` instead of `{ "data": [{ "embedding": [...] }] }`
|
|
98
|
+
- Returning the vector as a nested array `[[0.01, ...]]` instead of flat `[0.01, ...]`
|
|
99
|
+
- Returning Float32Array or typed array instead of plain JSON numbers
|
|
100
|
+
|
|
101
|
+
### 4. CORS or request rejection
|
|
102
|
+
|
|
103
|
+
The Groove daemon makes requests from `localhost` (or wherever it runs). If the rebuilt server has stricter CORS, different firewall rules, or middleware that rejects requests without a specific User-Agent or Origin header, the fetch would fail.
|
|
104
|
+
|
|
105
|
+
**To check:** The original Central Command had `Access-Control-Allow-Origin: *`. Verify the rebuild has the same.
|
|
106
|
+
|
|
107
|
+
### 5. URL path mismatch
|
|
108
|
+
|
|
109
|
+
The client hits exactly: `POST https://api.groovedev.ai/v1/embed`
|
|
110
|
+
|
|
111
|
+
**To check:** Verify the route is mounted at `/v1/embed`, not `/embed` or `/v1/embeddings` or `/api/v1/embed`.
|
|
112
|
+
|
|
113
|
+
## Quick diagnostic commands
|
|
114
|
+
|
|
115
|
+
Run these on the Central Command server:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# 1. Verify endpoint exists and responds correctly
|
|
119
|
+
curl -s -X POST http://localhost:8443/v1/embed \
|
|
120
|
+
-H "Content-Type: application/json" \
|
|
121
|
+
-d '{"input": "Python programming debugging testing"}' \
|
|
122
|
+
| python3 -c "
|
|
123
|
+
import sys, json
|
|
124
|
+
d = json.load(sys.stdin)
|
|
125
|
+
v = d.get('data', [{}])[0].get('embedding')
|
|
126
|
+
if v and isinstance(v, list):
|
|
127
|
+
print(f'OK: {len(v)}-dim vector, type={type(v[0]).__name__}')
|
|
128
|
+
else:
|
|
129
|
+
print(f'BROKEN: response structure = {json.dumps(d)[:200]}')"
|
|
130
|
+
|
|
131
|
+
# 2. Simulate the full init sequence (health probe + 40 centroids)
|
|
132
|
+
python3 -c "
|
|
133
|
+
import requests, time
|
|
134
|
+
url = 'http://localhost:8443/v1/embed'
|
|
135
|
+
domains = [
|
|
136
|
+
'Python programming, debugging, testing with pytest',
|
|
137
|
+
'TypeScript and Node.js backend, Express frameworks',
|
|
138
|
+
'React frontend development, JSX components, hooks',
|
|
139
|
+
# ...add all 40 domain descriptions from domain-tagger.js
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# Health probe
|
|
143
|
+
start = time.time()
|
|
144
|
+
r = requests.post(url, json={'input': 'health check', 'model': 'sentence-transformers/all-MiniLM-L6-v2'}, timeout=5)
|
|
145
|
+
print(f'Health probe: {r.status_code} in {time.time()-start:.1f}s')
|
|
146
|
+
|
|
147
|
+
# Centroid batch
|
|
148
|
+
fails = 0
|
|
149
|
+
for i, desc in enumerate(domains):
|
|
150
|
+
try:
|
|
151
|
+
r = requests.post(url, json={'input': desc}, timeout=10)
|
|
152
|
+
d = r.json()
|
|
153
|
+
v = d.get('data', [{}])[0].get('embedding')
|
|
154
|
+
if not v or not isinstance(v, list) or len(v) != 384:
|
|
155
|
+
print(f' Domain {i}: BAD RESPONSE')
|
|
156
|
+
fails += 1
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f' Domain {i}: FAILED - {e}')
|
|
159
|
+
fails += 1
|
|
160
|
+
|
|
161
|
+
print(f'Centroid build: {len(domains)-fails}/{len(domains)} succeeded, {fails} failed')
|
|
162
|
+
if fails > 0:
|
|
163
|
+
print('THIS IS WHY EMBEDDINGS ARE NULL - any failure causes full fallback to keyword mode')
|
|
164
|
+
"
|
|
165
|
+
|
|
166
|
+
# 3. Check response time under sequential load
|
|
167
|
+
for i in $(seq 1 10); do
|
|
168
|
+
time curl -s -X POST http://localhost:8443/v1/embed \
|
|
169
|
+
-H "Content-Type: application/json" \
|
|
170
|
+
-d "{\"input\": \"test embedding call $i\"}" > /dev/null
|
|
171
|
+
done
|
|
172
|
+
|
|
173
|
+
# 4. Check server logs during a live Groove session
|
|
174
|
+
# Start a Groove agent and watch for /v1/embed requests in the Central Command logs
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## What success looks like
|
|
178
|
+
|
|
179
|
+
After fix, a SESSION_CLOSE envelope should show:
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"metadata": {
|
|
183
|
+
"domain_tags": {
|
|
184
|
+
"primary": { "domain": "react_frontend", "confidence": 0.49 },
|
|
185
|
+
"secondary": { "domain": "vue_frontend", "confidence": 0.47 },
|
|
186
|
+
"tertiary": { "domain": "typescript_node", "confidence": 0.40 }
|
|
187
|
+
},
|
|
188
|
+
"session_embedding": {
|
|
189
|
+
"model": "sentence-transformers/all-MiniLM-L6-v2",
|
|
190
|
+
"vector": [0.0123, -0.0456, "...384 floats"],
|
|
191
|
+
"source_text": "frontend\nI'll scaffold the project..."
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Confidence scores in the 0.25-0.55 range (cosine similarity), NOT 0.06-0.08 (keyword hit ratio).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"envelope_id":"env_70dfa132-575b-48a3-becc-a2ea8a75aecc","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"9f2d5a6369edcb2545181101574e61e02c65e939e253c068d398d2cbf8f20cd4","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"fullstack","agent_id":"e18a8fa4","task_complexity":"medium","team_size":2,"session_quality":0,"groove_version":"0.27.113","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777354900.276,"content":"Let me trace the exact URL the client is using.","token_count":12},{"step":2,"type":"action","timestamp":1777354901.244,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'EMBEDDING_SERVICE_URL\\|CENTRAL_COMMAND_URL' moe-training/shared/constants.js"},"content":"Using Bash","token_count":37},{"step":3,"type":"observation","timestamp":1777354901.808,"content":"41:export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';\n42:export const EMBEDDING_SERVICE_URL = process.env.EMBEDDING_SERVICE_URL || `${CENTRAL_COMMAND_URL}/v1/embed`;\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":71,"is_error":false,"tool":"Bash","token_count":69},{"step":4,"type":"action","timestamp":1777354905.484,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'serviceUrl\\|EMBEDDING' moe-training/client/domain-tagger.js | head -10"},"content":"Using Bash","token_count":36},{"step":5,"type":"observation","timestamp":1777354905.528,"content":"3:import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';\n176: this._serviceUrl = options.serviceUrl !== undefined ? options.serviceUrl : EMBEDDING_SERVICE_URL;\n208: if (this._serviceUrl) {\n210: const res = await fetch(this._serviceUrl, {\n361: const res = await fetch(this._serviceUrl, {\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":97,"is_error":false,"tool":"Bash","token_count":95},{"step":6,"type":"thought","timestamp":1777354911.293,"content":"The constant looks correct — `https://api.groovedev.ai/v1/embed`, no trailing slash. But let me check what the DomainTagger actually receives at runtime. The `TrajectoryCapture` creates the DomainTagger — let me see if it passes any options.","token_count":61},{"step":7,"type":"action","timestamp":1777354911.644,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'DomainTagger\\|domainTagger\\|_domainTagger' moe-training/client/trajectory-capture.js"},"content":"Using Bash","token_count":40},{"step":8,"type":"observation","timestamp":1777354911.695,"content":"11:import { DomainTagger } from './domain-tagger.js';\n48: this._domainTagger = new DomainTagger();\n49: await this._domainTagger.init();\n299: if (this._domainTagger) {\n304: const routingText = DomainTagger.buildRoutingText(role, firstPrompt, remainingThoughts);\n305: ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);\n306: ctx.metadata.session_embedding = await this._domainTagger.embed(routingText);\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":129,"is_error":false,"tool":"Bash","token_count":127},{"step":9,"type":"thought","timestamp":1777354916.313,"content":"`new DomainTagger()` — no options passed, so it uses the default `EMBEDDING_SERVICE_URL`. Now let me check where the daemon actually imports and uses this. The `moe-training` module is consumed by the daemon.","token_count":52},{"step":10,"type":"action","timestamp":1777354916.852,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -rn 'TrajectoryCapture\\|trajectory-capture\\|moe-training' packages/daemon/src/ | grep -v node_modules"},"content":"Using Bash","token_count":43},{"step":11,"type":"observation","timestamp":1777354916.946,"content":"packages/daemon/src/index.js:46:import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';\npackages/daemon/src/index.js:594: this._initTrajectoryCapture().catch(() => {});\npackages/daemon/src/index.js:680: async _initTrajectoryCapture() {\npackages/daemon/src/index.js:685: this.trajectoryCapture = new TrajectoryCapture({\npackages/daemon/src/api.js:18:import { ConsentManager } from '../../../moe-training/client/index.js';\npackages/daemon/src/api.js:4939: await daemon._initTrajectoryCapture();\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":155,"is_error":false,"tool":"Bash","token_count":153},{"step":12,"type":"action","timestamp":1777354920.727,"tool":"Read","arguments":{"file_path":"~/Desktop/groove-project/groove/packages/daemon/src/index.js","offset":678,"limit":25},"content":"Using Read","token_count":28},{"step":13,"type":"observation","timestamp":1777354920.728,"content":"678\t }\n679\t\n680\t async _initTrajectoryCapture() {\n681\t try {\n682\t if (!ConsentManager.isCaptureEnabled()) return;\n683\t const pkgPath = new URL('../package.json', import.meta.url);\n684\t const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;\n685\t this.trajectoryCapture = new TrajectoryCapture({\n686\t centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',\n687\t grooveVersion: version,\n688\t });\n689\t this.trajectoryCapture.init();\n690\t } catch (e) {\n691\t // Training capture is never critical\n692\t }\n693\t }\n694\t\n695\t _startGarbageCollector() {\n696\t // Run once on startup, then every 24 hours\n697\t this._gc();\n698\t this._gcInterval = setInterval(() => this._gc(), 24 * 60 * 60 * 1000);\n699\t\n700\t // Periodic state save — crash protection (every 30s)\n701\t this._stateSaveInterval = setInterval(async () => {\n702\t try { this.state.set('agents', this.registry.getAll()); await this.state.save(); } catch {}","truncated":false,"original_token_count":255,"is_error":false,"tool":"Read","token_count":255},{"step":14,"type":"thought","timestamp":1777354925.525,"content":"Line 689 — `this.trajectoryCapture.init()` — no `await`. That's not the bug here though. Let me look at what `TrajectoryCapture.init()` does with the DomainTagger.","token_count":41},{"step":15,"type":"action","timestamp":1777354926.407,"tool":"Read","arguments":{"file_path":"~/Desktop/groove-project/groove/moe-training/client/trajectory-capture.js","offset":38,"limit":16},"content":"Using Read","token_count":31},{"step":16,"type":"observation","timestamp":1777354926.407,"content":"38\t async init() {\n39\t if (!ConsentManager.isCaptureEnabled()) {\n40\t this._enabled = false;\n41\t return;\n42\t }\n43\t this._enabled = true;\n44\t this._scrubber = new PIIScrubber();\n45\t this._attestation = new SessionAttestation(this._centralCommandUrl);\n46\t this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);\n47\t this._transmissionQueue.start();\n48\t this._domainTagger = new DomainTagger();\n49\t await this._domainTagger.init();\n50\t this._offlineRetryTimer = setInterval(() => {\n51\t this._retryOfflineQueue();\n52\t }, OFFLINE_RETRY_INTERVAL_MS);\n53\t }","truncated":false,"original_token_count":155,"is_error":false,"tool":"Read","token_count":155},{"step":17,"type":"thought","timestamp":1777354935.764,"content":"Wait — line 689 in `index.js`: `this.trajectoryCapture.init()` — **no await**. That means `init()` runs as a fire-and-forget promise. The DomainTagger's `init()` calls `await this._domainTagger.init()` which does 41 HTTP requests. But the daemon doesn't await the TrajectoryCapture's init.\n\nThat's fine in theory — the DomainTagger should still eventually resolve. But let me check: if `init()` isn't awaited and an agent spawns before the DomainTagger finishes its 41 embed calls, the `_domainTagger` might be set but not yet `_ready`. Let me look at the timing.\n\nActually, that's not the issue either — `onAgentSpawn` doesn't check `_domainTagger.ready`. The domain tagging only happens in `_closeAgent`, by which point init would have long completed.\n\nLet me look at this from another angle — is the `EMBEDDING_SERVICE_URL` constant actually making it into the published npm package?","token_count":222},{"step":18,"type":"action","timestamp":1777354936.676,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { EMBEDDING_SERVICE_URL, CENTRAL_COMMAND_URL } from './moe-training/shared/constants.js';\nconsole.log('CENTRAL_COMMAND_URL:', CENTRAL_COMMAND_URL);\nconsole.log('EMBEDDING_SERVICE_URL:', EMBEDDING_SERVICE_URL);\n\""},"content":"Using Bash","token_count":74},{"step":19,"type":"observation","timestamp":1777354936.764,"content":"CENTRAL_COMMAND_URL: https://api.groovedev.ai\nEMBEDDING_SERVICE_URL: https://api.groovedev.ai/v1/embed\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":44,"is_error":false,"tool":"Bash","token_count":42},{"step":20,"type":"action","timestamp":1777354948.816,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('serviceUrl:', t._serviceUrl);\nconsole.log('mode:', t._mode);\nconsole.log('ready:', t._ready);\n\""},"content":"Using Bash","token_count":73},{"step":21,"type":"observation","timestamp":1777354948.897,"content":"serviceUrl: https://api.groovedev.ai/v1/embed\nmode: null\nready: false\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":36,"is_error":false,"tool":"Bash","token_count":34},{"step":22,"type":"thought","timestamp":1777354952.865,"content":"URL is correct. Let me test the full init flow.","token_count":12},{"step":23,"type":"action","timestamp":1777354956.383,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('serviceUrl:', t._serviceUrl);\nconsole.time('init');\ntry {\n await t.init();\n console.timeEnd('init');\n console.log('mode:', t.mode);\n console.log('ready:', t.ready);\n console.log('lastError:', t.lastError);\n if (t._centroids) {\n console.log('centroids:', Object.keys(t._centroids).length);\n }\n const result = await t.tag('Build a React frontend with TypeScript and Tailwind CSS');\n console.log('tag result:', JSON.stringify(result));\n const emb = await t.embed('Build a React frontend with TypeScript');\n console.log('embed:', emb ? emb.vector.length + 'd' : 'null');\n} catch (e) {\n console.error('ERROR:', e.message);\n}\n\" 2>&1","timeout":60000},"content":"Using Bash","token_count":217},{"step":24,"type":"observation","timestamp":1777354961.386,"content":"serviceUrl: https://api.groovedev.ai/v1/embed\ninit: 4.706s\nmode: http\nready: true\nlastError: null\ncentroids: 40\ntag result: {\"primary\":{\"domain\":\"react_frontend\",\"confidence\":0.685},\"secondary\":{\"domain\":\"typescript_node\",\"confidence\":0.4633},\"tertiary\":{\"domain\":\"vue_frontend\",\"confidence\":0.3264}}\nembed: 384d\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":97,"is_error":false,"tool":"Bash","token_count":94},{"step":25,"type":"thought","timestamp":1777354969.631,"content":"It works perfectly from this machine — `http` mode, 40 centroids, 0.685 confidence on React, 384d embedding. The code is correct.\n\nThe issue is that the **running daemon** was started before these changes were committed. The daemon process has the old code in memory where `DomainTagger` didn't import `EMBEDDING_SERVICE_URL` and defaulted `serviceUrl` to `null`.\n\nYou need to restart the Groove daemon to pick up the new code. The changes are in the files but the running Node process is still using the old version.","token_count":130},{"step":26,"type":"action","timestamp":1777354971.472,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('Confirmed: serviceUrl =', t._serviceUrl);\nconsole.log('This proves the code is correct.');\nconsole.log('If live sessions show keyword mode, the running daemon has stale code in memory.');\nconsole.log('Fix: restart the daemon (groove stop && groove start)');\n\""},"content":"Using Bash","token_count":114},{"step":27,"type":"observation","timestamp":1777354971.549,"content":"Confirmed: serviceUrl = https://api.groovedev.ai/v1/embed\nThis proves the code is correct.\nIf live sessions show keyword mode, the running daemon has stale code in memory.\nFix: restart the daemon (groove stop && groove start)\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":75,"is_error":false,"tool":"Bash","token_count":73},{"step":28,"type":"thought","timestamp":1777354976.411,"content":"Mystery solved. The code works — `http` mode, 40 centroids built in 4.7s, 0.685 confidence, 384d embeddings. The running daemon just needs a restart to pick up the new `EMBEDDING_SERVICE_URL` default. Run `groove stop && groove start` and the next session will have full embeddings.","token_count":71},{"step":29,"type":"resolution","timestamp":1777354976.457,"content":"Mystery solved. The code works — `http` mode, 40 centroids built in 4.7s, 0.685 confidence, 384d embeddings. The running daemon just needs a restart to pick up the new `EMBEDDING_SERVICE_URL` default. Run `groove stop && groove start` and the next session will have full embeddings.","token_count":71}]}
|
|
2
|
+
{"envelope_id":"env_8c59a800-92c2-486e-aab2-d251958883da","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","type":"SESSION_CLOSE","attestation":{"session_hmac":"3fd6f3891108e43eb7482a0361fc5789ecfb9fdf40f637a63edb01e154c6f74f","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"fullstack","agent_id":"e18a8fa4","task_complexity":"medium","team_size":2,"session_quality":70,"groove_version":"0.27.113","leaf_context":null,"domain_tags":{"primary":{"domain":"linux_admin","confidence":0.0769},"secondary":{"domain":"nlp_llm","confidence":0.0769},"tertiary":{"domain":"react_frontend","confidence":0.0667}},"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":70,"quality_tier":"TIER_A","quality_tier_reason":"high_quality_no_errors","user_interventions":0,"total_steps":29,"total_chunks":1,"total_tokens":2462,"duration_seconds":86,"files_modified":0,"errors_encountered":0,"errors_recovered":0,"coordination_events":0,"training_eligible":true,"training_exclusion_reason":null}}
|
|
3
|
+
{"envelope_id":"env_eafaf868-6b85-46c8-b687-43a576aedc50","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","type":"USER_FEEDBACK","attestation":{"session_hmac":"a60cc84ce26d8714749bbaedd3d51a8fb153d008e3cb396d3413740d6970454c","sequence":2,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"feedback":{"signal":"accepted","timestamp":1777354977.009,"context":"session completed successfully with no user interventions","target_step":29,"revision_rounds":0,"delta_summary":null}}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// GROOVE CLI — team commands
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { createInterface } from 'readline';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { apiCall } from '../client.js';
|
|
6
7
|
|
|
8
|
+
function confirm(prompt) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim().toLowerCase() === 'y'); });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
export async function teamCreate(name) {
|
|
8
16
|
try {
|
|
9
17
|
const team = await apiCall('POST', '/api/teams', { name });
|
|
@@ -37,9 +45,43 @@ export async function teamList() {
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
export async function teamDelete(id) {
|
|
48
|
+
const ok = await confirm(` This will archive the team directory. Continue? [y/N] `);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
40
53
|
try {
|
|
41
54
|
await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}`);
|
|
42
|
-
console.log(chalk.green(`
|
|
55
|
+
console.log(chalk.green(` Archived team "${id}"`) + chalk.dim(' — restore with `groove team restore <id>`'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(chalk.red(' Failed:'), err.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function teamArchived() {
|
|
63
|
+
try {
|
|
64
|
+
const { archived } = await apiCall('GET', '/api/teams/archived');
|
|
65
|
+
if (archived.length === 0) {
|
|
66
|
+
console.log(chalk.dim(' No archived teams.'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.bold(`\n Archived Teams (${archived.length})\n`));
|
|
70
|
+
for (const t of archived) {
|
|
71
|
+
const date = t.deletedAt ? new Date(t.deletedAt).toLocaleDateString() : 'unknown';
|
|
72
|
+
console.log(` ${chalk.bold(t.originalName || t.id)} — archive-id: ${t.id} — deleted ${date} — ${t.agentCount} agent(s)`);
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
} catch {
|
|
76
|
+
console.error(chalk.red(' Cannot connect to daemon.'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function teamRestore(id) {
|
|
82
|
+
try {
|
|
83
|
+
const team = await apiCall('POST', `/api/teams/archived/${encodeURIComponent(id)}/restore`);
|
|
84
|
+
console.log(chalk.green(` Restored team "${team.name}"`) + ` (new id: ${team.id})`);
|
|
43
85
|
} catch (err) {
|
|
44
86
|
console.error(chalk.red(' Failed:'), err.message);
|
|
45
87
|
process.exit(1);
|
|
@@ -1100,6 +1100,30 @@ export function createApi(app, daemon) {
|
|
|
1100
1100
|
}
|
|
1101
1101
|
});
|
|
1102
1102
|
|
|
1103
|
+
app.get('/api/teams/archived', (req, res) => {
|
|
1104
|
+
res.json({ archived: daemon.teams.listArchived() });
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
app.post('/api/teams/archived/:id/restore', (req, res) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const team = daemon.teams.restore(req.params.id);
|
|
1110
|
+
daemon.audit.log('team.restore', { archivedId: req.params.id, newId: team.id, name: team.name });
|
|
1111
|
+
res.json(team);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
res.status(400).json({ error: err.message });
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
app.delete('/api/teams/archived/:id', (req, res) => {
|
|
1118
|
+
try {
|
|
1119
|
+
daemon.teams.purge(req.params.id);
|
|
1120
|
+
daemon.audit.log('team.purge', { archivedId: req.params.id });
|
|
1121
|
+
res.json({ ok: true });
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
res.status(400).json({ error: err.message });
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1103
1127
|
app.patch('/api/teams/:id', (req, res) => {
|
|
1104
1128
|
try {
|
|
1105
1129
|
if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
|
|
@@ -8,6 +8,7 @@ export class FileWatcher {
|
|
|
8
8
|
constructor(daemon) {
|
|
9
9
|
this.daemon = daemon;
|
|
10
10
|
this.watchers = new Map(); // relPath → { watcher, timer }
|
|
11
|
+
this.dirWatchers = new Map(); // relPath → { watcher, timer }
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
watch(relPath) {
|
|
@@ -51,9 +52,53 @@ export class FileWatcher {
|
|
|
51
52
|
this.watchers.delete(relPath);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
watchDir(relPath) {
|
|
56
|
+
if (typeof relPath !== 'string') return;
|
|
57
|
+
if (relPath && relPath.includes('..')) return;
|
|
58
|
+
if (this.dirWatchers.has(relPath)) return;
|
|
59
|
+
|
|
60
|
+
const fullPath = relPath ? resolve(this.daemon.projectDir, relPath) : this.daemon.projectDir;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const watcher = watch(fullPath, () => {
|
|
64
|
+
const entry = this.dirWatchers.get(relPath);
|
|
65
|
+
if (!entry) return;
|
|
66
|
+
|
|
67
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
68
|
+
entry.timer = setTimeout(() => {
|
|
69
|
+
this.daemon.broadcast({
|
|
70
|
+
type: 'file:tree-changed',
|
|
71
|
+
path: relPath,
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
}, 300);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
watcher.on('error', () => {
|
|
78
|
+
this.unwatchDir(relPath);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.dirWatchers.set(relPath, { watcher, timer: null });
|
|
82
|
+
} catch {
|
|
83
|
+
// Directory doesn't exist or not watchable — ignore
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
unwatchDir(relPath) {
|
|
88
|
+
const entry = this.dirWatchers.get(relPath);
|
|
89
|
+
if (!entry) return;
|
|
90
|
+
|
|
91
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
92
|
+
try { entry.watcher.close(); } catch { /* already closed */ }
|
|
93
|
+
this.dirWatchers.delete(relPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
54
96
|
unwatchAll() {
|
|
55
97
|
for (const [relPath] of this.watchers) {
|
|
56
98
|
this.unwatch(relPath);
|
|
57
99
|
}
|
|
100
|
+
for (const [relPath] of this.dirWatchers) {
|
|
101
|
+
this.unwatchDir(relPath);
|
|
102
|
+
}
|
|
58
103
|
}
|
|
59
104
|
}
|
|
@@ -325,8 +325,9 @@ export class Daemon {
|
|
|
325
325
|
data: enrichAgents(this.registry.getAll()),
|
|
326
326
|
}));
|
|
327
327
|
|
|
328
|
-
// Track which files this client is watching (for cleanup on disconnect)
|
|
328
|
+
// Track which files/dirs this client is watching (for cleanup on disconnect)
|
|
329
329
|
const watchedFiles = new Set();
|
|
330
|
+
const watchedDirs = new Set();
|
|
330
331
|
|
|
331
332
|
ws.on('message', (raw) => {
|
|
332
333
|
try {
|
|
@@ -335,7 +336,7 @@ export class Daemon {
|
|
|
335
336
|
// Validate message type against whitelist
|
|
336
337
|
const VALID_WS_TYPES = new Set([
|
|
337
338
|
'terminal:spawn', 'terminal:resize', 'terminal:input', 'terminal:close', 'terminal:kill', 'terminal:rename',
|
|
338
|
-
'editor:watch', 'editor:unwatch', 'editor:save',
|
|
339
|
+
'editor:watch', 'editor:unwatch', 'editor:save', 'editor:watchdir', 'editor:unwatchdir',
|
|
339
340
|
'ping'
|
|
340
341
|
]);
|
|
341
342
|
if (!msg || typeof msg !== 'object' || !VALID_WS_TYPES.has(msg.type)) return;
|
|
@@ -351,6 +352,14 @@ export class Daemon {
|
|
|
351
352
|
case 'editor:unwatch':
|
|
352
353
|
if (msg.path) { this.fileWatcher.unwatch(msg.path); watchedFiles.delete(msg.path); }
|
|
353
354
|
break;
|
|
355
|
+
case 'editor:watchdir':
|
|
356
|
+
if (typeof msg.path === 'string' && !msg.path.includes('..')) {
|
|
357
|
+
this.fileWatcher.watchDir(msg.path); watchedDirs.add(msg.path);
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
case 'editor:unwatchdir':
|
|
361
|
+
if (typeof msg.path === 'string') { this.fileWatcher.unwatchDir(msg.path); watchedDirs.delete(msg.path); }
|
|
362
|
+
break;
|
|
354
363
|
// Terminal
|
|
355
364
|
case 'terminal:spawn': {
|
|
356
365
|
if (msg.cwd !== undefined && (typeof msg.cwd !== 'string' || msg.cwd.includes('..'))) break;
|
|
@@ -389,6 +398,9 @@ export class Daemon {
|
|
|
389
398
|
for (const path of watchedFiles) {
|
|
390
399
|
this.fileWatcher.unwatch(path);
|
|
391
400
|
}
|
|
401
|
+
for (const path of watchedDirs) {
|
|
402
|
+
this.fileWatcher.unwatchDir(path);
|
|
403
|
+
}
|
|
392
404
|
this.terminalManager.cleanupClient(ws);
|
|
393
405
|
});
|
|
394
406
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// GROOVE — Teams (Live Agent Groups)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync } from 'fs';
|
|
5
|
-
import { resolve } from 'path';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
|
|
5
|
+
import { resolve, basename } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { validateTeamName } from './validate.js';
|
|
8
8
|
|
|
@@ -162,17 +162,39 @@ export class Teams {
|
|
|
162
162
|
this.daemon.registry.remove(agent.id);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
166
|
-
// (legacy default teams that were never migrated point there).
|
|
165
|
+
// Archive the team's working directory instead of deleting it
|
|
167
166
|
if (
|
|
168
167
|
team.workingDir &&
|
|
169
168
|
team.workingDir !== this.daemon.projectDir &&
|
|
170
169
|
existsSync(team.workingDir)
|
|
171
170
|
) {
|
|
172
171
|
try {
|
|
173
|
-
|
|
172
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
173
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
174
|
+
const slug = basename(team.workingDir);
|
|
175
|
+
const archiveName = `${slug}-${Date.now()}`;
|
|
176
|
+
const archivePath = resolve(archiveDir, archiveName);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
renameSync(team.workingDir, archivePath);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err.code === 'EXDEV') {
|
|
182
|
+
cpSync(team.workingDir, archivePath, { recursive: true });
|
|
183
|
+
rmSync(team.workingDir, { recursive: true, force: true });
|
|
184
|
+
} else {
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const metadata = {
|
|
190
|
+
originalName: team.name,
|
|
191
|
+
originalId: team.id,
|
|
192
|
+
deletedAt: new Date().toISOString(),
|
|
193
|
+
agentCount: agents.length,
|
|
194
|
+
};
|
|
195
|
+
writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
174
196
|
} catch (err) {
|
|
175
|
-
console.log(`[Groove:Teams] Failed to
|
|
197
|
+
console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
|
|
176
198
|
}
|
|
177
199
|
}
|
|
178
200
|
|
|
@@ -193,6 +215,78 @@ export class Teams {
|
|
|
193
215
|
return true;
|
|
194
216
|
}
|
|
195
217
|
|
|
218
|
+
listArchived() {
|
|
219
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
220
|
+
if (!existsSync(archiveDir)) return [];
|
|
221
|
+
const entries = readdirSync(archiveDir, { withFileTypes: true });
|
|
222
|
+
const result = [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!entry.isDirectory()) continue;
|
|
225
|
+
const metaPath = resolve(archiveDir, entry.name, 'metadata.json');
|
|
226
|
+
try {
|
|
227
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
228
|
+
result.push({ id: entry.name, ...meta });
|
|
229
|
+
} catch {
|
|
230
|
+
result.push({ id: entry.name, originalName: entry.name, deletedAt: null, agentCount: 0 });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
restore(archivedId) {
|
|
237
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
238
|
+
const archivePath = resolve(archiveDir, archivedId);
|
|
239
|
+
if (!existsSync(archivePath)) throw new Error('Archived team not found');
|
|
240
|
+
|
|
241
|
+
let meta = {};
|
|
242
|
+
const metaPath = resolve(archivePath, 'metadata.json');
|
|
243
|
+
try { meta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* use defaults */ }
|
|
244
|
+
|
|
245
|
+
const name = meta.originalName || archivedId;
|
|
246
|
+
const dirName = slugify(name);
|
|
247
|
+
let workingDir = resolve(this.daemon.projectDir, dirName);
|
|
248
|
+
|
|
249
|
+
if (existsSync(workingDir)) {
|
|
250
|
+
workingDir = resolve(this.daemon.projectDir, `${dirName}-${Date.now()}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
renameSync(archivePath, workingDir);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err.code === 'EXDEV') {
|
|
257
|
+
cpSync(archivePath, workingDir, { recursive: true });
|
|
258
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
259
|
+
} else {
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Remove the metadata file from the restored directory
|
|
265
|
+
const restoredMetaPath = resolve(workingDir, 'metadata.json');
|
|
266
|
+
try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
|
|
267
|
+
|
|
268
|
+
const id = randomUUID().slice(0, 8);
|
|
269
|
+
const team = {
|
|
270
|
+
id,
|
|
271
|
+
name,
|
|
272
|
+
isDefault: false,
|
|
273
|
+
workingDir,
|
|
274
|
+
createdAt: new Date().toISOString(),
|
|
275
|
+
};
|
|
276
|
+
this.teams.set(id, team);
|
|
277
|
+
this._save();
|
|
278
|
+
this.daemon.broadcast({ type: 'team:created', team });
|
|
279
|
+
return team;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
purge(archivedId) {
|
|
283
|
+
const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
|
|
284
|
+
const archivePath = resolve(archiveDir, archivedId);
|
|
285
|
+
if (!existsSync(archivePath)) throw new Error('Archived team not found');
|
|
286
|
+
rmSync(archivePath, { recursive: true, force: true });
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
196
290
|
// Migrate old agents (teamName but no teamId) to default team
|
|
197
291
|
migrateAgents() {
|
|
198
292
|
const defaultTeam = this.getDefault();
|