@jaguilar87/gaia-ops 5.0.0-beta.4 → 5.0.0-beta.5

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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "gaia-security",
10
10
  "description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
11
- "version": "5.0.0-beta.4",
11
+ "version": "5.0.0-beta.5",
12
12
  "source": "./dist/gaia-security"
13
13
  }
14
14
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaia-ops",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.5",
4
4
  "description": "Security-first orchestrator with specialized agents, hooks, and governance for AI coding",
5
5
  "author": {
6
6
  "name": "jaguilar87"
@@ -6,6 +6,25 @@
6
6
  * Verifies the complete Gaia-Ops installation is healthy.
7
7
  * Run after install, update, or when things seem broken.
8
8
  *
9
+ * Checks (in order):
10
+ * 1. Gaia-Ops version - package.json readable
11
+ * 2. Claude Code - CLI installed (prerequisite)
12
+ * 3. Python - Python 3.9+ available (hooks need it)
13
+ * 4. Plugin mode - ops vs security, registry valid
14
+ * 5. Symlinks - .claude/ symlinks resolve to package content
15
+ * 6. Identity - orchestrator agent configured in settings
16
+ * 7. Settings - hooks registered, permissions, deny rules
17
+ * 8. Hook files - all hook scripts present on disk
18
+ * 9. project-context - project-context.json valid and enriched
19
+ * 10. Project dirs - paths declared in context exist
20
+ * 11. Memory dirs - speckit, episodic memory dirs present
21
+ *
22
+ * Severity levels:
23
+ * pass - check passed
24
+ * info - informational, not an issue
25
+ * warning - degrades functionality
26
+ * error - critical, Gaia will not work
27
+ *
9
28
  * Usage:
10
29
  * npx gaia-doctor # Full health check
11
30
  * npx gaia-doctor --fix # Attempt auto-fix for common issues
@@ -28,6 +47,17 @@ const __filename = fileURLToPath(import.meta.url);
28
47
  const __dirname = dirname(__filename);
29
48
  const CWD = process.cwd();
30
49
 
50
+ // ============================================================================
51
+ // Severity helpers
52
+ // ============================================================================
53
+
54
+ /** Create a check result with explicit severity. */
55
+ function result(name, severity, detail, fix = null) {
56
+ // Backward-compatible `ok` field: pass and info are considered ok
57
+ const ok = severity === 'pass' || severity === 'info';
58
+ return { name, severity, ok, detail, ...(fix ? { fix } : {}) };
59
+ }
60
+
31
61
  // ============================================================================
32
62
  // Health Checks
33
63
  // ============================================================================
@@ -35,111 +65,180 @@ const CWD = process.cwd();
35
65
  async function checkGaiaVersion() {
36
66
  try {
37
67
  const pkg = JSON.parse(await fs.readFile(join(__dirname, '..', 'package.json'), 'utf-8'));
38
- return { name: 'Gaia-Ops', ok: true, detail: `v${pkg.version}` };
68
+ return result('Gaia-Ops', 'pass', `v${pkg.version}`);
69
+ } catch {
70
+ return result('Gaia-Ops', 'error', 'Version unknown', 'Reinstall @jaguilar87/gaia-ops');
71
+ }
72
+ }
73
+
74
+ async function checkPluginMode() {
75
+ // Determine plugin mode (ops vs security) and verify the registry.
76
+ const registryPath = join(CWD, '.claude', 'plugin-registry.json');
77
+
78
+ if (!existsSync(registryPath)) {
79
+ return result('Plugin mode', 'warning', 'No plugin-registry.json', 'Run gaia-scan or restart Claude Code');
80
+ }
81
+
82
+ try {
83
+ const registry = JSON.parse(await fs.readFile(registryPath, 'utf-8'));
84
+ const installed = (registry.installed || []).map(p => p.name);
85
+ const source = registry.source || 'unknown';
86
+
87
+ if (installed.includes('gaia-ops')) {
88
+ return result('Plugin mode', 'pass', `ops (source: ${source})`);
89
+ } else if (installed.includes('gaia-security')) {
90
+ return result('Plugin mode', 'pass', `security (source: ${source})`);
91
+ } else {
92
+ return result('Plugin mode', 'warning', `Unknown plugin: ${installed.join(', ')}`, 'Verify installation');
93
+ }
39
94
  } catch {
40
- return { name: 'Gaia-Ops', ok: false, detail: 'Version unknown', fix: 'Reinstall @jaguilar87/gaia-ops' };
95
+ return result('Plugin mode', 'warning', 'Invalid plugin-registry.json', 'Delete and restart Claude Code');
41
96
  }
42
97
  }
43
98
 
44
99
  async function checkSymlinks() {
45
100
  const names = ['agents', 'tools', 'hooks', 'commands', 'templates', 'config', 'speckit', 'skills', 'CHANGELOG.md'];
46
- const results = [];
101
+ // Critical symlinks that break core functionality if missing
102
+ const critical = new Set(['agents', 'hooks', 'skills']);
103
+ const sub = [];
47
104
  let valid = 0;
105
+ let hasCriticalMissing = false;
48
106
 
49
107
  for (const name of names) {
50
108
  const linkPath = join(CWD, '.claude', name);
51
109
  const exists = existsSync(linkPath);
52
110
 
53
111
  if (exists) {
54
- // Verify symlink target actually resolves
55
112
  try {
56
113
  await fs.realpath(linkPath);
57
114
  valid++;
58
- results.push({ name, status: 'ok' });
115
+ sub.push({ name, status: 'ok' });
59
116
  } catch {
60
- results.push({ name, status: 'broken', fix: `rm .claude/${name} && gaia-scan` });
117
+ if (critical.has(name)) hasCriticalMissing = true;
118
+ sub.push({ name, status: 'broken', fix: `rm .claude/${name} && gaia-scan` });
61
119
  }
62
120
  } else {
63
- results.push({ name, status: 'missing', fix: 'Run gaia-scan to recreate' });
121
+ if (critical.has(name)) hasCriticalMissing = true;
122
+ sub.push({ name, status: 'missing', fix: 'Run gaia-scan to recreate' });
64
123
  }
65
124
  }
66
125
 
126
+ if (valid === names.length) {
127
+ return { ...result('Symlinks', 'pass', `${valid}/${names.length} valid`), sub };
128
+ }
129
+
130
+ const severity = hasCriticalMissing ? 'error' : 'warning';
67
131
  return {
68
- name: 'Symlinks',
69
- ok: valid === names.length,
70
- detail: `${valid}/${names.length} valid`,
71
- fix: valid < names.length ? 'Run gaia-scan to recreate symlinks' : null,
72
- sub: results
132
+ ...result('Symlinks', severity, `${valid}/${names.length} valid`, 'Run gaia-scan to recreate symlinks'),
133
+ sub
73
134
  };
74
135
  }
75
136
 
76
- async function checkClaudeMd() {
77
- // CLAUDE.md is no longer generated -- identity is injected by UserPromptSubmit hook.
78
- // If a project still has a CLAUDE.md from a previous version, that's fine but not required.
79
- const path = join(CWD, 'CLAUDE.md');
80
- if (existsSync(path)) {
81
- return { name: 'CLAUDE.md', ok: true, detail: 'Present (legacy -- identity now injected by hook)' };
137
+ async function checkIdentity() {
138
+ // Identity is defined in the orchestrator agent definition (.md file) and
139
+ // activated by the `agent` field in settings.local.json. CLAUDE.md is no
140
+ // longer used and should not be present.
141
+ const issues = [];
142
+ const infos = [];
143
+
144
+ // 1. Check that orchestrator agent definition exists
145
+ const agentPath = join(CWD, '.claude', 'agents', 'gaia-orchestrator.md');
146
+ if (!existsSync(agentPath)) {
147
+ issues.push('gaia-orchestrator.md not found');
82
148
  }
83
- return { name: 'CLAUDE.md', ok: true, detail: 'Not present (identity injected by UserPromptSubmit hook)' };
149
+
150
+ // 2. Check that settings.local.json has `agent` field pointing to orchestrator
151
+ const localSettingsPath = join(CWD, '.claude', 'settings.local.json');
152
+ if (existsSync(localSettingsPath)) {
153
+ try {
154
+ const data = JSON.parse(await fs.readFile(localSettingsPath, 'utf-8'));
155
+ if (data.agent === 'gaia-orchestrator') {
156
+ // pass -- correct agent configured
157
+ } else if (data.agent) {
158
+ issues.push(`Agent set to "${data.agent}" (expected "gaia-orchestrator")`);
159
+ } else {
160
+ issues.push('No agent field in settings.local.json');
161
+ }
162
+ } catch { /* handled by settings check */ }
163
+ } else {
164
+ issues.push('settings.local.json missing');
165
+ }
166
+
167
+ // 3. Warn if legacy CLAUDE.md is still present
168
+ const claudeMdPath = join(CWD, 'CLAUDE.md');
169
+ if (existsSync(claudeMdPath)) {
170
+ infos.push('Legacy CLAUDE.md present (no longer used)');
171
+ }
172
+
173
+ if (issues.length > 0) {
174
+ return result('Identity', 'error', issues.join('; '), 'Run gaia-scan or npx gaia-update');
175
+ }
176
+
177
+ if (infos.length > 0) {
178
+ return result('Identity', 'info', `Orchestrator configured -- ${infos.join('; ')}`);
179
+ }
180
+
181
+ return result('Identity', 'pass', 'Orchestrator agent configured');
84
182
  }
85
183
 
86
- async function checkSettingsJson() {
87
- const path = join(CWD, '.claude', 'settings.json');
184
+ async function checkSettings() {
185
+ // Configuration lives in settings.local.json (hooks, permissions, agent, env).
186
+ // settings.json exists for Claude Code to detect the project but may be empty.
187
+ const localPath = join(CWD, '.claude', 'settings.local.json');
88
188
 
89
- if (!existsSync(path)) {
90
- return { name: 'settings.json', ok: false, detail: 'Missing', fix: 'Run gaia-scan' };
189
+ if (!existsSync(localPath)) {
190
+ return result('Settings', 'error', 'settings.local.json missing', 'Run gaia-scan or npx gaia-update');
91
191
  }
92
192
 
93
193
  try {
94
- const data = JSON.parse(await fs.readFile(path, 'utf-8'));
194
+ const data = JSON.parse(await fs.readFile(localPath, 'utf-8'));
95
195
  const issues = [];
196
+ const infos = [];
96
197
 
97
- // Check hooks are configured — hooks may live in settings.json OR
98
- // settings.local.json (gaia-update/gaia-scan puts them in local).
99
- let hooksConfig = data.hooks || null;
100
- const localPath = join(CWD, '.claude', 'settings.local.json');
101
- if (!hooksConfig && existsSync(localPath)) {
102
- try {
103
- const localData = JSON.parse(await fs.readFile(localPath, 'utf-8'));
104
- if (localData.hooks) hooksConfig = localData.hooks;
105
- } catch { /* ignore parse errors */ }
106
- }
107
-
198
+ // Check hooks configuration
199
+ const hooksConfig = data.hooks || null;
108
200
  if (!hooksConfig) {
109
- issues.push('No hooks configured (check settings.json and settings.local.json)');
201
+ issues.push('No hooks configured');
110
202
  } else {
111
203
  const hookTypes = Object.keys(hooksConfig);
112
- if (!hookTypes.includes('PreToolUse')) issues.push('Missing PreToolUse hook');
113
- if (!hookTypes.includes('PostToolUse')) issues.push('Missing PostToolUse hook');
204
+ const required = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart'];
205
+ const missing = required.filter(h => !hookTypes.includes(h));
206
+ if (missing.length > 0) {
207
+ issues.push(`Missing hooks: ${missing.join(', ')}`);
208
+ }
114
209
  }
115
210
 
116
- // Check permissions — now live in settings.local.json (not settings.json)
117
- // localPath already declared above for hooks check
118
- let permCount = 0;
119
- if (existsSync(localPath)) {
120
- try {
121
- const localData = JSON.parse(await fs.readFile(localPath, 'utf-8'));
122
- if (localData.permissions) {
123
- permCount = Object.values(localData.permissions).flat().length;
124
- }
125
- } catch { /* ignore parse errors */ }
211
+ // Check permissions
212
+ const perms = data.permissions || {};
213
+ const allowCount = (perms.allow || []).length;
214
+ const denyCount = (perms.deny || []).length;
215
+ if (allowCount === 0) {
216
+ infos.push('No allow rules (tools will prompt for approval)');
126
217
  }
127
- // Also count permissions in settings.json (legacy installs)
128
- if (data.permissions) {
129
- permCount += Object.values(data.permissions).flat().length;
218
+ if (denyCount === 0) {
219
+ issues.push('No deny rules (destructive commands not blocked)');
130
220
  }
131
- if (permCount === 0) {
132
- issues.push('No permissions configured (check settings.local.json)');
221
+
222
+ // Check env vars
223
+ const env = data.env || {};
224
+ if (!env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) {
225
+ infos.push('AGENT_TEAMS env not set');
133
226
  }
134
227
 
135
228
  if (issues.length > 0) {
136
- return { name: 'settings.json', ok: false, detail: issues.join('; '), fix: 'Run gaia-scan or npx gaia-update' };
229
+ return result('Settings', 'error', issues.join('; '), 'Run gaia-scan or npx gaia-update');
137
230
  }
138
231
 
139
232
  const hookCount = hooksConfig ? Object.keys(hooksConfig).length : 0;
140
- return { name: 'settings.json', ok: true, detail: `${hookCount} hook types, ${permCount} rules` };
233
+ const permCount = allowCount + denyCount;
234
+
235
+ if (infos.length > 0) {
236
+ return result('Settings', 'info', `${hookCount} hook types, ${permCount} rules -- ${infos.join('; ')}`);
237
+ }
238
+
239
+ return result('Settings', 'pass', `${hookCount} hook types, ${permCount} rules`);
141
240
  } catch {
142
- return { name: 'settings.json', ok: false, detail: 'Invalid JSON', fix: 'Delete and run gaia-scan' };
241
+ return result('Settings', 'error', 'Invalid JSON in settings.local.json', 'Delete and run gaia-scan');
143
242
  }
144
243
  }
145
244
 
@@ -147,15 +246,16 @@ async function checkProjectContext() {
147
246
  const path = join(CWD, '.claude', 'project-context', 'project-context.json');
148
247
 
149
248
  if (!existsSync(path)) {
150
- return { name: 'project-context', ok: false, detail: 'Missing', fix: 'Run gaia-scan or /speckit.init' };
249
+ return result('project-context', 'warning', 'Missing', 'Run gaia-scan or /speckit.init');
151
250
  }
152
251
 
153
252
  try {
154
253
  const data = JSON.parse(await fs.readFile(path, 'utf-8'));
155
- const issues = [];
254
+ const warnings = [];
255
+ const infos = [];
156
256
 
157
- if (!data.metadata) issues.push('Missing metadata section');
158
- if (!data.sections) issues.push('Missing sections');
257
+ if (!data.metadata) warnings.push('Missing metadata section');
258
+ if (!data.sections) warnings.push('Missing sections');
159
259
 
160
260
  // Detect schema version: v2.0 uses metadata.version, v1.0 does not
161
261
  const isV2 = data.metadata?.version === '2.0' || data.metadata?.created_by === 'gaia-scan';
@@ -164,46 +264,52 @@ async function checkProjectContext() {
164
264
  const hasPaths = isV2
165
265
  ? !!data.sections?.infrastructure?.paths
166
266
  : !!data.paths;
167
- if (!hasPaths) issues.push('Missing paths section');
267
+ if (!hasPaths) infos.push('No paths section');
168
268
 
169
- // Check cloud provider: v2.0 in sections.infrastructure.cloud_providers, v1.0 in metadata
269
+ // Cloud provider and region are informational -- not all projects use cloud
170
270
  if (data.metadata) {
171
271
  const cloudProvider = isV2
172
272
  ? data.sections?.infrastructure?.cloud_providers?.[0]?.name
173
273
  : data.metadata.cloud_provider;
174
- if (!cloudProvider) issues.push('No cloud provider set');
274
+ if (!cloudProvider) infos.push('No cloud provider set');
175
275
 
176
- // Check region: v2.0 in terraform_infrastructure, v1.0 in metadata
177
276
  const region = isV2
178
277
  ? data.sections?.terraform_infrastructure?.provider_credentials?.gcp?.region
179
278
  : data.metadata.primary_region;
180
- // Region is optional in v2.0 (not all providers use gcp credentials)
181
- if (!isV2 && !region) issues.push('No region set');
279
+ if (!isV2 && !region) infos.push('No region set');
182
280
  }
183
281
 
184
282
  if (data.sections) {
185
283
  const sectionCount = Object.keys(data.sections).length;
186
- if (sectionCount < 3) issues.push(`Only ${sectionCount} sections (expected >=3)`);
284
+ if (sectionCount < 3) infos.push(`Only ${sectionCount} sections (expected >=3)`);
187
285
  }
188
286
 
189
- if (issues.length > 0) {
190
- return { name: 'project-context', ok: false, detail: issues.join('; '), fix: 'Run /speckit.init to enrich' };
287
+ // Warnings are real problems
288
+ if (warnings.length > 0) {
289
+ const detail = [...warnings, ...infos].join('; ');
290
+ return result('project-context', 'warning', detail, 'Run /speckit.init to enrich');
291
+ }
292
+
293
+ // Info-only items are not problems
294
+ if (infos.length > 0) {
295
+ const sectionCount = data.sections ? Object.keys(data.sections).length : 0;
296
+ return result('project-context', 'info', `${sectionCount} sections -- ${infos.join('; ')}`);
191
297
  }
192
298
 
193
299
  const sectionCount = Object.keys(data.sections).length;
194
300
  const cloud = isV2
195
301
  ? (data.sections?.infrastructure?.cloud_providers?.[0]?.name?.toUpperCase() || '?')
196
302
  : (data.metadata.cloud_provider?.toUpperCase() || '?');
197
- return { name: 'project-context', ok: true, detail: `${sectionCount} sections, ${cloud}` };
303
+ return result('project-context', 'pass', `${sectionCount} sections, ${cloud}`);
198
304
  } catch {
199
- return { name: 'project-context', ok: false, detail: 'Invalid JSON', fix: 'Regenerate with /speckit.init' };
305
+ return result('project-context', 'warning', 'Invalid JSON', 'Regenerate with /speckit.init');
200
306
  }
201
307
  }
202
308
 
203
309
  async function checkPython() {
204
310
  const pyCmd = findPython();
205
311
  if (!pyCmd) {
206
- return { name: 'Python', ok: false, detail: 'Not found', fix: 'Install Python 3.9+' };
312
+ return result('Python', 'error', 'Not found (hooks require Python)', 'Install Python 3.9+');
207
313
  }
208
314
 
209
315
  try {
@@ -215,98 +321,129 @@ async function checkPython() {
215
321
  const major = parseInt(match[1]);
216
322
  const minor = parseInt(match[2]);
217
323
  if (major < 3 || (major === 3 && minor < 9)) {
218
- return { name: 'Python', ok: false, detail: `${version} (need >=3.9)`, fix: 'Upgrade Python to 3.9+' };
324
+ return result('Python', 'error', `${version} (need >=3.9)`, 'Upgrade Python to 3.9+');
219
325
  }
220
326
  }
221
327
 
222
- return { name: 'Python', ok: true, detail: version };
328
+ return result('Python', 'pass', version);
223
329
  } catch {
224
- return { name: 'Python', ok: false, detail: 'Not found', fix: 'Install Python 3.9+' };
330
+ return result('Python', 'error', 'Not found (hooks require Python)', 'Install Python 3.9+');
225
331
  }
226
332
  }
227
333
 
228
334
  async function checkClaudeCode() {
229
335
  try {
230
336
  const { stdout } = await execAsync('claude --version 2>/dev/null || claude-code --version 2>/dev/null');
231
- return { name: 'Claude Code', ok: true, detail: stdout.trim().split('\n')[0] };
337
+ return result('Claude Code', 'pass', stdout.trim().split('\n')[0]);
232
338
  } catch {
233
- return { name: 'Claude Code', ok: false, detail: 'Not installed', fix: 'npm install -g @anthropic-ai/claude-code' };
339
+ // Claude Code is a prerequisite, not a Gaia issue -- informational only
340
+ return result('Claude Code', 'info', 'Not installed', 'npm install -g @anthropic-ai/claude-code');
234
341
  }
235
342
  }
236
343
 
237
344
  async function checkHooks() {
345
+ // Hook files that must exist for Gaia to function.
346
+ // Required: break core functionality if missing.
347
+ // Expected: part of the standard pipeline but non-fatal if absent.
238
348
  const hooks = [
239
349
  { file: 'pre_tool_use.py', required: true },
240
350
  { file: 'post_tool_use.py', required: true },
241
- { file: 'subagent_stop.py', required: false }
351
+ { file: 'user_prompt_submit.py', required: true },
352
+ { file: 'session_start.py', required: true },
353
+ { file: 'subagent_stop.py', expected: true },
354
+ { file: 'subagent_start.py', expected: true },
355
+ { file: 'stop_hook.py', expected: true },
356
+ { file: 'task_completed.py', expected: true },
357
+ { file: 'post_compact.py', expected: true },
358
+ { file: 'elicitation_result.py', expected: true }
242
359
  ];
243
360
 
244
- const issues = [];
361
+ const errors = [];
362
+ const warnings = [];
245
363
  let valid = 0;
246
364
 
247
- for (const { file, required } of hooks) {
365
+ for (const { file, required, expected } of hooks) {
248
366
  const hookPath = join(CWD, '.claude', 'hooks', file);
249
367
  if (existsSync(hookPath)) {
250
368
  valid++;
251
369
  } else if (required) {
252
- issues.push(`${file} missing`);
370
+ errors.push(`${file} missing`);
371
+ } else if (expected) {
372
+ warnings.push(file);
253
373
  }
254
374
  }
255
375
 
256
- if (issues.length > 0) {
257
- return { name: 'Hooks', ok: false, detail: issues.join('; '), fix: 'Recreate symlinks: gaia-scan' };
376
+ if (errors.length > 0) {
377
+ return result('Hook files', 'error', errors.join('; '), 'Recreate symlinks: gaia-scan');
378
+ }
379
+
380
+ if (warnings.length > 0) {
381
+ return result('Hook files', 'warning', `${valid}/${hooks.length} found (missing: ${warnings.join(', ')})`, 'Run gaia-scan to recreate symlinks');
258
382
  }
259
383
 
260
- return { name: 'Hooks', ok: true, detail: `${valid}/${hooks.length} found` };
384
+ return result('Hook files', 'pass', `${valid}/${hooks.length} found`);
261
385
  }
262
386
 
263
387
  async function checkMemoryDirs() {
388
+ // Each memory dir has its own severity -- some are auto-created, some need manual setup
264
389
  const checks = [
265
390
  {
266
391
  path: join(CWD, '.claude', 'project-context', 'speckit-project-specs'),
267
392
  label: 'speckit-project-specs',
393
+ severity: 'warning',
268
394
  fix: 'Run gaia-scan or /speckit.init'
269
395
  },
270
396
  {
271
397
  path: join(CWD, '.claude', 'project-context', 'speckit-project-specs', 'governance.md'),
272
398
  label: 'governance.md',
399
+ severity: 'warning',
273
400
  fix: 'Run /speckit.init to generate governance.md'
274
401
  },
275
402
  {
276
403
  path: join(CWD, '.claude', 'project-context', 'workflow-episodic-memory'),
277
404
  label: 'workflow-episodic-memory',
405
+ severity: 'warning',
278
406
  fix: 'Run gaia-scan to create workflow memory directory'
279
407
  },
280
408
  {
281
409
  path: join(CWD, '.claude', 'project-context', 'episodic-memory'),
282
410
  label: 'episodic-memory',
283
- fix: 'Directory is created automatically on first agent run'
411
+ severity: 'info', // Auto-created on first agent run
412
+ fix: 'Created automatically on first agent run'
284
413
  },
285
414
  ];
286
415
 
287
- const issues = [];
416
+ const warnings = [];
417
+ const infos = [];
288
418
  let found = 0;
289
- for (const { path, label, fix } of checks) {
419
+
420
+ for (const { path, label, severity, fix } of checks) {
290
421
  if (existsSync(path)) {
291
422
  found++;
423
+ } else if (severity === 'info') {
424
+ infos.push({ label, fix });
292
425
  } else {
293
- issues.push({ label, fix });
426
+ warnings.push({ label, fix });
294
427
  }
295
428
  }
296
429
 
297
- if (issues.length > 0) {
298
- const detail = issues.map(i => `${i.label} missing`).join('; ');
299
- const fix = issues[0].fix;
300
- return { name: 'Memory dirs', ok: false, detail, fix };
430
+ if (warnings.length > 0) {
431
+ const detail = warnings.map(i => `${i.label} missing`).join('; ');
432
+ return result('Memory dirs', 'warning', detail, warnings[0].fix);
301
433
  }
302
434
 
303
- return { name: 'Memory dirs', ok: true, detail: `${found}/${checks.length} present` };
435
+ if (infos.length > 0) {
436
+ const detail = `${found}/${checks.length} present (${infos.map(i => `${i.label}: ${i.fix}`).join('; ')})`;
437
+ return result('Memory dirs', 'info', detail);
438
+ }
439
+
440
+ return result('Memory dirs', 'pass', `${found}/${checks.length} present`);
304
441
  }
305
442
 
306
443
  async function checkProjectDirs() {
307
444
  const contextPath = join(CWD, '.claude', 'project-context', 'project-context.json');
308
445
  if (!existsSync(contextPath)) {
309
- return { name: 'Project dirs', ok: true, detail: 'Skipped (no context)' };
446
+ return result('Project dirs', 'pass', 'Skipped (no context)');
310
447
  }
311
448
 
312
449
  try {
@@ -322,12 +459,12 @@ async function checkProjectDirs() {
322
459
  }
323
460
 
324
461
  if (issues.length > 0) {
325
- return { name: 'Project dirs', ok: false, detail: issues.join('; '), fix: 'Create missing directories or update paths' };
462
+ return result('Project dirs', 'warning', issues.join('; '), 'Create missing directories or update paths');
326
463
  }
327
464
 
328
- return { name: 'Project dirs', ok: true, detail: `${Object.keys(paths).length} paths verified` };
465
+ return result('Project dirs', 'pass', `${Object.keys(paths).length} paths verified`);
329
466
  } catch {
330
- return { name: 'Project dirs', ok: true, detail: 'Skipped (parse error)' };
467
+ return result('Project dirs', 'pass', 'Skipped (parse error)');
331
468
  }
332
469
  }
333
470
 
@@ -369,8 +506,6 @@ async function autoFix() {
369
506
  }
370
507
  }
371
508
 
372
- // CLAUDE.md no longer generated -- identity injected by UserPromptSubmit hook
373
-
374
509
  // Create missing project dirs
375
510
  const contextPath = join(CWD, '.claude', 'project-context', 'project-context.json');
376
511
  if (existsSync(contextPath)) {
@@ -394,6 +529,32 @@ async function autoFix() {
394
529
  return fixed;
395
530
  }
396
531
 
532
+ // ============================================================================
533
+ // Display helpers
534
+ // ============================================================================
535
+
536
+ const SEVERITY_ICONS = {
537
+ pass: { icon: '\u2713', color: chalk.green }, // check mark
538
+ info: { icon: '\u2139', color: chalk.cyan }, // info symbol
539
+ warning: { icon: '\u26A0', color: chalk.yellow }, // warning triangle
540
+ error: { icon: '\u2717', color: chalk.red }, // X mark
541
+ };
542
+
543
+ function severityIcon(severity) {
544
+ const entry = SEVERITY_ICONS[severity] || SEVERITY_ICONS.warning;
545
+ return entry.color(entry.icon);
546
+ }
547
+
548
+ function severityDetail(severity, detail) {
549
+ switch (severity) {
550
+ case 'pass': return chalk.gray(detail || '');
551
+ case 'info': return chalk.cyan(detail);
552
+ case 'warning': return chalk.yellow(detail);
553
+ case 'error': return chalk.red(detail);
554
+ default: return detail;
555
+ }
556
+ }
557
+
397
558
  // ============================================================================
398
559
  // Main
399
560
  // ============================================================================
@@ -407,16 +568,18 @@ async function main() {
407
568
  .version(false)
408
569
  .parse();
409
570
 
410
- // Run all checks
571
+ // Run all checks -- ordered from most fundamental to most specific.
572
+ // Core platform first, then Gaia configuration, then project-specific.
411
573
  const checks = [
412
574
  checkGaiaVersion,
413
575
  checkClaudeCode,
414
- checkSymlinks,
415
- checkClaudeMd,
416
- checkSettingsJson,
417
- checkProjectContext,
418
576
  checkPython,
577
+ checkPluginMode,
578
+ checkSymlinks,
579
+ checkIdentity,
580
+ checkSettings,
419
581
  checkHooks,
582
+ checkProjectContext,
420
583
  checkProjectDirs,
421
584
  checkMemoryDirs
422
585
  ];
@@ -426,52 +589,57 @@ async function main() {
426
589
  try {
427
590
  results.push(await check());
428
591
  } catch (error) {
429
- results.push({ name: check.name, ok: false, detail: `Error: ${error.message}` });
592
+ results.push(result(check.name, 'error', `Error: ${error.message}`));
430
593
  }
431
594
  }
432
595
 
596
+ // Compute overall status from severity levels
597
+ const hasErrors = results.some(r => r.severity === 'error');
598
+ const hasWarnings = results.some(r => r.severity === 'warning');
599
+
433
600
  // JSON output mode
434
601
  if (args.json) {
435
- const allOk = results.every(r => r.ok);
436
- console.log(JSON.stringify({ healthy: allOk, checks: results }, null, 2));
437
- process.exit(allOk ? 0 : 1);
602
+ const status = hasErrors ? 'critical' : hasWarnings ? 'degraded' : 'healthy';
603
+ console.log(JSON.stringify({ healthy: !hasErrors && !hasWarnings, status, checks: results }, null, 2));
604
+ process.exit(hasErrors ? 2 : hasWarnings ? 1 : 0);
438
605
  }
439
606
 
440
607
  // Human-readable output
441
- // Extract version for header
442
608
  const gaiaCheck = results.find(r => r.name === 'Gaia-Ops');
443
- const versionTag = gaiaCheck?.ok ? chalk.gray(` (${gaiaCheck.detail})`) : '';
609
+ const versionTag = gaiaCheck?.severity === 'pass' ? chalk.gray(` (${gaiaCheck.detail})`) : '';
444
610
  console.log(chalk.cyan(`\n Gaia-Ops Health Check${versionTag}\n`));
445
611
 
446
- let allOk = true;
612
+ for (const r of results) {
613
+ const icon = severityIcon(r.severity);
614
+ const detail = severityDetail(r.severity, r.detail || '');
615
+ console.log(` ${icon} ${r.name.padEnd(18)} ${detail}`);
447
616
 
448
- for (const result of results) {
449
- const icon = result.ok ? chalk.green('✓') : chalk.yellow('⚠');
450
- const detail = result.ok ? chalk.gray(result.detail || '') : chalk.yellow(result.detail);
451
- console.log(` ${icon} ${result.name.padEnd(18)} ${detail}`);
452
-
453
- if (!result.ok && result.fix) {
454
- console.log(chalk.gray(` Fix: ${result.fix}`));
617
+ if ((r.severity === 'warning' || r.severity === 'error') && r.fix) {
618
+ console.log(chalk.gray(` Fix: ${r.fix}`));
455
619
  }
456
-
457
- if (!result.ok) allOk = false;
458
620
  }
459
621
 
460
622
  console.log('');
461
623
 
462
- if (allOk) {
463
- console.log(chalk.green.bold(' Status: HEALTHY\n'));
464
- } else {
624
+ if (hasErrors) {
625
+ console.log(chalk.red.bold(' Status: CRITICAL\n'));
626
+ if (args.fix) {
627
+ await autoFix();
628
+ } else {
629
+ console.log(chalk.gray(' Run with --fix to attempt auto-repair\n'));
630
+ }
631
+ } else if (hasWarnings) {
465
632
  console.log(chalk.yellow.bold(' Status: ISSUES FOUND\n'));
466
-
467
633
  if (args.fix) {
468
634
  await autoFix();
469
635
  } else {
470
636
  console.log(chalk.gray(' Run with --fix to attempt auto-repair\n'));
471
637
  }
638
+ } else {
639
+ console.log(chalk.green.bold(' Status: HEALTHY\n'));
472
640
  }
473
641
 
474
- process.exit(allOk ? 0 : 1);
642
+ process.exit(hasErrors ? 2 : hasWarnings ? 1 : 0);
475
643
  }
476
644
 
477
645
  main();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaia-ops",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.5",
4
4
  "description": "Full DevOps orchestration for Claude Code. Six specialized agents handle the complete development lifecycle \u2014 analysis, planning, execution, and deployment. Gaia-Ops scans your codebase to understand it and injects the right context into each sub-agent. Every command is classified by risk: read-only runs freely, state changes pause for your approval, and irreversible operations are permanently blocked.",
5
5
  "author": {
6
6
  "name": "jaguilar87"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaia-security",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.5",
4
4
  "description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
5
5
  "author": {
6
6
  "name": "jaguilar87"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaguilar87/gaia-ops",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.5",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",