@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/gaia-doctor.js +293 -125
- package/bin/gaia-update.js +53 -34
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/skills/blog-writing/SKILL.md +76 -0
- package/dist/gaia-ops/skills/blog-writing/reference.md +102 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/blog-writing/SKILL.md +76 -0
- package/skills/blog-writing/reference.md +102 -0
|
@@ -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.
|
|
11
|
+
"version": "5.0.0-beta.6",
|
|
12
12
|
"source": "./dist/gaia-security"
|
|
13
13
|
}
|
|
14
14
|
]
|
package/bin/gaia-doctor.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
sub.push({ name, status: 'ok' });
|
|
59
116
|
} catch {
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
87
|
-
|
|
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(
|
|
90
|
-
return
|
|
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(
|
|
194
|
+
const data = JSON.parse(await fs.readFile(localPath, 'utf-8'));
|
|
95
195
|
const issues = [];
|
|
196
|
+
const infos = [];
|
|
96
197
|
|
|
97
|
-
// Check hooks
|
|
98
|
-
|
|
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
|
|
201
|
+
issues.push('No hooks configured');
|
|
110
202
|
} else {
|
|
111
203
|
const hookTypes = Object.keys(hooksConfig);
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
254
|
+
const warnings = [];
|
|
255
|
+
const infos = [];
|
|
156
256
|
|
|
157
|
-
if (!data.metadata)
|
|
158
|
-
if (!data.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)
|
|
267
|
+
if (!hasPaths) infos.push('No paths section');
|
|
168
268
|
|
|
169
|
-
//
|
|
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)
|
|
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
|
-
|
|
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)
|
|
284
|
+
if (sectionCount < 3) infos.push(`Only ${sectionCount} sections (expected >=3)`);
|
|
187
285
|
}
|
|
188
286
|
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
303
|
+
return result('project-context', 'pass', `${sectionCount} sections, ${cloud}`);
|
|
198
304
|
} catch {
|
|
199
|
-
return
|
|
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
|
|
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
|
|
324
|
+
return result('Python', 'error', `${version} (need >=3.9)`, 'Upgrade Python to 3.9+');
|
|
219
325
|
}
|
|
220
326
|
}
|
|
221
327
|
|
|
222
|
-
return
|
|
328
|
+
return result('Python', 'pass', version);
|
|
223
329
|
} catch {
|
|
224
|
-
return
|
|
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
|
|
337
|
+
return result('Claude Code', 'pass', stdout.trim().split('\n')[0]);
|
|
232
338
|
} catch {
|
|
233
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
370
|
+
errors.push(`${file} missing`);
|
|
371
|
+
} else if (expected) {
|
|
372
|
+
warnings.push(file);
|
|
253
373
|
}
|
|
254
374
|
}
|
|
255
375
|
|
|
256
|
-
if (
|
|
257
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
416
|
+
const warnings = [];
|
|
417
|
+
const infos = [];
|
|
288
418
|
let found = 0;
|
|
289
|
-
|
|
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
|
-
|
|
426
|
+
warnings.push({ label, fix });
|
|
294
427
|
}
|
|
295
428
|
}
|
|
296
429
|
|
|
297
|
-
if (
|
|
298
|
-
const detail =
|
|
299
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
462
|
+
return result('Project dirs', 'warning', issues.join('; '), 'Create missing directories or update paths');
|
|
326
463
|
}
|
|
327
464
|
|
|
328
|
-
return
|
|
465
|
+
return result('Project dirs', 'pass', `${Object.keys(paths).length} paths verified`);
|
|
329
466
|
} catch {
|
|
330
|
-
return
|
|
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(
|
|
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
|
|
436
|
-
console.log(JSON.stringify({ healthy:
|
|
437
|
-
process.exit(
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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 (
|
|
463
|
-
console.log(chalk.
|
|
464
|
-
|
|
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(
|
|
642
|
+
process.exit(hasErrors ? 2 : hasWarnings ? 1 : 0);
|
|
475
643
|
}
|
|
476
644
|
|
|
477
645
|
main();
|
package/bin/gaia-update.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
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., `é` 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: — ’ “ ” etc. -->
|
|
50
|
+
|
|
51
|
+
<p class="closing-line"><em>Built with [theme]. Jorge Aguilar, 2025–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–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 `—` for em dashes, `’` for apostrophes, `“`/`”` for quotes
|
|
64
|
+
- Use `é`, `á`, `í`, `ó`, `ú`, `ñ` 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.
|
|
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
|
@@ -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., `é` 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: — ’ “ ” etc. -->
|
|
50
|
+
|
|
51
|
+
<p class="closing-line"><em>Built with [theme]. Jorge Aguilar, 2025–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–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 `—` for em dashes, `’` for apostrophes, `“`/`”` for quotes
|
|
64
|
+
- Use `é`, `á`, `í`, `ó`, `ú`, `ñ` 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.
|