erne-universal 0.10.24 → 0.10.25

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/lib/generate.js CHANGED
@@ -12,20 +12,20 @@ const VARIANT_MAP = {
12
12
  fields: ['state', 'serverState'],
13
13
  variants: {
14
14
  'zustand+tanstack-query': 'state-management/zustand-tanstack.md',
15
- 'zustand+none': 'state-management/zustand-only.md',
16
- 'zustand+swr': 'state-management/zustand-tanstack.md',
17
- 'redux-saga+none': 'state-management/redux-saga.md',
15
+ 'zustand+none': 'state-management/zustand-only.md',
16
+ 'zustand+swr': 'state-management/zustand-tanstack.md',
17
+ 'redux-saga+none': 'state-management/redux-saga.md',
18
18
  'redux-saga+tanstack-query': 'state-management/redux-saga.md',
19
- 'redux-toolkit+rtk-query':'state-management/redux-toolkit.md',
19
+ 'redux-toolkit+rtk-query': 'state-management/redux-toolkit.md',
20
20
  'redux-toolkit+tanstack-query': 'state-management/redux-toolkit.md',
21
- 'redux-toolkit+none': 'state-management/redux-toolkit.md',
21
+ 'redux-toolkit+none': 'state-management/redux-toolkit.md',
22
22
  },
23
23
  default: 'state-management/zustand-tanstack.md',
24
24
  },
