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.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Complete AI coding agent harness for React Native and Expo development.
4
4
 
5
+ <p align="center">
6
+ <img src="docs/assets/dashboard-preview.png" alt="ERNE Agent Dashboard — pixel-art office with 10 agents working in real-time" width="800">
7
+ </p>
8
+
5
9
  ## Quick Start
6
10
 
7
11
  ```bash
@@ -16,15 +20,50 @@ This will:
16
20
 
17
21
  ## What's Included
18
22
 
19
- | Component | Count |
20
- |-----------|-------|
21
- | Agents | 8 specialized AI agents |
22
- | Commands | 16 slash commands |
23
- | Rule layers | 5 (common, expo, bare-rn, native-ios, native-android) |
24
- | Hook profiles | 3 (minimal, standard, strict) |
25
- | Skills | 8 reusable knowledge modules |
26
- | Contexts | 3 behavior modes (dev, review, vibe) |
27
- | MCP configs | 10 server integrations |
23
+ | Component | Count | Description |
24
+ |-----------|-------|-------------|
25
+ | Agents | 10 | Specialized AI agents for architecture, development, review, testing, UI, native, and more |
26
+ | Commands | 16 | Slash commands for every React Native workflow |
27
+ | Rule layers | 5 | Conditional rules: common, expo, bare-rn, native-ios, native-android |
28
+ | Hook profiles | 3 | Minimal, standard, strict — quality enforcement your way |
29
+ | Skills | 8 | Reusable knowledge modules loaded on-demand |
30
+ | Contexts | 3 | Behavior modes: dev, review, vibe |
31
+ | MCP configs | 10 | Pre-configured server integrations |
32
+
33
+ ## Token Efficiency
34
+
35
+ ERNE's architecture is designed to minimize token usage through six layered mechanisms:
36
+
37
+ | Mechanism | How it works | Savings |
38
+ |-----------|-------------|---------|
39
+ | **Profile-gated hooks** | Minimal profile runs 3 hooks instead of 17 | ~31% |
40
+ | **Conditional rules** | Only loads rules matching your project type (Expo, bare RN, native) | ~26% |
41
+ | **On-demand skills** | Skills load only when their command is invoked, not always in context | ~12% |
42
+ | **Subagent isolation** | Fresh agent per task with only its own definition + relevant rules | ~12% |
43
+ | **Task-specific commands** | 16 focused prompts instead of one monolithic instruction set | ~13% |
44
+ | **Context-based behavior** | Modes change behavior dynamically without loading new rulesets | ~3% |
45
+
46
+ **Result:** Typical workflows use **60–67% fewer tokens** compared to a naive all-in-context approach. Vibe mode (minimal profile) reaches 67% savings, standard development 64%, and even strict mode saves 57%.
47
+
48
+ ## Agent Dashboard
49
+
50
+ ERNE includes a real-time pixel-art dashboard that visualizes all 10 agents working in an animated office environment.
51
+
52
+ ```bash
53
+ erne dashboard # Start on port 3333, open browser
54
+ erne dashboard --port 4444 # Custom port
55
+ erne dashboard --no-open # Don't open browser
56
+ erne start # Init project + dashboard in background
57
+ ```
58
+
59
+ **Features:**
60
+ - 4 office rooms (Development, Code Review, Testing, Conference)
61
+ - 8 unique procedural pixel-art agent sprites with animations
62
+ - Real-time status updates via WebSocket (connected to Claude Code hooks)
63
+ - Sidebar with agent status, task descriptions, and connection indicator
64
+ - Auto-reconnect with exponential backoff
65
+
66
+ The dashboard hooks into Claude Code's `PreToolUse` and `PostToolUse` events (pattern: `Agent`) to track which agents are actively working and what they're doing.
28
67
 
29
68
  ## IDE & Editor Support
30
69
 
@@ -43,32 +82,59 @@ All config files share the same React Native & Expo conventions: TypeScript stri
43
82
 
44
83
  ## Agents
45
84
 
46
- - **architect** System design and project structure
47
- - **code-reviewer** — Code quality and best practices
48
- - **tdd-guide** Test-driven development workflow
49
- - **performance-profiler** Performance diagnostics
50
- - **native-bridge-builder** Native module development
51
- - **expo-config-resolver** Expo configuration issues
52
- - **ui-designer** UI/UX implementation
53
- - **upgrade-assistant** Version migration
85
+ | Agent | Domain | Room |
86
+ |-------|--------|------|
87
+ | **architect** | System design and project structure | Development |
88
+ | **senior-developer** | End-to-end feature implementation, screens, hooks, API | Development |
89
+ | **feature-builder** | Focused implementation units, works in parallel | Development |
90
+ | **native-bridge-builder** | Turbo Modules and native platform APIs | Development |
91
+ | **expo-config-resolver** | Expo configuration and build issues | Development |
92
+ | **ui-designer** | Accessible, performant UI components | Development |
93
+ | **code-reviewer** | Code quality and best practices | Code Review |
94
+ | **upgrade-assistant** | Version migration guidance | Code Review |
95
+ | **tdd-guide** | Test-driven development workflow | Testing |
96
+ | **performance-profiler** | FPS diagnostics and bundle optimization | Testing |
54
97
 
55
98
  ## Hook Profiles
56
99
 
57
- | Profile | Use Case |
58
- |---------|----------|
59
- | minimal | Fast iteration, vibe coding |
60
- | standard | Balanced quality + speed (recommended) |
61
- | strict | Production-grade enforcement |
100
+ | Profile | Hooks | Use Case |
101
+ |---------|-------|----------|
102
+ | minimal | 3 | Fast iteration, vibe coding — maximum speed, minimum friction |
103
+ | standard | 11 | Balanced quality + speed (recommended) — catches real issues |
104
+ | strict | 17 | Production-grade enforcement — full security, accessibility, perf budgets |
62
105
 
63
- Change profile: Edit `hookProfile` in `.claude/settings.json` or use `/vibe` context.
106
+ Change profile: set `ERNE_PROFILE` env var, add `<!-- Hook Profile: standard -->` to CLAUDE.md, or use `/vibe` context.
64
107
 
65
108
  ## Commands
66
109
 
67
- Core: `/plan`, `/code-review`, `/tdd`, `/build-fix`, `/perf`, `/upgrade`, `/native-module`, `/navigate`
110
+ **Core:** `/plan`, `/code-review`, `/tdd`, `/build-fix`, `/perf`, `/upgrade`, `/native-module`, `/navigate`
111
+
112
+ **Extended:** `/animate`, `/deploy`, `/component`, `/debug`, `/quality-gate`
68
113
 
69
- Extended: `/animate`, `/deploy`, `/component`, `/debug`, `/quality-gate`
114
+ **Learning:** `/learn`, `/retrospective`, `/setup-device`
115
+
116
+ ## Architecture
117
+
118
+ ```
119
+ Claude Code Hooks ──▶ run-with-flags.js ──▶ Profile gate ──▶ Hook scripts
120
+
121
+ ┌──────────┴──────────┐
122
+ │ Only hooks for │
123
+ │ active profile │
124
+ │ are executed │
125
+ └─────────────────────┘
126
+
127
+ erne dashboard ──▶ HTTP + WS Server ──▶ Browser Canvas
128
+
129
+ Claude Code PreToolUse ─┤ (Agent pattern)
130
+ Claude Code PostToolUse ┘
131
+ ```
70
132
 
71
- Learning: `/learn`, `/retrospective`, `/setup-device`
133
+ **Key design principles:**
134
+ - **Zero runtime dependencies** for the harness itself (ws package only for dashboard)
135
+ - **Conditional loading** — rules, skills, and hooks load based on project type and profile
136
+ - **Fresh subagent per task** — no context pollution between agent invocations
137
+ - **Silent failure** — hooks never block Claude Code if something goes wrong
72
138
 
73
139
  ## Available On
74
140
 
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: feature-builder
3
+ description: Focused feature implementation — individual screens, components, hooks, and API endpoints. Designed to work in parallel with senior-developer. Triggered by /code, /feature, /component.
4
+ ---
5
+
6
+ You are the ERNE Feature Builder agent — a focused React Native/Expo implementation specialist who builds individual feature units quickly and correctly.
7
+
8
+ ## Your Role
9
+
10
+ Implement discrete feature units: a single screen, a custom hook, an API integration module, or a reusable component. You work best when given a clear, scoped task — often in parallel with the senior-developer agent on different parts of the same feature.
11
+
12
+ ## Capabilities
13
+
14
+ - **Screen building**: Implement individual screens with proper data fetching and state handling
15
+ - **Hook extraction**: Build focused custom hooks with clean interfaces and error handling
16
+ - **API modules**: Create typed API client methods, TanStack Query wrappers, and cache invalidation
17
+ - **Component building**: Build reusable UI components with props API, accessibility, and platform variants
18
+ - **Utility modules**: Implement formatters, validators, transforms, and platform-specific helpers
19
+ - **Migration scripts**: Write codemods and data migration utilities
20
+
21
+ ## Tech Stack
22
+
23
+ ```tsx
24
+ // Screen with data fetching
25
+ export const ProfileScreen = () => {
26
+ const { id } = useLocalSearchParams<{ id: string }>();
27
+ const { data: user, isLoading, error } = useUser(id);
28
+
29
+ if (isLoading) return <LoadingSkeleton />;
30
+ if (error) return <ErrorView error={error} onRetry={refetch} />;
31
+ if (!user) return <EmptyState message="User not found" />;
32
+
33
+ return (
34
+ <ScrollView contentContainerStyle={styles.container}>
35
+ <ProfileHeader user={user} />
36
+ <ProfileStats stats={user.stats} />
37
+ <ProfileActions userId={user.id} />
38
+ </ScrollView>
39
+ );
40
+ };
41
+
42
+ // Custom hook
43
+ export const useUser = (userId: string) => {
44
+ return useQuery({
45
+ queryKey: ['user', userId],
46
+ queryFn: () => apiClient.get<User>(`/users/${userId}`),
47
+ enabled: !!userId,
48
+ staleTime: 5 * 60 * 1000,
49
+ });
50
+ };
51
+ ```
52
+
53
+ ## Process
54
+
55
+ 1. **Receive scoped task** — One screen, one hook, one module at a time
56
+ 2. **Check dependencies** — Verify types, API contracts, and shared state are defined
57
+ 3. **Implement** — Write the code with all states handled (loading, error, empty, success)
58
+ 4. **Type everything** — Explicit return types, param interfaces, no `any`
59
+ 5. **Handle edges** — Null checks, empty arrays, network failures, platform differences
60
+ 6. **Deliver** — Complete file ready to integrate
61
+
62
+ ## Parallel Work Pattern
63
+
64
+ When working alongside senior-developer:
65
+ - **Senior-developer** handles: data layer, stores, navigation skeleton, complex multi-screen flows
66
+ - **Feature-builder** handles: individual screens, isolated components, utility hooks, API wrappers
67
+ - Coordinate via shared type definitions and agreed interfaces
68
+ - Never modify files the other agent is actively editing
69
+
70
+ ## Guidelines
71
+
72
+ - Functional components with `const` + arrow functions, named exports only
73
+ - Group imports: react → react-native → expo → external → internal → types
74
+ - Max 250 lines per file — if larger, you're doing too much
75
+ - `StyleSheet.create()` always, no inline styles
76
+ - Handle all UI states: loading, error, empty, success
77
+ - Every public function needs a TypeScript return type
78
+ - No `any` — use `unknown` and narrow, or define the type
79
+ - Accessibility: `accessibilityLabel`, `accessibilityRole`, `accessibilityHint` on interactive elements
80
+ - Test-ready: props-based, no hidden global state, injectable dependencies
81
+
82
+ ## Output Format
83
+
84
+ For each unit:
85
+ 1. File path and complete code
86
+ 2. Props/params interface
87
+ 3. Dependencies (what it imports from other modules)
88
+ 4. Integration point (how the parent screen/module uses it)
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: senior-developer
3
+ description: End-to-end feature implementation — screens, hooks, API integration, state management, navigation wiring. Triggered by /code, /feature, /plan (implementation phase).
4
+ ---
5
+
6
+ You are the ERNE Senior Developer agent — a senior React Native/Expo engineer who writes production-grade feature code.
7
+
8
+ ## Your Role
9
+
10
+ Implement complete features end-to-end: screens, custom hooks, API integration, state management, navigation wiring, and error handling. You are the one who turns architect plans into working code.
11
+
12
+ ## Capabilities
13
+
14
+ - **Screen implementation**: Build full screens with data fetching, loading/error states, pull-to-refresh, pagination
15
+ - **Custom hooks**: Extract reusable logic into typed hooks (`useAuth`, `useForm`, `useDebounce`, etc.)
16
+ - **API integration**: Wire TanStack Query mutations/queries, handle optimistic updates, error boundaries
17
+ - **State management**: Implement Zustand stores for client state, connect TanStack Query for server state
18
+ - **Navigation wiring**: Create Expo Router layouts, typed navigation params, deep link handlers
19
+ - **Form handling**: Build validated forms with proper keyboard handling, accessibility, and submission logic
20
+ - **Error handling**: Implement error boundaries, retry logic, user-facing error messages, offline fallbacks
21
+
22
+ ## Tech Stack
23
+
24
+ ```tsx
25
+ // Data fetching — TanStack Query
26
+ const { data, isLoading } = useQuery({
27
+ queryKey: ['users', userId],
28
+ queryFn: () => api.getUser(userId),
29
+ });
30
+
31
+ // Client state — Zustand
32
+ const useAuthStore = create<AuthState>()((set) => ({
33
+ user: null,
34
+ setUser: (user) => set({ user }),
35
+ logout: () => set({ user: null }),
36
+ }));
37
+
38
+ // Navigation — Expo Router (typed)
39
+ import { useLocalSearchParams, router } from 'expo-router';
40
+
41
+ type Params = { id: string; mode: 'edit' | 'view' };
42
+ const { id, mode } = useLocalSearchParams<Params>();
43
+
44
+ // Secure storage
45
+ import * as SecureStore from 'expo-secure-store';
46
+ ```
47
+
48
+ ## Process
49
+
50
+ 1. **Read the plan** — Understand the architect's design, component hierarchy, and data flow
51
+ 2. **Set up the skeleton** — Create files, routes, and type definitions
52
+ 3. **Implement data layer first** — API client, queries/mutations, stores
53
+ 4. **Build screens** — Wire data to UI, handle all states (loading, error, empty, success)
54
+ 5. **Add navigation** — Route params, transitions, deep links, back handling
55
+ 6. **Handle edge cases** — Offline, token expiry, race conditions, keyboard avoidance
56
+ 7. **Self-review** — Check for re-renders, missing error handling, accessibility, type safety
57
+
58
+ ## Guidelines
59
+
60
+ - Functional components with `const` + arrow functions, named exports only
61
+ - Group imports: react → react-native → expo → external → internal → types
62
+ - Max 250 lines per component — extract hooks and subcomponents when larger
63
+ - `StyleSheet.create()` for styles, no inline styles
64
+ - `FlashList` over `FlatList` for 100+ items
65
+ - Memoize with `React.memo`, `useMemo`, `useCallback` where measurable
66
+ - No anonymous functions in JSX render paths
67
+ - Validate all deep link params and external input
68
+ - Use `expo-secure-store` for tokens, never AsyncStorage
69
+ - Conventional Commits: `feat:`, `fix:`, `refactor:`
70
+
71
+ ## Output Format
72
+
73
+ For each implementation unit:
74
+ 1. File path and complete code
75
+ 2. Type definitions (interfaces, params)
76
+ 3. Integration notes (how it connects to other modules)
77
+ 4. Known trade-offs or TODOs for follow-up
package/bin/cli.js CHANGED
@@ -8,11 +8,11 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const { resolve, join } = require('path');
12
-
13
11
  const COMMANDS = {
14
12
  init: () => require('../lib/init'),
15
13
  update: () => require('../lib/update'),
14
+ dashboard: () => require('../lib/dashboard'),
15
+ start: () => require('../lib/start'),
16
16
  version: () => {
17
17
  const pkg = require('../package.json');
18
18
  console.log(`erne v${pkg.version}`);
@@ -28,6 +28,8 @@ const COMMANDS = {
28
28
  Commands:
29
29
  init Set up ERNE in your project
30
30
  update Update to the latest version
31
+ dashboard Launch the ERNE Agent Dashboard
32
+ start Init project and start dashboard
31
33
  version Show installed version
32
34
  help Show this help message
33
35
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "erne-dashboard",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "ERNE Agent Visual Dashboard",
6
+ "main": "server.js",
7
+ "dependencies": {
8
+ "ws": "^8.0.0"
9
+ }
10
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * ERNE Dashboard — Procedural 32x32 pixel-art agent sprites with animations
3
+ */
4
+ (function () {
5
+ 'use strict';
6
+
7
+ const FRAME_SIZE = 32;
8
+ const SHEET_COLS = 4;
9
+ const SHEET_ROWS = 4;
10
+ const SHEET_W = FRAME_SIZE * SHEET_COLS; // 128
11
+ const SHEET_H = FRAME_SIZE * SHEET_ROWS; // 128
12
+ const ANIM_FPS = 12;
13
+
14
+ const SKIN = '#FDBCB4';
15
+ const HAIR = '#4A3728';
16
+ const LEGS_COLOR = '#2c3e50';
17
+
18
+ const AGENT_DEFS = {
19
+ architect: { bodyColor: '#3498db', traitColor: '#2980b9', trait: 'hardhat' },
20
+ 'native-bridge-builder': { bodyColor: '#e74c3c', traitColor: '#c0392b', trait: 'wrench' },
21
+ 'expo-config-resolver': { bodyColor: '#9b59b6', traitColor: '#8e44ad', trait: 'gear' },
22
+ 'ui-designer': { bodyColor: '#e91e63', traitColor: '#c2185b', trait: 'paintbrush' },
23
+ 'code-reviewer': { bodyColor: '#2ecc71', traitColor: '#27ae60', trait: 'glasses' },
24
+ 'upgrade-assistant': { bodyColor: '#f39c12', traitColor: '#e67e22', trait: 'arrow' },
25
+ 'tdd-guide': { bodyColor: '#1abc9c', traitColor: '#16a085', trait: 'testtube' },
26
+ 'performance-profiler': { bodyColor: '#e67e22', traitColor: '#d35400', trait: 'stopwatch' },
27
+ 'senior-developer': { bodyColor: '#3455db', traitColor: '#2a44b0', trait: 'laptop' },
28
+ 'feature-builder': { bodyColor: '#00bcd4', traitColor: '#0097a7', trait: 'hammer' },
29
+ };
30
+
31
+ /* ---- Drawing primitives ---- */
32
+
33
+ const drawCharacter = (ctx, ox, oy, def, headBob, typing, armOffset) => {
34
+ const hb = headBob || 0;
35
+ const ao = armOffset || 0;
36
+
37
+ // Legs
38
+ ctx.fillStyle = LEGS_COLOR;
39
+ ctx.fillRect(ox + 11, oy + 24, 4, 6);
40
+ ctx.fillRect(ox + 17, oy + 24, 4, 6);
41
+
42
+ // Body
43
+ ctx.fillStyle = def.bodyColor;
44
+ ctx.fillRect(ox + 9, oy + 14, 14, 11);
45
+ // Shirt detail
46
+ ctx.fillStyle = def.traitColor;
47
+ ctx.fillRect(ox + 14, oy + 15, 4, 3);
48
+
49
+ // Arms
50
+ ctx.fillStyle = def.bodyColor;
51
+ ctx.fillRect(ox + 5, oy + 15 + ao, 4, 8);
52
+ ctx.fillRect(ox + 23, oy + 15 - ao, 4, 8);
53
+
54
+ // Hands
55
+ ctx.fillStyle = SKIN;
56
+ ctx.fillRect(ox + 5, oy + 23 + ao, 4, 3);
57
+ ctx.fillRect(ox + 23, oy + 23 - ao, 4, 3);
58
+
59
+ // Head
60
+ ctx.fillStyle = SKIN;
61
+ ctx.fillRect(ox + 10, oy + 4 + hb, 12, 11);
62
+
63
+ // Hair
64
+ ctx.fillStyle = HAIR;
65
+ ctx.fillRect(ox + 9, oy + 3 + hb, 14, 4);
66
+ ctx.fillRect(ox + 9, oy + 4 + hb, 2, 6);
67
+
68
+ // Eyes
69
+ ctx.fillStyle = '#222';
70
+ ctx.fillRect(ox + 13, oy + 8 + hb, 2, 2);
71
+ ctx.fillRect(ox + 18, oy + 8 + hb, 2, 2);
72
+
73
+ // Mouth
74
+ ctx.fillStyle = '#c0392b';
75
+ ctx.fillRect(ox + 14, oy + 12 + hb, 4, 1);
76
+
77
+ drawTrait(ctx, ox, oy + hb, def);
78
+ };
79
+
80
+ const drawWalkingCharacter = (ctx, ox, oy, def, legOffset) => {
81
+ const lo = legOffset || 0;
82
+
83
+ // Legs — walk cycle
84
+ ctx.fillStyle = LEGS_COLOR;
85
+ ctx.fillRect(ox + 11, oy + 24 + lo, 4, 6);
86
+ ctx.fillRect(ox + 17, oy + 24 - lo, 4, 6);
87
+
88
+ // Body
89
+ ctx.fillStyle = def.bodyColor;
90
+ ctx.fillRect(ox + 9, oy + 14, 14, 11);
91
+ ctx.fillStyle = def.traitColor;
92
+ ctx.fillRect(ox + 14, oy + 15, 4, 3);
93
+
94
+ // Arms swing
95
+ ctx.fillStyle = def.bodyColor;
96
+ ctx.fillRect(ox + 5, oy + 15 - lo, 4, 8);
97
+ ctx.fillRect(ox + 23, oy + 15 + lo, 4, 8);
98
+ ctx.fillStyle = SKIN;
99
+ ctx.fillRect(ox + 5, oy + 23 - lo, 4, 3);
100
+ ctx.fillRect(ox + 23, oy + 23 + lo, 4, 3);
101
+
102
+ // Head
103
+ ctx.fillStyle = SKIN;
104
+ ctx.fillRect(ox + 10, oy + 4, 12, 11);
105
+ ctx.fillStyle = HAIR;
106
+ ctx.fillRect(ox + 9, oy + 3, 14, 4);
107
+ ctx.fillRect(ox + 9, oy + 4, 2, 6);
108
+ ctx.fillStyle = '#222';
109
+ ctx.fillRect(ox + 13, oy + 8, 2, 2);
110
+ ctx.fillRect(ox + 18, oy + 8, 2, 2);
111
+ ctx.fillStyle = '#c0392b';
112
+ ctx.fillRect(ox + 14, oy + 12, 4, 1);
113
+
114
+ drawTrait(ctx, ox, oy, def);
115
+ };
116
+
117
+ const drawTrait = (ctx, ox, oy, def) => {
118
+ switch (def.trait) {
119
+ case 'hardhat':
120
+ ctx.fillStyle = '#f1c40f';
121
+ ctx.fillRect(ox + 8, oy + 1, 16, 4);
122
+ ctx.fillRect(ox + 10, oy - 1, 12, 3);
123
+ break;
124
+ case 'wrench':
125
+ ctx.fillStyle = def.traitColor;
126
+ ctx.fillRect(ox + 24, oy + 5, 3, 8);
127
+ ctx.fillRect(ox + 23, oy + 4, 5, 3);
128
+ break;
129
+ case 'gear':
130
+ ctx.fillStyle = def.traitColor;
131
+ ctx.fillRect(ox + 25, oy + 5, 5, 5);
132
+ ctx.fillStyle = def.bodyColor;
133
+ ctx.fillRect(ox + 26, oy + 6, 3, 3);
134
+ break;
135
+ case 'paintbrush':
136
+ ctx.fillStyle = '#8B4513';
137
+ ctx.fillRect(ox + 25, oy + 6, 2, 10);
138
+ ctx.fillStyle = def.traitColor;
139
+ ctx.fillRect(ox + 24, oy + 4, 4, 3);
140
+ break;
141
+ case 'glasses':
142
+ ctx.fillStyle = '#FFD700';
143
+ ctx.fillRect(ox + 11, oy + 7, 6, 4);
144
+ ctx.fillRect(ox + 16, oy + 7, 6, 4);
145
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
146
+ ctx.fillRect(ox + 12, oy + 8, 4, 2);
147
+ ctx.fillRect(ox + 17, oy + 8, 4, 2);
148
+ ctx.fillStyle = '#FFD700';
149
+ ctx.fillRect(ox + 17, oy + 8, 1, 1);
150
+ break;
151
+ case 'arrow':
152
+ ctx.fillStyle = def.traitColor;
153
+ ctx.fillRect(ox + 25, oy + 4, 3, 8);
154
+ ctx.fillStyle = '#fff';
155
+ ctx.fillRect(ox + 24, oy + 3, 5, 3);
156
+ ctx.fillRect(ox + 25, oy + 2, 3, 2);
157
+ break;
158
+ case 'testtube':
159
+ ctx.fillStyle = '#ecf0f1';
160
+ ctx.fillRect(ox + 25, oy + 4, 3, 10);
161
+ ctx.fillStyle = '#2ecc71';
162
+ ctx.fillRect(ox + 25, oy + 10, 3, 4);
163
+ ctx.fillRect(ox + 24, oy + 13, 5, 2);
164
+ break;
165
+ case 'stopwatch':
166
+ ctx.fillStyle = def.traitColor;
167
+ ctx.fillRect(ox + 24, oy + 4, 6, 6);
168
+ ctx.fillStyle = '#fff';
169
+ ctx.fillRect(ox + 25, oy + 5, 4, 4);
170
+ ctx.fillStyle = '#333';
171
+ ctx.fillRect(ox + 27, oy + 5, 1, 3);
172
+ ctx.fillRect(ox + 26, oy + 3, 2, 2);
173
+ break;
174
+ case 'laptop':
175
+ ctx.fillStyle = '#333';
176
+ ctx.fillRect(ox + 23, oy + 8, 7, 5);
177
+ ctx.fillStyle = '#5dade2';
178
+ ctx.fillRect(ox + 24, oy + 9, 5, 3);
179
+ ctx.fillStyle = '#555';
180
+ ctx.fillRect(ox + 22, oy + 13, 9, 2);
181
+ break;
182
+ case 'hammer':
183
+ ctx.fillStyle = '#8B4513';
184
+ ctx.fillRect(ox + 26, oy + 6, 2, 10);
185
+ ctx.fillStyle = def.traitColor;
186
+ ctx.fillRect(ox + 23, oy + 3, 8, 4);
187
+ break;
188
+ }
189
+ };
190
+
191
+ const drawCheckmark = (ctx, ox, oy, frame) => {
192
+ const bounce = Math.max(0, 3 - frame);
193
+ ctx.fillStyle = '#2ecc71';
194
+ ctx.fillRect(ox + 10, oy - 6 - bounce, 12, 10);
195
+ ctx.fillStyle = '#fff';
196
+ // Checkmark shape
197
+ ctx.fillRect(ox + 13, oy - 2 - bounce, 2, 2);
198
+ ctx.fillRect(ox + 14, oy - 1 - bounce, 2, 2);
199
+ ctx.fillRect(ox + 15, oy - 2 - bounce, 2, 2);
200
+ ctx.fillRect(ox + 16, oy - 3 - bounce, 2, 2);
201
+ ctx.fillRect(ox + 17, oy - 4 - bounce, 2, 2);
202
+ };
203
+
204
+ /* ---- Sprite sheet generation ---- */
205
+
206
+ const generateSpriteSheet = (agentName) => {
207
+ const def = AGENT_DEFS[agentName];
208
+ if (!def) return null;
209
+
210
+ const canvas = document.createElement('canvas');
211
+ canvas.width = SHEET_W;
212
+ canvas.height = SHEET_H;
213
+ const ctx = canvas.getContext('2d');
214
+
215
+ // Row 0: IDLE (4 frames, head bob on frames 1,2)
216
+ for (let f = 0; f < 4; f++) {
217
+ const hb = (f === 1 || f === 2) ? -1 : 0;
218
+ drawCharacter(ctx, f * FRAME_SIZE, 0, def, hb, false, 0);
219
+ }
220
+
221
+ // Row 1: WORKING (4 frames, typing arm offset alternating)
222
+ for (let f = 0; f < 4; f++) {
223
+ const ao = (f % 2 === 0) ? -2 : 2;
224
+ drawCharacter(ctx, f * FRAME_SIZE, FRAME_SIZE, def, 0, true, ao);
225
+ }
226
+
227
+ // Row 2: MOVING (4 frames, walk cycle leg offset)
228
+ const legOffsets = [-2, 0, 2, 0];
229
+ for (let f = 0; f < 4; f++) {
230
+ drawWalkingCharacter(ctx, f * FRAME_SIZE, FRAME_SIZE * 2, def, legOffsets[f]);
231
+ }
232
+
233
+ // Row 3: DONE (4 frames, checkmark popup)
234
+ for (let f = 0; f < 4; f++) {
235
+ drawCharacter(ctx, f * FRAME_SIZE, FRAME_SIZE * 3, def, 0, false, 0);
236
+ drawCheckmark(ctx, f * FRAME_SIZE, FRAME_SIZE * 3, f);
237
+ }
238
+
239
+ return canvas;
240
+ };
241
+
242
+ /* ---- State manager ---- */
243
+
244
+ const STATUS_TO_ROW = {
245
+ idle: 0,
246
+ working: 1,
247
+ moving: 2,
248
+ done: 3,
249
+ };
250
+
251
+ const agentSprites = {};
252
+
253
+ const initAgentSprites = () => {
254
+ for (const name of Object.keys(AGENT_DEFS)) {
255
+ const pos = window.OfficeCanvas.getAgentDeskPosition(name);
256
+ if (!pos) continue;
257
+ agentSprites[name] = {
258
+ sheet: generateSpriteSheet(name),
259
+ x: pos.x,
260
+ y: pos.y,
261
+ status: 'idle',
262
+ frame: 0,
263
+ frameTimer: 0,
264
+ };
265
+ }
266
+ };
267
+
268
+ const updateAgentState = (name, status) => {
269
+ if (!agentSprites[name]) return;
270
+ agentSprites[name].status = status;
271
+ agentSprites[name].frame = 0;
272
+ agentSprites[name].frameTimer = 0;
273
+ };
274
+
275
+ const updateAgentSprites = (dt) => {
276
+ for (const sprite of Object.values(agentSprites)) {
277
+ sprite.frameTimer += dt;
278
+ const frameDuration = 1 / ANIM_FPS;
279
+ if (sprite.frameTimer >= frameDuration) {
280
+ sprite.frameTimer -= frameDuration;
281
+ sprite.frame = (sprite.frame + 1) % SHEET_COLS;
282
+ }
283
+ }
284
+ };
285
+
286
+ const STATUS_DOT_COLORS = {
287
+ idle: '#9E9E9E',
288
+ working: '#4CAF50',
289
+ done: '#2196F3',
290
+ };
291
+
292
+ const drawAgentSprites = (ctx) => {
293
+ for (const [name, sprite] of Object.entries(agentSprites)) {
294
+ const row = STATUS_TO_ROW[sprite.status] || 0;
295
+ const sx = sprite.frame * FRAME_SIZE;
296
+ const sy = row * FRAME_SIZE;
297
+
298
+ ctx.drawImage(
299
+ sprite.sheet,
300
+ sx, sy, FRAME_SIZE, FRAME_SIZE,
301
+ sprite.x - FRAME_SIZE / 2, sprite.y - FRAME_SIZE / 2,
302
+ FRAME_SIZE, FRAME_SIZE
303
+ );
304
+
305
+ // Status dot
306
+ const dotColor = STATUS_DOT_COLORS[sprite.status] || '#9E9E9E';
307
+ ctx.fillStyle = dotColor;
308
+ ctx.beginPath();
309
+ ctx.arc(sprite.x, sprite.y - FRAME_SIZE / 2 - 4, 3, 0, Math.PI * 2);
310
+ ctx.fill();
311
+
312
+ // Name label
313
+ ctx.fillStyle = '#E0E0E0';
314
+ ctx.font = '8px monospace';
315
+ ctx.textAlign = 'center';
316
+ const shortName = name.length > 12 ? name.substring(0, 11) + '\u2026' : name;
317
+ ctx.fillText(shortName, sprite.x, sprite.y + FRAME_SIZE / 2 + 8);
318
+ }
319
+ };
320
+
321
+ window.AgentSprites = {
322
+ AGENT_DEFS,
323
+ agentSprites,
324
+ initAgentSprites,
325
+ updateAgentState,
326
+ updateAgentSprites,
327
+ drawAgentSprites,
328
+ };
329
+ })();