@rune-kit/rune 2.4.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +47 -17
  2. package/compiler/__tests__/executive-dashboards.test.js +285 -0
  3. package/compiler/__tests__/inject.test.js +128 -0
  4. package/compiler/__tests__/orchestrators.test.js +151 -0
  5. package/compiler/__tests__/org-templates.test.js +447 -0
  6. package/compiler/__tests__/pack-split.test.js +141 -1
  7. package/compiler/__tests__/parser.test.js +147 -1
  8. package/compiler/__tests__/scripts-bundling.test.js +10 -11
  9. package/compiler/__tests__/skill-index.test.js +218 -0
  10. package/compiler/__tests__/status.test.js +336 -0
  11. package/compiler/__tests__/templates.test.js +245 -0
  12. package/compiler/__tests__/visualizer.test.js +325 -0
  13. package/compiler/adapters/antigravity.js +18 -4
  14. package/compiler/bin/rune.js +90 -1
  15. package/compiler/doctor.js +283 -2
  16. package/compiler/emitter.js +490 -17
  17. package/compiler/parser.js +255 -4
  18. package/compiler/status.js +342 -0
  19. package/compiler/visualizer.js +622 -0
  20. package/hooks/hooks.json +12 -0
  21. package/hooks/intent-router/index.cjs +108 -0
  22. package/hooks/pre-tool-guard/index.cjs +177 -68
  23. package/package.json +63 -63
  24. package/skills/autopsy/SKILL.md +48 -1
  25. package/skills/brainstorm/SKILL.md +2 -0
  26. package/skills/completion-gate/SKILL.md +26 -1
  27. package/skills/context-engine/SKILL.md +93 -2
  28. package/skills/cook/SKILL.md +794 -648
  29. package/skills/debug/SKILL.md +409 -392
  30. package/skills/deploy/SKILL.md +2 -0
  31. package/skills/docs/SKILL.md +28 -3
  32. package/skills/fix/SKILL.md +284 -281
  33. package/skills/mcp-builder/SKILL.md +53 -1
  34. package/skills/onboard/SKILL.md +58 -1
  35. package/skills/perf/SKILL.md +34 -1
  36. package/skills/plan/SKILL.md +372 -342
  37. package/skills/preflight/SKILL.md +396 -360
  38. package/skills/retro/SKILL.md +95 -1
  39. package/skills/review/SKILL.md +535 -489
  40. package/skills/scope-guard/SKILL.md +1 -0
  41. package/skills/scout/SKILL.md +1 -0
  42. package/skills/sentinel/SKILL.md +353 -299
  43. package/skills/sentinel/references/policy-driven-constraints.md +424 -0
  44. package/skills/session-bridge/SKILL.md +58 -2
  45. package/skills/session-bridge/references/evolutionary-memory-patterns.md +312 -0
  46. package/skills/team/SKILL.md +16 -1
  47. package/skills/test/SKILL.md +587 -585
  48. package/skills/verification/SKILL.md +1 -0
  49. package/skills/watchdog/SKILL.md +2 -0