25
25
  'rules/common/navigation.md': {
26
26
  fields: ['navigation'],
27
27
  variants: {
28
- 'expo-router': 'navigation/expo-router.md',
28
+ 'expo-router': 'navigation/expo-router.md',
29
29
  'react-navigation': 'navigation/react-navigation.md',
30
30
  },
31
31
  default: 'navigation/expo-router.md',
@@ -33,21 +33,21 @@ const VARIANT_MAP = {
33
33
  'rules/common/performance.md': {
34
34
  fields: ['lists', 'images'],
35
35
  variants: {
36
- 'flashlist+expo-image': 'performance/modern.md',
37
- 'flashlist+rn-image': 'performance/modern.md',
38
- 'flashlist+fast-image': 'performance/modern.md',
39
- 'flatlist+expo-image': 'performance/modern.md',
40
- 'flatlist+rn-image': 'performance/legacy.md',
41
- 'flatlist+fast-image': 'performance/legacy.md',
36
+ 'flashlist+expo-image': 'performance/modern.md',
37
+ 'flashlist+rn-image': 'performance/modern.md',
38
+ 'flashlist+fast-image': 'performance/modern.md',
39
+ 'flatlist+expo-image': 'performance/modern.md',
40
+ 'flatlist+rn-image': 'performance/legacy.md',
41
+ 'flatlist+fast-image': 'performance/legacy.md',
42
42
  },
43
43
  default: 'performance/modern.md',
44
44
  },
45
45
  'rules/common/coding-style.md': {
46
46
  fields: ['componentStyle'],
47
47
  variants: {
48
- 'functional': 'coding-style/functional.md',
49
- 'class': 'coding-style/mixed.md',
50
- 'mixed': 'coding-style/mixed.md',
48
+ functional: 'coding-style/functional.md',
49
+ class: 'coding-style/mixed.md',
50
+ mixed: 'coding-style/mixed.md',
51
51
  },
52
52
  default: 'coding-style/functional.md',
53
53
  },
@@ -55,72 +55,72 @@ const VARIANT_MAP = {
55
55
  fields: ['storage'],
56
56
  variants: {
57
57
  'expo-secure-store': 'security/expo-secure.md',
58
- 'rn-keychain': 'security/rn-keychain.md',
59
- 'async-storage': 'security/async-storage.md',
58
+ 'rn-keychain': 'security/rn-keychain.md',
59
+ 'async-storage': 'security/async-storage.md',
60
60
  },
61
61
  default: 'security/async-storage.md',
62
62
  },
63
63
  'rules/common/styling.md': {
64
64
  fields: ['styling'],
65
65
  variants: {
66
- 'stylesheet': 'styling/stylesheet.md',
67
- 'nativewind': 'styling/nativewind.md',
68
- 'tamagui': 'styling/stylesheet.md',
69
- 'unistyles': 'styling/stylesheet.md',
66
+ stylesheet: 'styling/stylesheet.md',
67
+ nativewind: 'styling/nativewind.md',
68
+ tamagui: 'styling/stylesheet.md',
69
+ unistyles: 'styling/stylesheet.md',
70
70
  },
71
71
  default: 'styling/stylesheet.md',
72
72
  },
73
73
  'agents/ui-designer.md': {
74
74
  fields: ['styling'],
75
75
  variants: {
76
- 'stylesheet': 'ui-designer/stylesheet.md',
77
- 'nativewind': 'ui-designer/nativewind.md',
78
- 'tamagui': 'ui-designer/stylesheet.md',
79
- 'unistyles': 'ui-designer/stylesheet.md',
76
+ stylesheet: 'ui-designer/stylesheet.md',
77
+ nativewind: 'ui-designer/nativewind.md',
78
+ tamagui: 'ui-designer/stylesheet.md',
79
+ unistyles: 'ui-designer/stylesheet.md',
80
80
  },
81
81
  default: 'ui-designer/stylesheet.md',
82
82
  },
83
83
  'agents/architect.md': {
84
84
  fields: ['state', 'hasMonorepo'],
85
85
  variants: {
86
- 'zustand+false': 'architect/zustand.md',
87
- 'zustand+true': 'architect/monorepo.md',
86
+ 'zustand+false': 'architect/zustand.md',
87
+ 'zustand+true': 'architect/monorepo.md',
88
88
  'redux-toolkit+false': 'architect/redux.md',
89
- 'redux-toolkit+true': 'architect/monorepo.md',
90
- 'redux-saga+false': 'architect/redux.md',
91
- 'redux-saga+true': 'architect/monorepo.md',
92
- 'mobx+false': 'architect/zustand.md',
93
- 'mobx+true': 'architect/monorepo.md',
89
+ 'redux-toolkit+true': 'architect/monorepo.md',
90
+ 'redux-saga+false': 'architect/redux.md',
91
+ 'redux-saga+true': 'architect/monorepo.md',
92
+ 'mobx+false': 'architect/zustand.md',
93
+ 'mobx+true': 'architect/monorepo.md',
94
94
  },
95
95
  default: 'architect/zustand.md',
96
96
  },
97
97
  'agents/senior-developer.md': {
98
98
  fields: ['framework', 'state'],
99
99
  variants: {
100
- 'expo-managed+zustand': 'senior-developer/modern-expo.md',
100
+ 'expo-managed+zustand': 'senior-developer/modern-expo.md',
101
101
  'expo-managed+redux-toolkit': 'senior-developer/legacy-bare.md',
102
- 'expo-managed+redux-saga': 'senior-developer/legacy-bare.md',
103
- 'expo-bare+zustand': 'senior-developer/modern-expo.md',
104
- 'expo-bare+redux-toolkit': 'senior-developer/legacy-bare.md',
105
- 'expo-bare+redux-saga': 'senior-developer/legacy-bare.md',
106
- 'bare-rn+redux-saga': 'senior-developer/legacy-bare.md',
107
- 'bare-rn+redux-toolkit': 'senior-developer/legacy-bare.md',
108
- 'bare-rn+zustand': 'senior-developer/modern-expo.md',
102
+ 'expo-managed+redux-saga': 'senior-developer/legacy-bare.md',
103
+ 'expo-bare+zustand': 'senior-developer/modern-expo.md',
104
+ 'expo-bare+redux-toolkit': 'senior-developer/legacy-bare.md',
105
+ 'expo-bare+redux-saga': 'senior-developer/legacy-bare.md',
106
+ 'bare-rn+redux-saga': 'senior-developer/legacy-bare.md',
107
+ 'bare-rn+redux-toolkit': 'senior-developer/legacy-bare.md',
108
+ 'bare-rn+zustand': 'senior-developer/modern-expo.md',
109
109
  },
110
110
  default: 'senior-developer/modern-expo.md',
111
111
  },
112
112
  'agents/feature-builder.md': {
113
113
  fields: ['framework', 'state'],
114
114
  variants: {
115
- 'expo-managed+zustand': 'feature-builder/modern-expo.md',
115
+ 'expo-managed+zustand': 'feature-builder/modern-expo.md',
116
116
  'expo-managed+redux-toolkit': 'feature-builder/legacy-bare.md',
117
- 'expo-managed+redux-saga': 'feature-builder/legacy-bare.md',
118
- 'expo-bare+zustand': 'feature-builder/modern-expo.md',
119
- 'expo-bare+redux-toolkit': 'feature-builder/legacy-bare.md',
120
- 'expo-bare+redux-saga': 'feature-builder/legacy-bare.md',
121
- 'bare-rn+redux-saga': 'feature-builder/legacy-bare.md',
122
- 'bare-rn+redux-toolkit': 'feature-builder/legacy-bare.md',
123
- 'bare-rn+zustand': 'feature-builder/modern-expo.md',
117
+ 'expo-managed+redux-saga': 'feature-builder/legacy-bare.md',
118
+ 'expo-bare+zustand': 'feature-builder/modern-expo.md',
119
+ 'expo-bare+redux-toolkit': 'feature-builder/legacy-bare.md',
120
+ 'expo-bare+redux-saga': 'feature-builder/legacy-bare.md',
121
+ 'bare-rn+redux-saga': 'feature-builder/legacy-bare.md',
122
+ 'bare-rn+redux-toolkit': 'feature-builder/legacy-bare.md',
123
+ 'bare-rn+zustand': 'feature-builder/modern-expo.md',
124
124
  },
125
125
  default: 'feature-builder/modern-expo.md',
126
126
  },
@@ -132,12 +132,14 @@ function selectVariant(targetPath, detection) {
132
132
  const mapping = VARIANT_MAP[targetPath];
133
133
  if (!mapping) return null;
134
134
 
135
- const key = mapping.fields.map(field => {
136
- if (field === 'componentStyle') return detection.componentStyle;
137
- if (field === 'hasMonorepo') return String(detection.hasMonorepo);
138
- if (field === 'framework') return detection.framework;
139
- return detection.stack?.[field];
140
- }).join('+');
135
+ const key = mapping.fields
136
+ .map((field) => {
137
+ if (field === 'componentStyle') return detection.componentStyle;
138
+ if (field === 'hasMonorepo') return String(detection.hasMonorepo);
139
+ if (field === 'framework') return detection.framework;
140
+ return detection.stack?.[field];
141
+ })
142
+ .join('+');
141
143
 
142
144
  return mapping.variants[key] || mapping.default;
143
145
  }
@@ -146,8 +148,10 @@ function selectVariant(targetPath, detection) {
146
148
 
147
149
  function determineRuleLayers(detection, cwd) {
148
150
  const layers = ['common'];
149
- if (detection.framework === 'expo-managed' || detection.framework === 'expo-bare') layers.push('expo');
150
- if (detection.framework === 'bare-rn' || detection.framework === 'expo-bare') layers.push('bare-rn');
151
+ if (detection.framework === 'expo-managed' || detection.framework === 'expo-bare')
152
+ layers.push('expo');
153
+ if (detection.framework === 'bare-rn' || detection.framework === 'expo-bare')
154
+ layers.push('bare-rn');
151
155
 
152
156
  // Add native rule layers for bare projects with native directories
153
157
  if (cwd && (detection.framework === 'bare-rn' || detection.framework === 'expo-bare')) {
@@ -162,9 +166,22 @@ function determineRuleLayers(detection, cwd) {
162
166
  // Converts ERNE's flat hook array into Claude Code's settings.local.json format.
163
167
  // Claude Code reads hooks from the "hooks" key in settings files, NOT from hooks.json.
164
168
 
169
+ const HOOK_TIMEOUTS = {
170
+ 'session-start.js': 10,
171
+ 'post-edit-typecheck.js': 60,
172
+ 'pre-edit-test-gate.js': 60,
173
+ 'audit-refresh.js': 60,
174
+ 'post-edit-format.js': 30,
175
+ 'bundle-size-check.js': 30,
176
+ 'pre-commit-lint.js': 30,
177
+ 'security-scan.js': 30,
178
+ 'evaluate-session.js': 30,
179
+ };
180
+ const DEFAULT_HOOK_TIMEOUT = 15;
181
+
165
182
  function convertToClaudeCodeHooks(erneHooks, profileName) {
166
183
  // erneHooks is { hooks: [{ event, pattern?, script, command, profiles }] }
167
- const activeHooks = erneHooks.hooks.filter(h => {
184
+ const activeHooks = erneHooks.hooks.filter((h) => {
168
185
  const hookProfiles = h.profiles || ['minimal', 'standard', 'strict'];
169
186
  return hookProfiles.includes(profileName);
170
187
  });
@@ -190,10 +207,10 @@ function convertToClaudeCodeHooks(erneHooks, profileName) {
190
207
  for (const [pattern, patternHooks] of Object.entries(byPattern)) {
191
208
  const entry = {};
192
209
  if (pattern) entry.matcher = pattern;
193
- entry.hooks = patternHooks.map(h => ({
210
+ entry.hooks = patternHooks.map((h) => ({
194
211
  type: 'command',
195
212
  command: 'node .claude/hooks/scripts/run-with-flags.js ' + h.script,
196
- timeout: 5,
213
+ timeout: HOOK_TIMEOUTS[h.script] || DEFAULT_HOOK_TIMEOUT,
197
214
  }));
198
215
  result[event].push(entry);
199
216
  }
@@ -210,7 +227,7 @@ function mergeHookProfile(masterHooks, profileHooks, profileName) {
210
227
  // - event-keyed format: { PreToolUse: [...], PostToolUse: [...], _meta: {...} }
211
228
  if (Array.isArray(masterHooks.hooks)) {
212
229
  // Flat array format — filter by profile, then group by event
213
- const filtered = masterHooks.hooks.filter(hook => {
230
+ const filtered = masterHooks.hooks.filter((hook) => {
214
231
  const hookProfiles = hook.profiles || ['minimal', 'standard', 'strict'];
215
232
  return hookProfiles.includes(profileName);
216
233
  });
@@ -232,7 +249,7 @@ function mergeHookProfile(masterHooks, profileHooks, profileName) {
232
249
  }
233
250
 
234
251
  if (Array.isArray(hooks)) {
235
- result[event] = hooks.filter(hook => {
252
+ result[event] = hooks.filter((hook) => {
236
253
  const hookProfiles = hook.profiles || ['minimal', 'standard', 'strict'];
237
254
  return hookProfiles.includes(profileName);
238
255
  });
@@ -293,7 +310,7 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
293
310
  // Copy commands as Claude Code skills: commands/foo.md → skills/foo/SKILL.md
294
311
  const commandsSrc = path.join(erneRoot, 'commands');
295
312
  if (fs.existsSync(commandsSrc)) {
296
- const commandFiles = fs.readdirSync(commandsSrc).filter(f => f.endsWith('.md'));
313
+ const commandFiles = fs.readdirSync(commandsSrc).filter((f) => f.endsWith('.md'));
297
314
  for (const file of commandFiles) {
298
315
  const name = path.basename(file, '.md');
299
316
  const skillDir = path.join(targetDir, 'skills', name);
@@ -352,7 +369,12 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
352
369
  const agentFiles = fs.readdirSync(agentsSrc);
353
370
  for (const file of agentFiles) {
354
371
  // Skip native-bridge-builder unless bare-rn
355
- if (file === 'native-bridge-builder.md' && detection.framework !== 'bare-rn' && detection.framework !== 'expo-bare') continue;
372
+ if (
373
+ file === 'native-bridge-builder.md' &&
374
+ detection.framework !== 'bare-rn' &&
375
+ detection.framework !== 'expo-bare'
376
+ )
377
+ continue;
356
378
  // Skip expo-config-resolver if bare-rn
357
379
  if (file === 'expo-config-resolver.md' && detection.framework === 'bare-rn') continue;
358
380
 
@@ -410,7 +432,11 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
410
432
  const settingsLocalPath = path.join(targetDir, 'settings.local.json');
411
433
  let settingsLocal = {};
412
434
  if (fs.existsSync(settingsLocalPath)) {
413
- try { settingsLocal = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); } catch { /* fresh */ }
435
+ try {
436
+ settingsLocal = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8'));
437
+ } catch {
438
+ /* fresh */
439
+ }
414
440
  }
415
441
  settingsLocal.hooks = claudeCodeHooks;
416
442
  fs.writeFileSync(settingsLocalPath, JSON.stringify(settingsLocal, null, 2) + '\n');
@@ -440,7 +466,9 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
440
466
  if (mcpConfig.env) serverEntry.env = mcpConfig.env;
441
467
  if (mcpConfig.url) serverEntry.url = mcpConfig.url;
442
468
  mcpServers[sel] = serverEntry;
443
- } catch { /* skip invalid json */ }
469
+ } catch {
470
+ /* skip invalid json */
471
+ }
444
472
  }
445
473
  }
446
474
  }
@@ -450,7 +478,11 @@ function generateConfig(erneRoot, targetDir, detection, profile, mcpSelections)
450
478
  const mcpJsonPath = path.join(projectRoot, '.mcp.json');
451
479
  let existingMcp = {};
452
480
  if (fs.existsSync(mcpJsonPath)) {
453
- try { existingMcp = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8')); } catch { /* fresh */ }
481
+ try {
482
+ existingMcp = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
483
+ } catch {
484
+ /* fresh */
485
+ }
454
486
  }
455
487
  if (!existingMcp.mcpServers) existingMcp.mcpServers = {};
456
488
  // Merge — don't overwrite existing servers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erne-universal",
3
- "version": "0.10.24",
3
+ "version": "0.10.25",
4
4
  "description": "Complete AI coding agent harness for React Native and Expo development",
5
5
  "keywords": [
6
6
  "react-native",
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  const { execFileSync } = require('child_process');
3
- const { readStdin, pass, fail, warn } = require('./lib/hook-utils');
3
+ const { readStdin, pass, warn } = require('./lib/hook-utils');
4
4
 
5
5
  const input = readStdin();
6
6
  const command = (input.tool_input && input.tool_input.command) || '';
@@ -18,7 +18,8 @@ try {
18
18
  } catch (err) {
19
19
  const output = err.stdout || err.stderr || '';
20
20
  if (output.includes('error') || output.includes('warning')) {
21
- return fail(`ERNE: Lint errors found. Fix before committing:\n${output.slice(0, 500)}`);
21
+ console.error(`ERNE: Lint errors found. Fix before committing:\n${output.slice(0, 500)}`);
22
+ return pass();
22
23
  }
23
24
  if (err.status === 127 || output.includes('not found')) {
24
25
  return pass('ERNE: ESLint not available, skipping lint check');
@@ -31,9 +31,7 @@ function resolveProfile() {
31
31
  for (const mdPath of claudeMdPaths) {
32
32
  try {
33
33
  const content = fs.readFileSync(mdPath, 'utf8');
34
- const match = content.match(
35
- /<!--\s*Hook Profile:\s*(minimal|standard|strict)\s*-->/i
36
- );
34
+ const match = content.match(/<!--\s*Hook Profile:\s*(minimal|standard|strict)\s*-->/i);
37
35
  if (match) return match[1].toLowerCase();
38
36
  } catch {}
39
37
  }
@@ -63,7 +61,9 @@ function loadHooksConfig() {
63
61
  for (const candidate of candidates) {
64
62
  try {
65
63
  return JSON.parse(fs.readFileSync(candidate, 'utf8'));
66
- } catch { /* try next */ }
64
+ } catch {
65
+ /* try next */
66
+ }
67
67
  }
68
68
 
69
69
  return { hooks: [] };
@@ -73,7 +73,7 @@ const profile = resolveProfile();
73
73
  const config = loadHooksConfig();
74
74
 
75
75
  // Find hook entry in config
76
- const hookEntry = config.hooks.find(h => h.script === HOOK_SCRIPT);
76
+ const hookEntry = config.hooks.find((h) => h.script === HOOK_SCRIPT);
77
77
  if (!hookEntry) {
78
78
  process.exit(0);
79
79
  }
@@ -99,7 +99,7 @@ const result = spawnSync('node', [scriptPath], {
99
99
  input: stdinData,
100
100
  encoding: 'utf8',
101
101
  stdio: ['pipe', 'pipe', 'pipe'],
102
- timeout: 30000,
102
+ timeout: 120000,
103
103
  env,
104
104
  });
105
105
 
@@ -109,7 +109,7 @@ if (result.stdout) process.stdout.write(result.stdout);
109
109
  if (result.stderr) process.stderr.write(result.stderr);
110
110
 
111
111
  // Report hook execution metrics to dashboard (fire-and-forget)
112
- if (process.env.ERNE_DASHBOARD_PORT || process.env.ERNE_HOOK_CHAIN) {
112
+ if (process.env.ERNE_DASHBOARD_PORT) {
113
113
  try {
114
114
  const http = require('http');
115
115
  const { resolveDashboardPort } = require('./lib/port-registry');
@@ -122,14 +122,15 @@ if (process.env.ERNE_DASHBOARD_PORT || process.env.ERNE_HOOK_CHAIN) {
122
122
  duration_ms: durationMs,
123
123
  exit_code: result.status ?? 0,
124
124
  skipped: false,
125
- }
125
+ },
126
126
  });
127
127
  const req = http.request({
128
- hostname: '127.0.0.1', port,
128
+ hostname: '127.0.0.1',
129
+ port,
129
130
  path: '/api/events',
130
131
  method: 'POST',
131
132
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
132
- timeout: 300
133
+ timeout: 300,
133
134
  });
134
135
  req.on('error', () => {});
135
136
  req.write(payload);
@@ -138,7 +139,7 @@ if (process.env.ERNE_DASHBOARD_PORT || process.env.ERNE_HOOK_CHAIN) {
138
139
  }
139
140
 
140
141
  if (result.signal === 'SIGTERM') {
141
- console.error('ERNE: hook timed out after 30s');
142
+ console.error('ERNE: hook timed out after 120s');
142
143
  process.exit(2);
143
144
  }
144
145
 
@@ -26,9 +26,7 @@ function findFilesWithExt(dir, ext) {
26
26
  withFileTypes: true,
27
27
  recursive: true,
28
28
  });
29
- return entries.some(
30
- e => e.isFile() && e.name.endsWith(ext)
31
- );
29
+ return entries.some((e) => e.isFile() && e.name.endsWith(ext));
32
30
  } catch {
33
31
  return false;
34
32
  }
@@ -85,15 +83,19 @@ try {
85
83
  hasSettings = true;
86
84
  profile = settings.profile || 'standard';
87
85
  version = settings.erneVersion || '';
88
- } catch { /* no settings */ }
86
+ } catch {
87
+ /* no settings */
88
+ }
89
89
 
90
90
  // Count agents
91
91
  try {
92
92
  const agentDir = path.join(projectDir, '.claude', 'agents');
93
93
  if (fs.existsSync(agentDir)) {
94
- agentCount = fs.readdirSync(agentDir).filter(f => f.endsWith('.md')).length;
94
+ agentCount = fs.readdirSync(agentDir).filter((f) => f.endsWith('.md')).length;
95
95
  }
96
- } catch { /* skip */ }
96
+ } catch {
97
+ /* skip */
98
+ }
97
99
 
98
100
  // ─── Dashboard detection (no auto-start — use `npx erne-universal dashboard`) ─
99
101
 
@@ -106,7 +108,9 @@ if (hasSettings) {
106
108
  if (existingPort) {
107
109
  dashboardUrl = `http://localhost:${existingPort}`;
108
110
  }
109
- } catch { /* port-registry not available — skip */ }
111
+ } catch {
112
+ /* port-registry not available — skip */
113
+ }
110
114
  }
111
115
 
112
116
  // ─── Print banner ────────────────────────────────────────────────────────────
@@ -118,7 +122,7 @@ if (hasSettings) {
118
122
  parts.push(profile);
119
123
  if (agentCount > 0) parts.push(`${agentCount} agents`);
120
124
  if (dashboardUrl) parts.push(`Dashboard: ${dashboardUrl}`);
121
- console.log(`ERNE ${parts.join(' | ')}`);
125
+ console.error(`ERNE ${parts.join(' | ')}`);
122
126
  } else {
123
127
  // Fallback: layer-based output for projects without full ERNE init
124
128
  const parts = [];
@@ -126,8 +130,8 @@ if (hasSettings) {
126
130
  parts.push(`${profile} profile`);
127
131
  if (agentCount > 0) parts.push(`${agentCount} agents`);
128
132
  parts.push(`layers: ${layers.join(', ')}`);
129
- console.log(`ERNE ${parts.join(' | ')}`);
130
- console.log(`Use /erne- commands (e.g. /erne-plan, /erne-perf, /erne-doctor)`);
133
+ console.error(`ERNE ${parts.join(' | ')}`);
134
+ console.error(`Use /erne- commands (e.g. /erne-plan, /erne-perf, /erne-doctor)`);
131
135
  }
132
136
 
133
137
  if (!hasSignals) {