erne-universal 0.2.0 → 0.3.0

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.
@@ -0,0 +1,275 @@
1
+ # ERNE Agent Dashboard — Design Spec
2
+
3
+ **Date:** 2026-03-11
4
+ **Status:** Approved
5
+
6
+ ## Overview
7
+
8
+ A local pixel-art visual dashboard that shows all ERNE agents working in real-time. When a user starts the ERNE agent system, a local server launches and opens a browser with an animated "office" where each agent sits at a desk, moves between rooms, and visually reflects its current status.
9
+
10
+ Inspired by OpenClaw's "Agents Team" view.
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ Claude Code Hooks → HTTP POST → Dashboard Server → WebSocket → Browser Canvas
16
+ ```
17
+
18
+ ### Components
19
+
20
+ 1. **Dashboard Server** (`dashboard/server.js`) — Node.js HTTP + WebSocket server. Uses `ws` npm package for WebSocket (the only dependency).
21
+ 2. **Claude Code Hooks** — `PreToolUse` (pattern: `Agent`) and `PostToolUse` (pattern: `Agent`) hooks that POST events to `localhost:3333/api/events` via a Node.js hook script.
22
+ 3. **Frontend** (`dashboard/public/`) — HTML5 Canvas pixel-art office + WebSocket client
23
+ 4. **CLI Integration** (`bin/cli.js`) — `erne dashboard` and `erne start` commands added to COMMANDS map
24
+
25
+ ### File Structure
26
+
27
+ ```
28
+ dashboard/
29
+ ├── server.js # HTTP + WebSocket server (ws package)
30
+ ├── package.json # { "dependencies": { "ws": "^8" } }
31
+ ├── public/
32
+ │ ├── index.html # Entry point
33
+ │ ├── canvas.js # Office renderer (rooms, furniture, grid)
34
+ │ ├── agents.js # Agent sprites & animations
35
+ │ ├── sidebar.js # Status panel
36
+ │ ├── ws-client.js # WebSocket connection (auto-reconnect)
37
+ │ └── sprites/ # Pixel art assets (optional future PNGs)
38
+ scripts/hooks/
39
+ ├── dashboard-event.js # Hook script that parses stdin and POSTs to dashboard
40
+ ```
41
+
42
+ ### Changes to Existing Files
43
+
44
+ - **`bin/cli.js`**: Add `dashboard` and `start` to `COMMANDS` map. `dashboard` spawns `dashboard/server.js`. `start` runs existing init + spawns dashboard in background.
45
+ - **`hooks/hooks.json`**: Add two new entries for `PreToolUse` (pattern: `Agent`) and `PostToolUse` (pattern: `Agent`) pointing to `dashboard-event.js`, profile: `["minimal", "standard", "strict"]`.
46
+ - **`package.json`**: Add `ws` as dependency.
47
+
48
+ ## Office Layout
49
+
50
+ 4 rooms, each mapped to agent roles:
51
+
52
+ ```
53
+ ┌──────────────────────────────────────────────┐
54
+ │ ERNE HQ │
55
+ │ ┌──────────────┐ ┌──────────────┐ │
56
+ │ │ DEVELOPMENT │ │ REVIEW │ │
57
+ │ │ architect │ │ code-reviewer│ │
58
+ │ │ native- │ │ upgrade- │ │
59
+ │ │ bridge- │ │ assistant │ │
60
+ │ │ builder │ │ │ │
61
+ │ │ expo-config │ │ │ │
62
+ │ │ ui-designer │ │ │ │
63
+ │ └──────────────┘ └──────────────┘ │
64
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────┐ │
65
+ │ │ TESTING │ │ CONFERENCE │ │SIDE- │ │
66
+ │ │ tdd-guide │ │ (multi-agent │ │BAR │ │
67
+ │ │ performance │ │ tasks) │ │ │ │
68
+ │ │ profiler │ │ │ │ │ │
69
+ │ └──────────────┘ └──────────────┘ └──────┘ │
70
+ └──────────────────────────────────────────────┘
71
+ ```
72
+
73
+ ### Agent-Room Assignment
74
+
75
+ | Agent | Default Room | Sprite Trait |
76
+ |-------|-------------|-------------|
77
+ | architect | Development | Blue hard hat |
78
+ | native-bridge-builder | Development | Wrench |
79
+ | expo-config-resolver | Development | Gear icon |
80
+ | ui-designer | Development | Paintbrush |
81
+ | code-reviewer | Review | Glasses |
82
+ | upgrade-assistant | Review | Arrow up |
83
+ | tdd-guide | Testing | Test tube |
84
+ | performance-profiler | Testing | Stopwatch |
85
+
86
+ ## Agent States & Animations
87
+
88
+ - **IDLE** — Sitting at desk, head bob animation
89
+ - **WORKING** — Typing on computer, screen text animation, colored indicator on desk
90
+ - **MOVING** — Walking to Conference Room for multi-agent tasks
91
+ - **DONE** — Green checkmark popup, then returns to IDLE
92
+
93
+ Note: ERROR state is deferred to v2. In v1, agents that time out transition directly from WORKING to IDLE.
94
+
95
+ ## Event System
96
+
97
+ ### Hook Implementation
98
+
99
+ Claude Code hooks receive context via stdin as JSON. The hook script `scripts/hooks/dashboard-event.js` reads stdin, extracts the agent name from the tool input parameters, and POSTs to the dashboard server.
100
+
101
+ **Hook entries in `hooks/hooks.json`:**
102
+
103
+ ```json
104
+ {
105
+ "event": "PreToolUse",
106
+ "pattern": "Agent",
107
+ "script": "dashboard-event.js",
108
+ "command": "node scripts/hooks/run-with-flags.js dashboard-event.js",
109
+ "profiles": ["minimal", "standard", "strict"]
110
+ },
111
+ {
112
+ "event": "PostToolUse",
113
+ "pattern": "Agent",
114
+ "script": "dashboard-event.js",
115
+ "command": "node scripts/hooks/run-with-flags.js dashboard-event.js",
116
+ "profiles": ["minimal", "standard", "strict"]
117
+ }
118
+ ```
119
+
120
+ **`dashboard-event.js` logic:**
121
+
122
+ ```js
123
+ // Reads hook context from stdin (JSON)
124
+ // PreToolUse with Agent pattern → extracts agent name from tool_input.prompt
125
+ // PostToolUse with Agent pattern → agent completed
126
+ // Maps agent name to ERNE agent by keyword matching
127
+ // POSTs { type, agent, task } to http://localhost:3333/api/events
128
+ // Silently fails if dashboard server is not running (non-blocking)
129
+ ```
130
+
131
+ The script uses `http` built-in module (no dependencies). It matches agent names by scanning the `prompt` field for known ERNE agent keywords (architect, code-reviewer, tdd-guide, etc.).
132
+
133
+ ### Event Schema
134
+
135
+ ```js
136
+ {
137
+ type: "agent:start" | "agent:complete" | "tool:call",
138
+ agent: "architect" | "code-reviewer" | ...,
139
+ task: "Planning authentication module", // optional, from prompt
140
+ timestamp: 1710000000000 // added by server
141
+ }
142
+ ```
143
+
144
+ ### Server State (in-memory)
145
+
146
+ ```js
147
+ agents: {
148
+ "architect": {
149
+ status: "working", // idle | working | done
150
+ task: "Planning auth module",
151
+ room: "development",
152
+ startedAt: 1710000000000,
153
+ lastEvent: 1710000005000
154
+ }
155
+ }
156
+ ```
157
+
158
+ Auto-timeout: if no event from agent for 5 minutes, status transitions directly to IDLE.
159
+
160
+ ## CLI Integration
161
+
162
+ ```bash
163
+ erne dashboard # Start on port 3333, open browser
164
+ erne dashboard --port 4444 # Custom port
165
+ erne dashboard --no-open # Don't open browser
166
+
167
+ erne start # Existing functionality + dashboard in background
168
+ ```
169
+
170
+ ### Changes to `bin/cli.js`
171
+
172
+ Add to `COMMANDS`:
173
+
174
+ ```js
175
+ dashboard: () => require('../lib/dashboard'),
176
+ start: () => require('../lib/start'),
177
+ ```
178
+
179
+ Update help text to include `dashboard` and `start` commands.
180
+
181
+ ### `lib/dashboard.js` Flow
182
+
183
+ 1. Parse `--port` (default 3333) and `--no-open` flags from `process.argv`
184
+ 2. Check if port is available (try `net.createServer().listen()`)
185
+ 3. Start `dashboard/server.js` via `child_process.fork()`
186
+ 4. Open `http://localhost:{port}` in browser (`open` on macOS, `xdg-open` on Linux)
187
+ 5. Console: `ERNE Dashboard running at http://localhost:{port}`
188
+ 6. Handle SIGINT for clean shutdown (kill child process)
189
+
190
+ ### `lib/start.js` Flow
191
+
192
+ 1. Run existing `init` logic
193
+ 2. Spawn dashboard in background (detached, stdio ignored)
194
+ 3. Log dashboard URL
195
+
196
+ ### Auto Hook Setup
197
+
198
+ On first `erne dashboard` run, checks `.claude/hooks.json` for dashboard-event entries. If missing, prompts:
199
+ ```
200
+ Dashboard hooks not configured. Add them now? (Y/n)
201
+ ```
202
+ If yes, appends the two hook entries to the hooks array.
203
+
204
+ ## Error Handling
205
+
206
+ ### Dashboard Server
207
+ - **Port in use**: Log error and suggest `--port` flag, exit 1
208
+ - **CORS**: Not needed — hooks use `curl`/`http.request` (not browser), and the browser frontend is served from the same origin
209
+ - **Malformed events**: Validate incoming JSON, ignore invalid payloads with a warning log
210
+
211
+ ### WebSocket Client (browser)
212
+ - **Disconnection**: Auto-reconnect with exponential backoff (1s, 2s, 4s, max 30s)
213
+ - **Connection indicator**: Small dot in the corner — green=connected, red=disconnected
214
+
215
+ ### Hook Script
216
+ - **Dashboard not running**: `dashboard-event.js` catches connection errors silently (no stderr, no exit code) so it never blocks Claude Code
217
+ - **Invalid stdin**: Skip and exit 0
218
+
219
+ ## Sprite System
220
+
221
+ ### Procedural Pixel Art (v1)
222
+
223
+ Sprites are drawn programmatically on Canvas (no PNG files needed for v1). Each agent is 32x32 pixels with unique visual traits.
224
+
225
+ ### Sprite Sheet Layout (per agent, programmatic)
226
+
227
+ ```
228
+ ┌────┬────┬────┬────┐
229
+ │ I1 │ I2 │ I3 │ I4 │ IDLE (row 0)
230
+ ├────┼────┼────┼────┤
231
+ │ W1 │ W2 │ W3 │ W4 │ WORKING (row 1)
232
+ ├────┼────┼────┼────┤
233
+ │ M1 │ M2 │ M3 │ M4 │ MOVING (row 2)
234
+ ├────┼────┼────┼────┤
235
+ │ D1 │ D2 │ D3 │ D4 │ DONE (row 3)
236
+ └────┴────┴────┴────┘
237
+ 128x128 total per agent
238
+ ```
239
+
240
+ ### Rendering
241
+
242
+ - Office background: pre-rendered static tilemap
243
+ - Rooms separated by doors
244
+ - Each desk has assigned `{x, y}` position
245
+ - Animation: `requestAnimationFrame` at 12 FPS sprite / 60 FPS canvas
246
+ - Room furniture: desks, computers, whiteboard, coffee machine
247
+
248
+ ### Color Palette
249
+
250
+ - Office background: #2C2137, #4A3F5C, #7B6B8D
251
+ - Room floor: #8B7355
252
+ - Furniture: #5C4033, #8B6914
253
+ - Status: WORKING=#4CAF50, IDLE=#9E9E9E, DONE=#2196F3
254
+
255
+ ## Scope Boundaries
256
+
257
+ **In scope (v1):**
258
+ - Dashboard server with WebSocket (`ws` package)
259
+ - Pixel art office with 4 rooms
260
+ - 8 agent sprites (procedural)
261
+ - Agent status tracking via Claude Code hooks (PreToolUse/PostToolUse with Agent pattern)
262
+ - Sidebar with agent list and status
263
+ - CLI commands (`erne dashboard`, `erne start` integration)
264
+ - Auto hook configuration
265
+ - WebSocket auto-reconnect
266
+ - Auto-timeout for stale agents
267
+
268
+ **Out of scope (v2+):**
269
+ - ERROR state with dedicated hook
270
+ - Hand-drawn PNG sprites
271
+ - Task history / timeline view
272
+ - Multiple user sessions
273
+ - Remote access
274
+ - Sound effects
275
+ - Agent chat/communication visualization
package/hooks/hooks.json CHANGED
@@ -97,6 +97,20 @@
97
97
  "command": "node scripts/hooks/run-with-flags.js accessibility-check.js",