@@ -0,0 +1,325 @@
1
+ import assert from 'node:assert';
2
+ import path from 'node:path';
3
+ import { describe, test } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { collectGraphData, generateMeshHTML } from '../visualizer.js';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const RUNE_ROOT = path.resolve(__dirname, '../..');
9
+ const PRO_DIR = path.resolve(RUNE_ROOT, '../../Pro/extensions');
10
+ const BIZ_DIR = path.resolve(RUNE_ROOT, '../../Business/extensions');
11
+
12
+ // ─── collectGraphData ───
13
+
14
+ describe('collectGraphData', () => {
15
+ test('collects nodes for all core skills', async () => {
16
+ const data = await collectGraphData(RUNE_ROOT);
17
+ const coreNodes = data.nodes.filter((n) => n.layer !== 'L4');
18
+ assert.ok(coreNodes.length >= 60, `Expected >= 60 core nodes, got ${coreNodes.length}`);
19
+ });
20
+
21
+ test('collects L4 pack nodes', async () => {
22
+ const data = await collectGraphData(RUNE_ROOT);
23
+ const packNodes = data.nodes.filter((n) => n.layer === 'L4');
24
+ assert.ok(packNodes.length >= 14, `Expected >= 14 pack nodes, got ${packNodes.length}`);
25
+ });
26
+
27
+ test('nodes have required fields', async () => {
28
+ const data = await collectGraphData(RUNE_ROOT);
29
+ for (const n of data.nodes) {
30
+ assert.ok(n.id, 'Node must have id');
31
+ assert.ok(n.layer, 'Node must have layer');
32
+ assert.ok(n.tier, 'Node must have tier');
33
+ }
34
+ });
35
+
36
+ test('nodes have correct layer values', async () => {
37
+ const data = await collectGraphData(RUNE_ROOT);
38
+ const validLayers = new Set(['L0', 'L1', 'L2', 'L3', 'L4']);
39
+ for (const n of data.nodes) {
40
+ assert.ok(validLayers.has(n.layer), `Invalid layer ${n.layer} for ${n.id}`);
41
+ }
42
+ });
43
+
44
+ test('collects cross-ref edges', async () => {
45
+ const data = await collectGraphData(RUNE_ROOT);
46
+ assert.ok(data.edges.length > 100, `Expected > 100 edges, got ${data.edges.length}`);
47
+ });
48
+
49
+ test('edges reference valid nodes', async () => {
50
+ const data = await collectGraphData(RUNE_ROOT);
51
+ const nodeIds = new Set(data.nodes.map((n) => n.id));
52
+ for (const e of data.edges) {
53
+ assert.ok(nodeIds.has(e.source), `Edge source ${e.source} not in nodes`);
54
+ }
55
+ });
56
+
57
+ test('edges have type field', async () => {
58
+ const data = await collectGraphData(RUNE_ROOT);
59
+ for (const e of data.edges) {
60
+ assert.strictEqual(e.type, 'crossref');
61
+ }
62
+ });
63
+
64
+ test('collects signal edges', async () => {
65
+ const data = await collectGraphData(RUNE_ROOT);
66
+ assert.ok(data.signalEdges.length > 0, 'Should have signal edges');
67
+ });
68
+
69
+ test('signal edges have signal name', async () => {
70
+ const data = await collectGraphData(RUNE_ROOT);
71
+ for (const e of data.signalEdges) {
72
+ assert.ok(e.signal, 'Signal edge must have signal name');
73
+ assert.ok(e.source, 'Signal edge must have source');
74
+ assert.ok(e.target, 'Signal edge must have target');
75
+ }
76
+ });
77
+
78
+ test('counts unique signals', async () => {
79
+ const data = await collectGraphData(RUNE_ROOT);
80
+ assert.ok(data.signals.length >= 10, `Expected >= 10 signals, got ${data.signals.length}`);
81
+ });
82
+
83
+ test('stats are accurate', async () => {
84
+ const data = await collectGraphData(RUNE_ROOT);
85
+ assert.strictEqual(data.stats.nodeCount, data.nodes.length);
86
+ assert.strictEqual(data.stats.edgeCount, data.edges.length);
87
+ assert.strictEqual(data.stats.signalEdgeCount, data.signalEdges.length);
88
+ assert.strictEqual(data.stats.signalCount, data.signals.length);
89
+ });
90
+
91
+ test('includes Pro pack nodes when tier provided', async () => {
92
+ const data = await collectGraphData(RUNE_ROOT, { pro: PRO_DIR });
93
+ const proNodes = data.nodes.filter((n) => n.tier === 'pro');
94
+ if (proNodes.length > 0) {
95
+ assert.ok(proNodes.length >= 4, `Expected >= 4 pro nodes`);
96
+ for (const n of proNodes) {
97
+ assert.strictEqual(n.layer, 'L4');
98
+ assert.ok(n.id.startsWith('pack:'));
99
+ }
100
+ }
101
+ });
102
+
103
+ test('includes Business pack nodes when tier provided', async () => {
104
+ const data = await collectGraphData(RUNE_ROOT, { business: BIZ_DIR });
105
+ const bizNodes = data.nodes.filter((n) => n.tier === 'business');
106
+ if (bizNodes.length > 0) {
107
+ assert.ok(bizNodes.length >= 4);
108
+ for (const n of bizNodes) {
109
+ assert.strictEqual(n.layer, 'L4');
110
+ }
111
+ }
112
+ });
113
+
114
+ test('handles non-existent tier paths', async () => {
115
+ const data = await collectGraphData(RUNE_ROOT, {
116
+ pro: '/nonexistent',
117
+ business: '/nonexistent',
118
+ });
119
+ const proNodes = data.nodes.filter((n) => n.tier === 'pro');
120
+ assert.strictEqual(proNodes.length, 0);
121
+ });
122
+
123
+ test('no duplicate node IDs', async () => {
124
+ const data = await collectGraphData(RUNE_ROOT, { pro: PRO_DIR, business: BIZ_DIR });
125
+ const ids = data.nodes.map((n) => n.id);
126
+ const unique = new Set(ids);
127
+ assert.strictEqual(ids.length, unique.size, 'Duplicate node IDs found');
128
+ });
129
+
130
+ test('L0 has exactly 1 node (skill-router)', async () => {
131
+ const data = await collectGraphData(RUNE_ROOT);
132
+ const l0 = data.nodes.filter((n) => n.layer === 'L0');
133
+ assert.strictEqual(l0.length, 1);
134
+ assert.strictEqual(l0[0].id, 'skill-router');
135
+ });
136
+
137
+ test('L1 has orchestrator nodes', async () => {
138
+ const data = await collectGraphData(RUNE_ROOT);
139
+ const l1 = data.nodes.filter((n) => n.layer === 'L1');
140
+ assert.ok(l1.length >= 4);
141
+ const names = l1.map((n) => n.id);
142
+ assert.ok(names.includes('cook'));
143
+ assert.ok(names.includes('team'));
144
+ });
145
+ });
146
+
147
+ // ─── generateMeshHTML ───
148
+
149
+ describe('generateMeshHTML', () => {
150
+ test('generates valid HTML document', async () => {
151
+ const data = await collectGraphData(RUNE_ROOT);
152
+ const html = generateMeshHTML(data);
153
+ assert.ok(html.startsWith('<!DOCTYPE html>'));
154
+ assert.ok(html.includes('<html'));
155
+ assert.ok(html.includes('</html>'));
156
+ });
157
+
158
+ test('is self-contained (no CDN links)', async () => {
159
+ const data = await collectGraphData(RUNE_ROOT);
160
+ const html = generateMeshHTML(data);
161
+ assert.ok(!html.includes('cdn.'), 'Should not reference CDN');
162
+ assert.ok(!html.includes('unpkg.com'), 'Should not reference unpkg');
163
+ assert.ok(!html.includes('cdnjs.'), 'Should not reference cdnjs');
164
+ });
165
+
166
+ test('embeds graph data as JSON', async () => {
167
+ const data = await collectGraphData(RUNE_ROOT);
168
+ const html = generateMeshHTML(data);
169
+ assert.ok(html.includes('const DATA ='));
170
+ assert.ok(html.includes('"nodeCount"'));
171
+ });
172
+
173
+ test('includes inline CSS', async () => {
174
+ const data = await collectGraphData(RUNE_ROOT);
175
+ const html = generateMeshHTML(data);
176
+ assert.ok(html.includes('<style>'));
177
+ assert.ok(html.includes('</style>'));
178
+ });
179
+
180
+ test('includes inline JavaScript', async () => {
181
+ const data = await collectGraphData(RUNE_ROOT);
182
+ const html = generateMeshHTML(data);
183
+ assert.ok(html.includes('<script>'));
184
+ assert.ok(html.includes('</script>'));
185
+ });
186
+
187
+ test('includes search input', async () => {
188
+ const data = await collectGraphData(RUNE_ROOT);
189
+ const html = generateMeshHTML(data);
190
+ assert.ok(html.includes('id="search"'));
191
+ assert.ok(html.includes('Search skills'));
192
+ });
193
+
194
+ test('includes layer filter buttons', async () => {
195
+ const data = await collectGraphData(RUNE_ROOT);
196
+ const html = generateMeshHTML(data);
197
+ assert.ok(html.includes('id="filters"'));
198
+ assert.ok(html.includes('filter-btn'));
199
+ });
200
+
201
+ test('includes detail panel', async () => {
202
+ const data = await collectGraphData(RUNE_ROOT);
203
+ const html = generateMeshHTML(data);
204
+ assert.ok(html.includes('detail-panel'));
205
+ assert.ok(html.includes('detail-content'));
206
+ });
207
+
208
+ test('includes legend', async () => {
209
+ const data = await collectGraphData(RUNE_ROOT);
210
+ const html = generateMeshHTML(data);
211
+ assert.ok(html.includes('L0 Router'));
212
+ assert.ok(html.includes('L1 Orchestrator'));
213
+ assert.ok(html.includes('L2 Workflow'));
214
+ assert.ok(html.includes('L3 Utility'));
215
+ assert.ok(html.includes('L4 Extension'));
216
+ assert.ok(html.includes('Cross-ref'));
217
+ assert.ok(html.includes('Signal'));
218
+ });
219
+
220
+ test('includes canvas element', async () => {
221
+ const data = await collectGraphData(RUNE_ROOT);
222
+ const html = generateMeshHTML(data);
223
+ assert.ok(html.includes('<canvas id="canvas"'));
224
+ });
225
+
226
+ test('includes layer colors config', async () => {
227
+ const data = await collectGraphData(RUNE_ROOT);
228
+ const html = generateMeshHTML(data);
229
+ assert.ok(html.includes('#f59e0b')); // L0
230
+ assert.ok(html.includes('#ef4444')); // L1
231
+ assert.ok(html.includes('#6366f1')); // L2
232
+ assert.ok(html.includes('#10b981')); // L3
233
+ assert.ok(html.includes('#8b5cf6')); // L4
234
+ });
235
+
236
+ test('includes tier border colors for paid packs', async () => {
237
+ const data = await collectGraphData(RUNE_ROOT);
238
+ const html = generateMeshHTML(data);
239
+ assert.ok(html.includes('TIER_BORDER'));
240
+ assert.ok(html.includes('pro'));
241
+ assert.ok(html.includes('business'));
242
+ });
243
+
244
+ test('handles interaction code', async () => {
245
+ const data = await collectGraphData(RUNE_ROOT);
246
+ const html = generateMeshHTML(data);
247
+ assert.ok(html.includes('mousemove'));
248
+ assert.ok(html.includes('mousedown'));
249
+ assert.ok(html.includes('wheel'));
250
+ assert.ok(html.includes('hitTest'));
251
+ });
252
+
253
+ test('has zoom/pan support', async () => {
254
+ const data = await collectGraphData(RUNE_ROOT);
255
+ const html = generateMeshHTML(data);
256
+ assert.ok(html.includes('cam.zoom'));
257
+ assert.ok(html.includes('cam.x'));
258
+ });
259
+
260
+ test('HTML escapes data to prevent XSS', async () => {
261
+ const data = await collectGraphData(RUNE_ROOT);
262
+ const html = generateMeshHTML(data);
263
+ assert.ok(html.includes('function esc('));
264
+ assert.ok(html.includes('&amp;'));
265
+ assert.ok(html.includes('&lt;'));
266
+ });
267
+ });
268
+
269
+ // ─── Graph Properties ───
270
+
271
+ describe('graph properties', () => {
272
+ test('graph is connected (most nodes reachable from cook)', async () => {
273
+ const data = await collectGraphData(RUNE_ROOT);
274
+ const adj = {};
275
+ for (const n of data.nodes) adj[n.id] = [];
276
+ for (const e of data.edges) {
277
+ if (adj[e.source]) adj[e.source].push(e.target);
278
+ if (adj[e.target]) adj[e.target].push(e.source);
279
+ }
280
+
281
+ // BFS from cook
282
+ const visited = new Set();
283
+ const queue = ['cook'];
284
+ visited.add('cook');
285
+ while (queue.length > 0) {
286
+ const current = queue.shift();
287
+ for (const neighbor of adj[current] || []) {
288
+ if (!visited.has(neighbor)) {
289
+ visited.add(neighbor);
290
+ queue.push(neighbor);
291
+ }
292
+ }
293
+ }
294
+
295
+ // Most core skills should be reachable
296
+ const coreNodes = data.nodes.filter((n) => n.layer !== 'L4');
297
+ const reachable = coreNodes.filter((n) => visited.has(n.id));
298
+ const ratio = reachable.length / coreNodes.length;
299
+ assert.ok(ratio > 0.5, `Only ${(ratio * 100).toFixed(0)}% of core skills reachable from cook`);
300
+ });
301
+
302
+ test('no self-referencing edges', async () => {
303
+ const data = await collectGraphData(RUNE_ROOT);
304
+ for (const e of data.edges) {
305
+ assert.notStrictEqual(e.source, e.target, `Self-edge found: ${e.source}`);
306
+ }
307
+ });
308
+
309
+ test('signal edges connect different nodes', async () => {
310
+ const data = await collectGraphData(RUNE_ROOT);
311
+ for (const e of data.signalEdges) {
312
+ assert.notStrictEqual(e.source, e.target, `Signal self-edge: ${e.source} via ${e.signal}`);
313
+ }
314
+ });
315
+
316
+ test('all layers represented', async () => {
317
+ const data = await collectGraphData(RUNE_ROOT);
318
+ const layers = new Set(data.nodes.map((n) => n.layer));
319
+ assert.ok(layers.has('L0'));
320
+ assert.ok(layers.has('L1'));
321
+ assert.ok(layers.has('L2'));
322
+ assert.ok(layers.has('L3'));
323
+ assert.ok(layers.has('L4'));
324
+ });
325
+ });
@@ -1,7 +1,13 @@
1
1
  /**
2
2
  * Google Antigravity (Jules) Adapter
3
3
  *
4
- * Emits .md rule files for .agent/rules/ directory.
4
+ * Emits SKILL.md files into .agents/skills/{name}/ directories.
5
+ * Uses the same SKILL.md frontmatter format (name, description)
6
+ * with markdown body — identical to Codex pattern.
7
+ *
8
+ * Antigravity project context: AGENTS.md (+ CLAUDE.md fallback)
9
+ * Antigravity skills dir: .agents/skills/
10
+ * Antigravity skill format: .agents/skills/{name}/SKILL.md
5
11
  */
