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

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.6",
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.6",
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();
@@ -121,6 +121,32 @@ async function updateLocalPermissions() {
121
121
  return false;
122
122
  }
123
123
 
124
+ // Load existing settings.local.json — preserve everything (enabledPlugins, MCP servers, etc.)
125
+ let existing = {};
126
+ if (existsSync(localPath)) {
127
+ try {
128
+ existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
129
+ } catch {
130
+ existing = {};
131
+ }
132
+ }
133
+
134
+ // Track what changed
135
+ let changed = false;
136
+
137
+ // Set the orchestrator agent identity (always, even if Python extraction fails)
138
+ if (existing.agent !== 'gaia-orchestrator') {
139
+ existing.agent = 'gaia-orchestrator';
140
+ changed = true;
141
+ }
142
+
143
+ // Add env vars (smart merge: add if not present, don't overwrite)
144
+ existing.env = existing.env || {};
145
+ if (!('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' in existing.env)) {
146
+ existing.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
147
+ changed = true;
148
+ }
149
+
124
150
  // Load permissions from plugin_setup.py — the single source of truth.
125
151
  // We use ast.literal_eval to extract the constants without importing
126
152
  // the module (which has relative imports that fail standalone).
@@ -128,11 +154,16 @@ async function updateLocalPermissions() {
128
154
  try {
129
155
  const setupPath = join(__dirname, '..', 'hooks', 'modules', 'core', 'plugin_setup.py');
130
156
  const pythonCmd = findPython() || 'python3';
131
- const { stdout } = await execAsync(
132
- `${pythonCmd} -c "
133
- import ast, json, re
134
157
 
135
- source = open('${setupPath.replace(/'/g, "\\'")}').read()
158
+ // Write the extraction script to a temp file instead of using -c with
159
+ // inline code. This avoids shell quoting issues on Windows where
160
+ // backslash paths and nested quotes break the inline Python string.
161
+ const tempScript = join(claudeDir, '.gaia-extract-perms.py');
162
+ const scriptContent = `
163
+ import ast, json, re, sys
164
+
165
+ setup_path = sys.argv[1]
166
+ source = open(setup_path, encoding="utf-8").read()
136
167
 
137
168
  # Extract _DENY_RULES list
138
169
  deny_match = re.search(r'^_DENY_RULES\\s*=\\s*\\[', source, re.MULTILINE)
@@ -163,28 +194,32 @@ else:
163
194
  ops_perms = {'permissions': {'allow': [], 'deny': deny_rules, 'ask': []}}
164
195
 
165
196
  print(json.dumps(ops_perms))
166
- "`,
167
- { timeout: 10000 }
168
- );
169
- gaiaPerms = JSON.parse(stdout.trim());
197
+ `;
198
+ await fs.writeFile(tempScript, scriptContent);
199
+ try {
200
+ const { stdout } = await execAsync(
201
+ `${pythonCmd} "${tempScript}" "${setupPath}"`,
202
+ { timeout: 10000 }
203
+ );
204
+ gaiaPerms = JSON.parse(stdout.trim());
205
+ } finally {
206
+ // Clean up temp script
207
+ try { await fs.unlink(tempScript); } catch { /* ignore */ }
208
+ }
170
209
  } catch (pyError) {
171
210
  spinner.warn(`Could not load permissions from Python — ${pyError.message || 'unknown error'}`);
211
+ // Still write agent and env changes even if permissions extraction fails
212
+ if (changed) {
213
+ await fs.writeFile(localPath, JSON.stringify(existing, null, 2) + '\n');
214
+ spinner.succeed('settings.local.json agent and env merged (permissions skipped)');
215
+ return true;
216
+ }
172
217
  return false;
173
218
  }
174
219
 
175
220
  const ourAllow = new Set(gaiaPerms.permissions.allow || []);
176
221
  const ourDeny = new Set(gaiaPerms.permissions.deny || []);
177
222
 
178
- // Load existing settings.local.json — preserve everything (enabledPlugins, MCP servers, etc.)
179
- let existing = {};
180
- if (existsSync(localPath)) {
181
- try {
182
- existing = JSON.parse(await fs.readFile(localPath, 'utf-8'));
183
- } catch {
184
- existing = {};
185
- }
186
- }
187
-
188
223
  const perms = existing.permissions || {};
189
224
  const currentAllow = new Set(perms.allow || []);
190
225
  const currentDeny = new Set(perms.deny || []);
@@ -193,9 +228,6 @@ print(json.dumps(ops_perms))
193
228
  const mergedAllow = [...new Set([...currentAllow, ...ourAllow])].sort();
194
229
  const mergedDeny = [...new Set([...currentDeny, ...ourDeny])].sort();
195
230
 
196
- // Track what changed
197
- let changed = false;
198
-
199
231
  // Check if permissions changed
200
232
  const allowChanged = mergedAllow.length !== currentAllow.size
201
233
  || mergedAllow.some(r => !currentAllow.has(r));
@@ -210,19 +242,6 @@ print(json.dumps(ops_perms))
210
242
  changed = true;
211
243
  }
212
244
 
213
- // Add env vars (smart merge: add if not present, don't overwrite)
214
- existing.env = existing.env || {};
215
- if (!('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' in existing.env)) {
216
- existing.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
217
- changed = true;
218
- }
219
-
220
- // Set the orchestrator agent identity
221
- if (existing.agent !== 'gaia-orchestrator') {
222
- existing.agent = 'gaia-orchestrator';
223
- changed = true;
224
- }
225
-
226
245
  if (!changed) {
227
246
  spinner.succeed('settings.local.json permissions already up to date');
228
247
  return false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaia-ops",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.6",
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"
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: blog-writing
3
+ description: Use when writing, drafting, or publishing a blog article for metraton.github.io
4
+ metadata:
5
+ user-invocable: true
6
+ type: technique
7
+ ---
8
+
9
+ # Blog Writing
10
+
11
+ Jorge's blog (metraton.github.io) is where he thinks out loud about agentic systems, context engineering, and the intersection of architecture and AI. The articles are bilingual (English and Spanish), grounded in real experience, and written in first person. They are not corporate content -- they are reflections from someone building these systems daily.
12
+
13
+ ## The Writing Process
14
+
15
+ ### 1. Find the story first
16
+
17
+ Every article starts with something that actually happened -- a deployment that went sideways, a late-night refactor, a conversation that shifted your thinking. The story is the anchor. Without it, you are writing a tutorial, not an article.
18
+
19
+ Ask Jorge: "What happened recently that surprised you or changed how you think about something?" The best topics come from moments where the outcome was different from the expectation.
20
+
21
+ ### 2. Build the brief
22
+
23
+ Before writing a single paragraph, define: title, audience, core thesis (one sentence), and format. Jorge's natural format is **Strategic Insight** -- personal experience analyzed through a technical lens, ending with a transferable lesson.
24
+
25
+ The audience is engineers, solution architects, AI practitioners, and tech leaders. They do not need hand-holding but they appreciate honesty about what did not work.
26
+
27
+ ### 3. Draft in Markdown, iterate with Jorge
28
+
29
+ Write the first draft in Markdown at `/home/jorge/ws/me/<slug>.md`. Work section by section -- do not dump an entire draft and ask "what do you think?" Each section should be reviewed before moving to the next.
30
+
31
+ **Article structure** (not rigid, but this is Jorge's natural flow):
32
+ - Opening story -- what happened, told personally
33
+ - The problem -- what was broken or surprising
34
+ - Investigation -- what you looked into and found
35
+ - The shift -- the insight or reframe
36
+ - In practice -- what changed, concretely
37
+ - Results -- evidence it worked
38
+ - Closing thought -- punchy, memorable, one line
39
+
40
+ ### 4. Principles that matter
41
+
42
+ **Examples must be real.** Not "imagine a team that..." but "I was deploying to Cloud Run when..." If you cannot point to something that actually happened, the example does not belong.
43
+
44
+ **Each section adds something new.** Do not repeat the same example or insight across sections. If the investigation section already showed the problem, the "in practice" section should show the solution -- not restate the problem.
45
+
46
+ **Quotes add weight when grounded.** Citing Anthropic, Hinton, or other thought leaders works when the quote connects directly to the experience. A quote floating without context is decoration.
47
+
48
+ **Technical terms stay in English in both languages.** LLM, skills, agent, Cloud Run, Terraform -- these do not get translated.
49
+
50
+ **Closing lines are signatures.** "Built with context.", "Built with reasoning." -- short, confident, tied to the article's thesis.
51
+
52
+ ### 5. Convert to bilingual HTML
53
+
54
+ Once the Markdown draft is approved, convert to the bilingual HTML format. The Spanish version is not a mechanical translation -- it is natural Latin American Spanish, with the same voice and directness. Read `reference.md` for the HTML template and front matter structure.
55
+
56
+ Final HTML goes in: `/home/jorge/ws/me/metraton.github.io/_posts/YYYY-MM-DD-slug.html`
57
+
58
+ ### 6. Preview and publish
59
+
60
+ Run Jekyll locally for preview (port 4000). Then commit and push to publish via GitHub Pages.
61
+
62
+ ## Jorge's Voice
63
+
64
+ - First person, always. "I was deploying..." not "The team deployed..."
65
+ - Honest about failures and iterations -- does not pretend things worked the first time
66
+ - Confident but not preachy. States what he found, not what everyone should do
67
+ - Direct. Short sentences when making a point. Longer when telling a story
68
+ - Latin American perspective -- references to the region's tech community are natural, not forced
69
+
70
+ ## Anti-Patterns
71
+
72
+ - Writing generic content that could appear on any corporate blog
73
+ - Translating English to Spanish mechanically instead of rewriting naturally
74
+ - Dumping an entire draft without iterating section by section
75
+ - Using hypothetical examples when real ones exist
76
+ - Repeating the same insight across multiple sections
@@ -0,0 +1,102 @@
1
+ # Blog Writing Reference
2
+
3
+ ## Blog Repository
4
+
5
+ - **Repo path:** `/home/jorge/ws/me/metraton.github.io/`
6
+ - **Site:** https://metraton.github.io/
7
+ - **Engine:** Jekyll static site, GitHub Pages hosted
8
+ - **Posts directory:** `_posts/`
9
+ - **Draft workspace:** `/home/jorge/ws/me/<slug>.md`
10
+
11
+ ## Front Matter Template
12
+
13
+ Every post requires this YAML front matter. All fields are mandatory for the bilingual layout to work correctly.
14
+
15
+ ```yaml
16
+ ---
17
+ layout: bilingual
18
+ title: "English Title Here"
19
+ title_en: "English Title Here"
20
+ title_es: "Spanish Title Here"
21
+ post_title_es: "Spanish Title Here"
22
+ date: YYYY-MM-DD
23
+ terminal_title: "jaguilar@github:~/posts$ cat slug-name"
24
+ terminal_title_es: "jaguilar@github:~/posts$ cat slug-name"
25
+ excerpt: "English excerpt -- one compelling sentence."
26
+ excerpt_es: "Spanish excerpt -- one compelling sentence."
27
+ ---
28
+ ```
29
+
30
+ **Notes:**
31
+ - `title` and `title_en` are always identical
32
+ - `title_es` and `post_title_es` are always identical
33
+ - `terminal_title` and `terminal_title_es` are usually the same (the cat command)
34
+ - `date` is the publication date in `YYYY-MM-DD` format
35
+ - Excerpts use HTML entities for special characters (e.g., `&eacute;` for accented characters in Spanish)
36
+ - Some older posts include `permalink:` -- newer posts rely on the filename for the URL
37
+
38
+ ## HTML Structure
39
+
40
+ The file is named `YYYY-MM-DD-slug.html` and placed in `_posts/`.
41
+
42
+ ```html
43
+ ---
44
+ (front matter as above)
45
+ ---
46
+
47
+ <div class="bilingual-content" lang="en" data-lang-section="en">
48
+ <!-- English content here -->
49
+ <!-- Use proper HTML entities: &mdash; &rsquo; &ldquo; &rdquo; etc. -->
50
+
51
+ <p class="closing-line"><em>Built with [theme]. Jorge Aguilar, 2025&ndash;2026.</em></p>
52
+ </div>
53
+ <div class="bilingual-content" lang="es" data-lang-section="es" hidden>
54
+ <!-- Spanish content here -->
55
+ <!-- Same structure as English, naturally rewritten -->
56
+
57
+ <p class="closing-line"><em>Construido con [tema]. Jorge Aguilar, 2025&ndash;2026.</em></p>
58
+ </div>
59
+ ```
60
+
61
+ **HTML conventions observed in existing posts:**
62
+ - Content is indented with 8 spaces (two levels inside the layout)
63
+ - Use `&mdash;` for em dashes, `&rsquo;` for apostrophes, `&ldquo;`/`&rdquo;` for quotes
64
+ - Use `&eacute;`, `&aacute;`, `&iacute;`, `&oacute;`, `&uacute;`, `&ntilde;` for Spanish accented characters
65
+ - Section headings are `<h2>`, subsection headings are `<h3>`
66
+ - Horizontal rules (`<hr />`) separate major sections
67
+ - Blockquotes (`<blockquote>`) are used for key insights and external quotes
68
+ - Code inline uses `<code>`, code blocks use `<pre><code>`
69
+ - The Spanish section has `hidden` attribute (JavaScript toggles it)
70
+ - The closing line uses `class="closing-line"` with `<em>` wrapper
71
+
72
+ ## Existing Articles (for style reference)
73
+
74
+ | Date | Slug | Theme |
75
+ |------|------|-------|
76
+ | 2025-09-29 | context-design-agentic-deployment | Context engineering and agentic deployment on GCP |
77
+ | 2025-11-01 | beyond-delegation-agentic-systems | Moving past simple delegation in agentic workflows |
78
+ | 2026-04-02 | faster-development-orchestration | How orchestration improves faster development cycles |
79
+ | 2026-04-10 | writing-skills-that-actually-work | Skill design for LLM agents -- judgment over compliance |
80
+
81
+ ## Local Preview
82
+
83
+ ```bash
84
+ cd /home/jorge/ws/me/metraton.github.io
85
+ bundle exec jekyll serve
86
+ # Preview at http://localhost:4000
87
+ # Note: WSL2 may require port forwarding for browser access
88
+ ```
89
+
90
+ ## Publication
91
+
92
+ ```bash
93
+ cd /home/jorge/ws/me/metraton.github.io
94
+ git add _posts/YYYY-MM-DD-slug.html
95
+ git commit -m "Add: article title"
96
+ git push origin main
97
+ # GitHub Pages deploys automatically
98
+ ```
99
+
100
+ ## Pending Article Ideas
101
+
102
+ - **"How to Build an Agent Identity"** -- about agent identities, what they contain, how to structure them, the relationship between identity and skills. More reflective than technical.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gaia-security",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.6",
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.6",
4
4
  "description": "Multi-agent orchestration system for Claude Code - DevOps automation toolkit",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: blog-writing
3
+ description: Use when writing, drafting, or publishing a blog article for metraton.github.io
4
+ metadata:
5
+ user-invocable: true
6
+ type: technique
7
+ ---
8
+
9
+ # Blog Writing
10
+
11
+ Jorge's blog (metraton.github.io) is where he thinks out loud about agentic systems, context engineering, and the intersection of architecture and AI. The articles are bilingual (English and Spanish), grounded in real experience, and written in first person. They are not corporate content -- they are reflections from someone building these systems daily.
12
+
13
+ ## The Writing Process
14
+
15
+ ### 1. Find the story first
16
+
17
+ Every article starts with something that actually happened -- a deployment that went sideways, a late-night refactor, a conversation that shifted your thinking. The story is the anchor. Without it, you are writing a tutorial, not an article.
18
+
19
+ Ask Jorge: "What happened recently that surprised you or changed how you think about something?" The best topics come from moments where the outcome was different from the expectation.
20
+
21
+ ### 2. Build the brief
22
+
23
+ Before writing a single paragraph, define: title, audience, core thesis (one sentence), and format. Jorge's natural format is **Strategic Insight** -- personal experience analyzed through a technical lens, ending with a transferable lesson.
24
+
25
+ The audience is engineers, solution architects, AI practitioners, and tech leaders. They do not need hand-holding but they appreciate honesty about what did not work.
26
+
27
+ ### 3. Draft in Markdown, iterate with Jorge
28
+
29
+ Write the first draft in Markdown at `/home/jorge/ws/me/<slug>.md`. Work section by section -- do not dump an entire draft and ask "what do you think?" Each section should be reviewed before moving to the next.
30
+
31
+ **Article structure** (not rigid, but this is Jorge's natural flow):
32
+ - Opening story -- what happened, told personally
33
+ - The problem -- what was broken or surprising
34
+ - Investigation -- what you looked into and found
35
+ - The shift -- the insight or reframe
36
+ - In practice -- what changed, concretely
37
+ - Results -- evidence it worked
38
+ - Closing thought -- punchy, memorable, one line
39
+
40
+ ### 4. Principles that matter
41
+
42
+ **Examples must be real.** Not "imagine a team that..." but "I was deploying to Cloud Run when..." If you cannot point to something that actually happened, the example does not belong.
43
+
44
+ **Each section adds something new.** Do not repeat the same example or insight across sections. If the investigation section already showed the problem, the "in practice" section should show the solution -- not restate the problem.
45
+
46
+ **Quotes add weight when grounded.** Citing Anthropic, Hinton, or other thought leaders works when the quote connects directly to the experience. A quote floating without context is decoration.
47
+
48
+ **Technical terms stay in English in both languages.** LLM, skills, agent, Cloud Run, Terraform -- these do not get translated.
49
+
50
+ **Closing lines are signatures.** "Built with context.", "Built with reasoning." -- short, confident, tied to the article's thesis.
51
+
52
+ ### 5. Convert to bilingual HTML
53
+
54
+ Once the Markdown draft is approved, convert to the bilingual HTML format. The Spanish version is not a mechanical translation -- it is natural Latin American Spanish, with the same voice and directness. Read `reference.md` for the HTML template and front matter structure.
55
+
56
+ Final HTML goes in: `/home/jorge/ws/me/metraton.github.io/_posts/YYYY-MM-DD-slug.html`
57
+
58
+ ### 6. Preview and publish
59
+
60
+ Run Jekyll locally for preview (port 4000). Then commit and push to publish via GitHub Pages.
61
+
62
+ ## Jorge's Voice
63
+
64
+ - First person, always. "I was deploying..." not "The team deployed..."
65
+ - Honest about failures and iterations -- does not pretend things worked the first time
66
+ - Confident but not preachy. States what he found, not what everyone should do
67
+ - Direct. Short sentences when making a point. Longer when telling a story
68
+ - Latin American perspective -- references to the region's tech community are natural, not forced
69
+
70
+ ## Anti-Patterns
71
+
72
+ - Writing generic content that could appear on any corporate blog
73
+ - Translating English to Spanish mechanically instead of rewriting naturally
74
+ - Dumping an entire draft without iterating section by section
75
+ - Using hypothetical examples when real ones exist
76
+ - Repeating the same insight across multiple sections
@@ -0,0 +1,102 @@
1
+ # Blog Writing Reference
2
+
3
+ ## Blog Repository
4
+
5
+ - **Repo path:** `/home/jorge/ws/me/metraton.github.io/`
6
+ - **Site:** https://metraton.github.io/
7
+ - **Engine:** Jekyll static site, GitHub Pages hosted
8
+ - **Posts directory:** `_posts/`
9
+ - **Draft workspace:** `/home/jorge/ws/me/<slug>.md`
10
+
11
+ ## Front Matter Template
12
+
13
+ Every post requires this YAML front matter. All fields are mandatory for the bilingual layout to work correctly.
14
+
15
+ ```yaml
16
+ ---
17
+ layout: bilingual
18
+ title: "English Title Here"
19
+ title_en: "English Title Here"
20
+ title_es: "Spanish Title Here"
21
+ post_title_es: "Spanish Title Here"
22
+ date: YYYY-MM-DD
23
+ terminal_title: "jaguilar@github:~/posts$ cat slug-name"
24
+ terminal_title_es: "jaguilar@github:~/posts$ cat slug-name"
25
+ excerpt: "English excerpt -- one compelling sentence."
26
+ excerpt_es: "Spanish excerpt -- one compelling sentence."
27
+ ---
28
+ ```
29
+
30
+ **Notes:**
31
+ - `title` and `title_en` are always identical
32
+ - `title_es` and `post_title_es` are always identical
33
+ - `terminal_title` and `terminal_title_es` are usually the same (the cat command)
34
+ - `date` is the publication date in `YYYY-MM-DD` format
35
+ - Excerpts use HTML entities for special characters (e.g., `&eacute;` for accented characters in Spanish)
36
+ - Some older posts include `permalink:` -- newer posts rely on the filename for the URL
37
+
38
+ ## HTML Structure
39
+
40
+ The file is named `YYYY-MM-DD-slug.html` and placed in `_posts/`.
41
+
42
+ ```html
43
+ ---
44
+ (front matter as above)
45
+ ---
46
+
47
+ <div class="bilingual-content" lang="en" data-lang-section="en">
48
+ <!-- English content here -->
49
+ <!-- Use proper HTML entities: &mdash; &rsquo; &ldquo; &rdquo; etc. -->
50
+
51
+ <p class="closing-line"><em>Built with [theme]. Jorge Aguilar, 2025&ndash;2026.</em></p>
52
+ </div>
53
+ <div class="bilingual-content" lang="es" data-lang-section="es" hidden>
54
+ <!-- Spanish content here -->
55
+ <!-- Same structure as English, naturally rewritten -->
56
+
57
+ <p class="closing-line"><em>Construido con [tema]. Jorge Aguilar, 2025&ndash;2026.</em></p>
58
+ </div>
59
+ ```
60
+
61
+ **HTML conventions observed in existing posts:**
62
+ - Content is indented with 8 spaces (two levels inside the layout)
63
+ - Use `&mdash;` for em dashes, `&rsquo;` for apostrophes, `&ldquo;`/`&rdquo;` for quotes
64
+ - Use `&eacute;`, `&aacute;`, `&iacute;`, `&oacute;`, `&uacute;`, `&ntilde;` for Spanish accented characters
65
+ - Section headings are `<h2>`, subsection headings are `<h3>`
66
+ - Horizontal rules (`<hr />`) separate major sections
67
+ - Blockquotes (`<blockquote>`) are used for key insights and external quotes
68
+ - Code inline uses `<code>`, code blocks use `<pre><code>`
69
+ - The Spanish section has `hidden` attribute (JavaScript toggles it)
70
+ - The closing line uses `class="closing-line"` with `<em>` wrapper
71
+
72
+ ## Existing Articles (for style reference)
73
+
74
+ | Date | Slug | Theme |
75
+ |------|------|-------|
76
+ | 2025-09-29 | context-design-agentic-deployment | Context engineering and agentic deployment on GCP |
77
+ | 2025-11-01 | beyond-delegation-agentic-systems | Moving past simple delegation in agentic workflows |
78
+ | 2026-04-02 | faster-development-orchestration | How orchestration improves faster development cycles |
79
+ | 2026-04-10 | writing-skills-that-actually-work | Skill design for LLM agents -- judgment over compliance |
80
+
81
+ ## Local Preview
82
+
83
+ ```bash
84
+ cd /home/jorge/ws/me/metraton.github.io
85
+ bundle exec jekyll serve
86
+ # Preview at http://localhost:4000
87
+ # Note: WSL2 may require port forwarding for browser access
88
+ ```
89
+
90
+ ## Publication
91
+
92
+ ```bash
93
+ cd /home/jorge/ws/me/metraton.github.io
94
+ git add _posts/YYYY-MM-DD-slug.html
95
+ git commit -m "Add: article title"
96
+ git push origin main
97
+ # GitHub Pages deploys automatically
98
+ ```
99
+
100
+ ## Pending Article Ideas
101
+
102
+ - **"How to Build an Agent Identity"** -- about agent identities, what they contain, how to structure them, the relationship between identity and skills. More reflective than technical.