98
98
  "profiles": ["strict"]
99
99
  },
100
+ {
101
+ "event": "PreToolUse",
102
+ "pattern": "Agent",
103
+ "script": "dashboard-event.js",
104
+ "command": "node scripts/hooks/run-with-flags.js dashboard-event.js",
105
+ "profiles": ["minimal", "standard", "strict"]
106
+ },
107
+ {
108
+ "event": "PostToolUse",
109
+ "pattern": "Agent",
110
+ "script": "dashboard-event.js",
111
+ "command": "node scripts/hooks/run-with-flags.js dashboard-event.js",
112
+ "profiles": ["minimal", "standard", "strict"]
113
+ },
100
114
  {
101
115
  "event": "Stop",
102
116
  "script": "continuous-learning-observer.js",
@@ -0,0 +1,156 @@
1
+ // lib/dashboard.js — Launch the ERNE Agent Dashboard
2
+ 'use strict';
3
+
4
+ const net = require('net');
5
+ const { fork } = require('child_process');
6
+ const { execFileSync } = require('child_process');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const readline = require('readline/promises');
10
+ const { stdin, stdout } = require('process');
11
+
12
+ const HOOKS_PATH = path.resolve(__dirname, '..', 'hooks', 'hooks.json');
13
+ const SERVER_PATH = path.resolve(__dirname, '..', 'dashboard', 'server.js');
14
+
15
+ function parseArgs(argv) {
16
+ const args = argv.slice(3);
17
+ let port = 3333;
18
+ let open = true;
19
+
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (args[i] === '--port' && args[i + 1]) {
22
+ const parsed = parseInt(args[i + 1], 10);
23
+ if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
24
+ console.error(`Invalid port: ${args[i + 1]} (must be 1-65535)`);
25
+ process.exit(1);
26
+ }
27
+ port = parsed;
28
+ i++;
29
+ } else if (args[i] === '--no-open') {
30
+ open = false;
31
+ }
32
+ }
33
+
34
+ return { port, open };
35
+ }
36
+
37
+ function checkPort(port) {
38
+ return new Promise((resolve, reject) => {
39
+ const server = net.createServer();
40
+ server.once('error', (err) => {
41
+ if (err.code === 'EADDRINUSE') {
42
+ reject(new Error(`Port ${port} is already in use`));
43
+ } else {
44
+ reject(err);
45
+ }
46
+ });
47
+ server.once('listening', () => {
48
+ server.close(() => resolve());
49
+ });
50
+ server.listen(port);
51
+ });
52
+ }
53
+
54
+ async function ensureHooksConfigured() {
55
+ try {
56
+ const raw = fs.readFileSync(HOOKS_PATH, 'utf8');
57
+ const data = JSON.parse(raw);
58
+ const hasDashboardHook = data.hooks.some(
59
+ (h) => h.script === 'dashboard-event.js'
60
+ );
61
+
62
+ if (hasDashboardHook) return;
63
+
64
+ const rl = readline.createInterface({ input: stdin, output: stdout });
65
+ const answer = await rl.question(
66
+ ' Dashboard hooks not configured. Add them now? (Y/n) '
67
+ );
68
+ rl.close();
69
+
70
+ if (answer.toLowerCase() === 'n') return;
71
+
72
+ data.hooks.push(
73
+ {
74
+ event: 'PreToolUse',
75
+ pattern: 'Agent',
76
+ script: 'dashboard-event.js',
77
+ command: 'node scripts/hooks/run-with-flags.js dashboard-event.js',
78
+ profiles: ['minimal', 'standard', 'strict'],
79
+ },
80
+ {
81
+ event: 'PostToolUse',
82
+ pattern: 'Agent',
83
+ script: 'dashboard-event.js',
84
+ command: 'node scripts/hooks/run-with-flags.js dashboard-event.js',
85
+ profiles: ['minimal', 'standard', 'strict'],
86
+ }
87
+ );
88
+
89
+ fs.writeFileSync(HOOKS_PATH, JSON.stringify(data, null, 2) + '\n', 'utf8');
90
+ console.log(' Dashboard hooks added to hooks.json');
91
+ } catch (err) {
92
+ if (err.code === 'ENOENT') {
93
+ console.warn(' Warning: hooks/hooks.json not found, skipping hook configuration');
94
+ } else {
95
+ throw err;
96
+ }
97
+ }
98
+ }
99
+
100
+ function openBrowser(url) {
101
+ const platform = process.platform;
102
+ try {
103
+ if (platform === 'darwin') {
104
+ execFileSync('open', [url]);
105
+ } else if (platform === 'linux') {
106
+ execFileSync('xdg-open', [url]);
107
+ }
108
+ } catch {
109
+ // Silently ignore — browser open is best-effort
110
+ }
111
+ }
112
+
113
+ module.exports = async function dashboard() {
114
+ const { port, open } = parseArgs(process.argv);
115
+
116
+ await checkPort(port);
117
+ await ensureHooksConfigured();
118
+
119
+ // Ensure ws dependency is installed
120
+ const dashboardDir = path.resolve(__dirname, '..', 'dashboard');
121
+ if (!fs.existsSync(path.join(dashboardDir, 'node_modules', 'ws'))) {
122
+ console.log(' Installing dashboard dependencies...');
123
+ require('child_process').execSync('npm install --production', { cwd: dashboardDir, stdio: 'ignore' });
124
+ }
125
+
126
+ const child = fork(SERVER_PATH, [], {
127
+ env: { ...process.env, ERNE_DASHBOARD_PORT: String(port) },
128
+ stdio: 'pipe',
129
+ });
130
+
131
+ child.stdout.on('data', (data) => process.stdout.write(data));
132
+ child.stderr.on('data', (data) => process.stderr.write(data));
133
+
134
+ const url = `http://localhost:${port}`;
135
+
136
+ // Wait briefly for the server to start before opening the browser
137
+ await new Promise((resolve) => setTimeout(resolve, 500));
138
+ console.log(` ERNE Dashboard: ${url}`);
139
+
140
+ if (open) {
141
+ openBrowser(url);
142
+ }
143
+
144
+ const shutdown = () => {
145
+ child.kill('SIGTERM');
146
+ process.exit(0);
147
+ };
148
+
149
+ process.on('SIGINT', shutdown);
150
+ process.on('SIGTERM', shutdown);
151
+
152
+ // Keep alive until child exits
153
+ await new Promise((resolve) => {
154
+ child.on('exit', resolve);
155
+ });
156
+ };