6
12
 
7
13
  import { BRANDING_FOOTER } from '../transforms/branding.js';
@@ -20,14 +26,17 @@ const TOOL_MAP = {
20
26
 
21
27
  export default {
22
28
  name: 'antigravity',
23
- outputDir: '.agent/rules',
29
+ outputDir: '.agents/skills',
24
30
  fileExtension: '.md',
25
31
  skillPrefix: 'rune-',
26
32
  skillSuffix: '',
27
33
 
34
+ useSkillDirectories: true,
35
+ skillFileName: 'SKILL.md',
36
+
28
37
  transformReference(skillName, raw) {
29
38
  const isBackticked = raw.startsWith('`') && raw.endsWith('`');
30
- const ref = `the rune-${skillName} rule`;
39
+ const ref = `the rune-${skillName} skill`;
31
40
  return isBackticked ? `\`${ref}\`` : ref;
32
41
  },
33
42
 
@@ -36,7 +45,8 @@ export default {
36
45
  },
37
46
 
38
47
  generateHeader(skill) {
39
- return `# rune-${skill.name}\n\n> Rune ${skill.layer} Skill | ${skill.group}\n\n`;
48
+ const desc = (skill.description || '').replace(/"/g, '\\"');
49
+ return ['---', `name: rune-${skill.name}`, `description: "${desc}"`, '---', '', ''].join('\n');
40
50
  },
41
51
 
42
52
  generateFooter() {
@@ -51,6 +61,10 @@ export default {
51
61
  return `rune-${skillName}-scripts`;
52
62
  },
53
63
 
64
+ referencesDir(skillName) {
65
+ return `rune-${skillName}-references`;
66
+ },
67
+
54
68
  postProcess(content) {
55
69
  return content.replace(/^context: fork\n/gm, '').replace(/^agent: general-purpose\n/gm, '');
56
70
  },
@@ -7,6 +7,8 @@
7
7
  * rune init — Interactive setup for a new project
8
8
  * rune build — Compile skills for the configured platform
9
9
  * rune doctor — Validate compiled output
10
+ * rune status — Project dashboard (neofetch-style)
11
+ * rune visualize — Interactive mesh graph
10
12
  */
11
13
 
12
14
  import { existsSync } from 'node:fs';
@@ -17,6 +19,8 @@ import { fileURLToPath } from 'node:url';
17
19
  import { getAdapter, listPlatforms } from '../adapters/index.js';
18
20
  import { formatDoctorResults, runDoctor } from '../doctor.js';
19
21
  import { buildAll } from '../emitter.js';
22
+ import { collectStats, renderStatus, renderStatusJson } from '../status.js';
23
+ import { collectGraphData, generateMeshHTML } from '../visualizer.js';
20
24
 
21
25
  const __filename = fileURLToPath(import.meta.url);
22
26
  const __dirname = path.dirname(__filename);
@@ -48,7 +52,7 @@ function detectPlatform(projectRoot) {
48
52
  if (existsSync(path.join(projectRoot, '.claude-plugin'))) return 'claude';
49
53
  if (existsSync(path.join(projectRoot, '.cursor'))) return 'cursor';
50
54
  if (existsSync(path.join(projectRoot, '.windsurf'))) return 'windsurf';
51
- if (existsSync(path.join(projectRoot, '.agent'))) return 'antigravity';
55
+ if (existsSync(path.join(projectRoot, '.agents'))) return 'antigravity';
52
56
  if (existsSync(path.join(projectRoot, '.openclaw'))) return 'openclaw';
53
57
  if (existsSync(path.join(projectRoot, '.codex'))) return 'codex';
54
58
  if (existsSync(path.join(projectRoot, '.opencode'))) return 'opencode';
@@ -262,6 +266,82 @@ async function cmdDoctor(projectRoot, args) {
262
266
  if (!results.healthy) process.exit(1);
263
267
  }
264
268
 
269
+ async function cmdStatus(projectRoot, args) {
270
+ const config = await readConfig(projectRoot);
271
+ const tierSources = resolveTierSources(config?.tiers, projectRoot);
272
+ const runeRoot =
273
+ config?.source === '@rune-kit/rune'
274
+ ? RUNE_ROOT
275
+ : config?.source
276
+ ? path.resolve(projectRoot, config.source)
277
+ : RUNE_ROOT;
278
+ const platform = config?.platform || detectPlatform(projectRoot) || '';
279
+
280
+ const pkg = JSON.parse(await readFile(path.join(RUNE_ROOT, 'package.json'), 'utf-8'));
281
+ const projectName = path.basename(projectRoot);
282
+
283
+ const stats = await collectStats(runeRoot, tierSources);
284
+
285
+ if (args.json) {
286
+ log(renderStatusJson(stats, { version: pkg.version, platform, projectName }));
287
+ } else {
288
+ log('');
289
+ log(renderStatus(stats, { version: pkg.version, platform, projectName }));
290
+ log('');
291
+ }
292
+ }
293
+
294
+ async function cmdVisualize(projectRoot, args) {
295
+ const config = await readConfig(projectRoot);
296
+ const tierSources = resolveTierSources(config?.tiers, projectRoot);
297
+ const runeRoot =
298
+ config?.source === '@rune-kit/rune'
299
+ ? RUNE_ROOT
300
+ : config?.source
301
+ ? path.resolve(projectRoot, config.source)
302
+ : RUNE_ROOT;
303
+
304
+ logStep('◎', 'Collecting mesh data...');
305
+ const graphData = await collectGraphData(runeRoot, tierSources);
306
+
307
+ logStep(
308
+ '◎',
309
+ `Found ${graphData.stats.nodeCount} nodes, ${graphData.stats.edgeCount} edges, ${graphData.stats.signalCount} signals`,
310
+ );
311
+
312
+ const html = generateMeshHTML(graphData);
313
+
314
+ const runeDir = path.join(projectRoot, '.rune');
315
+ if (!existsSync(runeDir)) {
316
+ const { mkdir: mkdirFs } = await import('node:fs/promises');
317
+ await mkdirFs(runeDir, { recursive: true });
318
+ }
319
+
320
+ const outputPath = args.output ? path.resolve(projectRoot, args.output) : path.join(runeDir, 'mesh.html');
321
+
322
+ const { writeFile: writeFileFs } = await import('node:fs/promises');
323
+ await writeFileFs(outputPath, html, 'utf-8');
324
+ logStep('✓', `Mesh visualization written to ${path.relative(projectRoot, outputPath)}`);
325
+
326
+ if (args.json) {
327
+ log(JSON.stringify(graphData, null, 2));
328
+ } else {
329
+ // Try to open in browser
330
+ try {
331
+ const { exec } = await import('node:child_process');
332
+ const cmd =
333
+ process.platform === 'win32'
334
+ ? `start "" "${outputPath}"`
335
+ : process.platform === 'darwin'
336
+ ? `open "${outputPath}"`
337
+ : `xdg-open "${outputPath}"`;
338
+ exec(cmd);
339
+ } catch {
340
+ /* ignore if browser open fails */
341
+ }
342
+ }
343
+ }
344
+
265
345
  // ─── Arg Parsing ───
266
346
 
267
347
  // Flags that require a string value (not boolean)
@@ -316,6 +396,13 @@ async function main() {
316
396
  case 'doctor':
317
397
  await cmdDoctor(projectRoot, args);
318
398
  break;
399
+ case 'status':
400
+ await cmdStatus(projectRoot, args);
401
+ break;
402
+ case 'visualize':
403
+ case 'viz':
404
+ await cmdVisualize(projectRoot, args);
405
+ break;
319
406
  case 'version':
320
407
  case '--version':
321
408
  case '-v': {
@@ -333,6 +420,8 @@ async function main() {
333
420
  log(' init Interactive setup (auto-detects platform)');
334
421
  log(' build Compile skills for configured platform');
335
422
  log(' doctor Validate compiled output');
423
+ log(' status Project dashboard (skills, signals, packs, health)');
424
+ log(' visualize Interactive mesh graph (opens in browser)');
336
425
  log('');
337
426
  log(' Options:');
338
427
  log(