pgserve 0.1.1

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 (158) hide show
  1. package/.genie/AGENTS.md +13 -0
  2. package/.genie/agents/README.md +110 -0
  3. package/.genie/agents/analyze.md +176 -0
  4. package/.genie/agents/forge.md +290 -0
  5. package/.genie/agents/garbage-cleaner.md +324 -0
  6. package/.genie/agents/garbage-collector.md +596 -0
  7. package/.genie/agents/github-issue-gc.md +618 -0
  8. package/.genie/agents/review.md +380 -0
  9. package/.genie/agents/semantic-analyzer/find-duplicates.md +90 -0
  10. package/.genie/agents/semantic-analyzer/find-orphans.md +99 -0
  11. package/.genie/agents/semantic-analyzer.md +101 -0
  12. package/.genie/agents/update.md +182 -0
  13. package/.genie/agents/wish.md +357 -0
  14. package/.genie/code/AGENTS.md +692 -0
  15. package/.genie/code/agents/audit/risk.md +173 -0
  16. package/.genie/code/agents/audit/security.md +189 -0
  17. package/.genie/code/agents/audit.md +145 -0
  18. package/.genie/code/agents/challenge.md +230 -0
  19. package/.genie/code/agents/change-reviewer.md +295 -0
  20. package/.genie/code/agents/code-garbage-collector.md +425 -0
  21. package/.genie/code/agents/code-quality.md +410 -0
  22. package/.genie/code/agents/commit-suggester.md +255 -0
  23. package/.genie/code/agents/commit.md +124 -0
  24. package/.genie/code/agents/consensus.md +204 -0
  25. package/.genie/code/agents/daily-standup.md +722 -0
  26. package/.genie/code/agents/docgen.md +48 -0
  27. package/.genie/code/agents/explore.md +79 -0
  28. package/.genie/code/agents/fix.md +100 -0
  29. package/.genie/code/agents/git/commit-advisory.md +219 -0
  30. package/.genie/code/agents/git/workflows/issue.md +244 -0
  31. package/.genie/code/agents/git/workflows/pr.md +179 -0
  32. package/.genie/code/agents/git/workflows/release.md +460 -0
  33. package/.genie/code/agents/git/workflows/report.md +342 -0
  34. package/.genie/code/agents/git.md +432 -0
  35. package/.genie/code/agents/implementor.md +161 -0
  36. package/.genie/code/agents/install.md +515 -0
  37. package/.genie/code/agents/issue-creator.md +344 -0
  38. package/.genie/code/agents/polish.md +116 -0
  39. package/.genie/code/agents/qa.md +653 -0
  40. package/.genie/code/agents/refactor.md +294 -0
  41. package/.genie/code/agents/release.md +1129 -0
  42. package/.genie/code/agents/roadmap.md +885 -0
  43. package/.genie/code/agents/tests.md +557 -0
  44. package/.genie/code/agents/tracer.md +50 -0
  45. package/.genie/code/agents/update/upstream-update.md +85 -0
  46. package/.genie/code/agents/update/versions/generic-update.md +305 -0
  47. package/.genie/code/agents/vibe.md +1317 -0
  48. package/.genie/code/spells/agent-configuration.md +58 -0
  49. package/.genie/code/spells/automated-rc-publishing.md +106 -0
  50. package/.genie/code/spells/branch-tracker-guidance.md +28 -0
  51. package/.genie/code/spells/debug.md +320 -0
  52. package/.genie/code/spells/emoji-naming-convention.md +303 -0
  53. package/.genie/code/spells/evidence-storage.md +26 -0
  54. package/.genie/code/spells/file-naming-rules.md +35 -0
  55. package/.genie/code/spells/forge-code-blueprints.md +195 -0
  56. package/.genie/code/spells/genie-integration.md +153 -0
  57. package/.genie/code/spells/publishing-protocol.md +61 -0
  58. package/.genie/code/spells/team-consultation-protocol.md +284 -0
  59. package/.genie/code/spells/tool-requirements.md +20 -0
  60. package/.genie/code/spells/triad-maintenance-protocol.md +154 -0
  61. package/.genie/code/teams/tech-council/council.md +328 -0
  62. package/.genie/code/teams/tech-council/jt.md +352 -0
  63. package/.genie/code/teams/tech-council/nayr.md +305 -0
  64. package/.genie/code/teams/tech-council/oettam.md +375 -0
  65. package/.genie/neurons/README.md +193 -0
  66. package/.genie/neurons/forge.md +106 -0
  67. package/.genie/neurons/genie.md +63 -0
  68. package/.genie/neurons/review.md +106 -0
  69. package/.genie/neurons/wish.md +104 -0
  70. package/.genie/product/README.md +20 -0
  71. package/.genie/product/cli-automation.md +359 -0
  72. package/.genie/product/environment.md +60 -0
  73. package/.genie/product/mission.md +60 -0
  74. package/.genie/product/roadmap.md +44 -0
  75. package/.genie/product/tech-stack.md +34 -0
  76. package/.genie/product/templates/context-template.md +218 -0
  77. package/.genie/product/templates/qa-done-report-template.md +68 -0
  78. package/.genie/product/templates/review-report-template.md +89 -0
  79. package/.genie/product/templates/wish-template.md +120 -0
  80. package/.genie/scripts/helpers/analyze-commit.js +195 -0
  81. package/.genie/scripts/helpers/bullet-counter.js +194 -0
  82. package/.genie/scripts/helpers/bullet-find.js +289 -0
  83. package/.genie/scripts/helpers/bullet-id.js +244 -0
  84. package/.genie/scripts/helpers/check-secrets.js +237 -0
  85. package/.genie/scripts/helpers/count-tokens.js +200 -0
  86. package/.genie/scripts/helpers/create-frontmatter.js +456 -0
  87. package/.genie/scripts/helpers/detect-markers.js +293 -0
  88. package/.genie/scripts/helpers/detect-todos.js +267 -0
  89. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +135 -0
  90. package/.genie/scripts/helpers/embeddings.js +344 -0
  91. package/.genie/scripts/helpers/find-empty-sections.js +158 -0
  92. package/.genie/scripts/helpers/index.js +319 -0
  93. package/.genie/scripts/helpers/validate-frontmatter.js +578 -0
  94. package/.genie/scripts/helpers/validate-links.js +207 -0
  95. package/.genie/scripts/helpers/validate-paths.js +373 -0
  96. package/.genie/spells/README.md +9 -0
  97. package/.genie/spells/ace-protocol.md +118 -0
  98. package/.genie/spells/ask-one-at-a-time.md +175 -0
  99. package/.genie/spells/backup-analyzer.md +542 -0
  100. package/.genie/spells/blocker.md +12 -0
  101. package/.genie/spells/break-things-move-fast.md +56 -0
  102. package/.genie/spells/context-candidates.md +72 -0
  103. package/.genie/spells/context-critic.md +51 -0
  104. package/.genie/spells/defer-to-expertise.md +278 -0
  105. package/.genie/spells/delegate-dont-do.md +292 -0
  106. package/.genie/spells/error-investigation-protocol.md +328 -0
  107. package/.genie/spells/evidence-based-completion.md +273 -0
  108. package/.genie/spells/experiment.md +65 -0
  109. package/.genie/spells/file-creation-protocol.md +229 -0
  110. package/.genie/spells/forge-integration.md +281 -0
  111. package/.genie/spells/forge-orchestration.md +514 -0
  112. package/.genie/spells/gather-context.md +18 -0
  113. package/.genie/spells/global-health-check.md +34 -0
  114. package/.genie/spells/global-noop-roundtrip.md +25 -0
  115. package/.genie/spells/install-genie.md +1232 -0
  116. package/.genie/spells/install.md +82 -0
  117. package/.genie/spells/investigate-before-commit.md +112 -0
  118. package/.genie/spells/know-yourself.md +288 -0
  119. package/.genie/spells/learn.md +828 -0
  120. package/.genie/spells/mcp-diagnostic-protocol.md +246 -0
  121. package/.genie/spells/mcp-first.md +124 -0
  122. package/.genie/spells/multi-step-execution.md +67 -0
  123. package/.genie/spells/orchestration-boundary-protocol.md +256 -0
  124. package/.genie/spells/orchestrator-not-implementor.md +189 -0
  125. package/.genie/spells/prompt.md +746 -0
  126. package/.genie/spells/reflect.md +404 -0
  127. package/.genie/spells/routing-decision-matrix.md +368 -0
  128. package/.genie/spells/run-in-parallel.md +12 -0
  129. package/.genie/spells/session-state-updater-example.md +196 -0
  130. package/.genie/spells/session-state-updater.md +220 -0
  131. package/.genie/spells/track-long-running-tasks.md +133 -0
  132. package/.genie/spells/troubleshoot-infrastructure.md +176 -0
  133. package/.genie/spells/upgrade-genie.md +415 -0
  134. package/.genie/spells/url-presentation-protocol.md +301 -0
  135. package/.genie/spells/wish-initiation.md +158 -0
  136. package/.genie/spells/wish-issue-linkage.md +410 -0
  137. package/.genie/spells/wish-lifecycle.md +100 -0
  138. package/.genie/state/provider-status.json +3 -0
  139. package/.genie/state/version.json +16 -0
  140. package/AGENTS.md +422 -0
  141. package/CLAUDE.md +1 -0
  142. package/LICENSE +21 -0
  143. package/Makefile +235 -0
  144. package/README.md +323 -0
  145. package/bin/pglite-server.js +457 -0
  146. package/ecosystem.config.cjs +23 -0
  147. package/examples/multi-tenant-demo.js +104 -0
  148. package/package.json +47 -0
  149. package/src/detector.js +105 -0
  150. package/src/index.js +177 -0
  151. package/src/pool.js +320 -0
  152. package/src/ports.js +114 -0
  153. package/src/protocol.js +216 -0
  154. package/src/registry.js +134 -0
  155. package/src/router.js +289 -0
  156. package/src/server.js +265 -0
  157. package/tests/benchmarks/runner.js +489 -0
  158. package/tests/multi-tenant.test.js +201 -0
@@ -0,0 +1,578 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Frontmatter Validator for Genie Framework
5
+ *
6
+ * Validates .md file frontmatter across Genie framework:
7
+ * - YAML syntax validation
8
+ * - Required fields presence
9
+ * - Amendment 7 violations (version, timestamps)
10
+ * - Proper delimiter structure
11
+ *
12
+ * Usage:
13
+ * node validate-frontmatter.js [path]
14
+ * (defaults to .genie/ if no path provided)
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const yaml = require('yaml');
20
+ const { execSync } = require('child_process');
21
+
22
+ // Configuration
23
+ const REQUIRED_FIELDS = {
24
+ agent: ['name', 'description', 'genie'],
25
+ spell: ['name', 'description'],
26
+ };
27
+
28
+ const FORBIDDEN_FIELDS = ['version', 'last_updated', 'author'];
29
+
30
+ // Valid Claude models
31
+ const VALID_CLAUDE_MODELS = ['haiku', 'sonnet', 'opus-4'];
32
+
33
+ // NOTE: Executor names are NOT validated against a hardcoded list
34
+ // They are fetched dynamically from Forge via AgentRegistry.getSupportedExecutors()
35
+ // Validation here only checks format (uppercase). Runtime validation happens in Forge.
36
+
37
+ // Cache for opencode models (fetched once)
38
+ let OPENCODE_MODELS_CACHE = null;
39
+
40
+ // Results tracking
41
+ const results = {
42
+ totalFiles: 0,
43
+ scannedFiles: 0,
44
+ validFiles: 0,
45
+ issues: [],
46
+ skipped: [],
47
+ };
48
+
49
+ /**
50
+ * Fetch valid OpenCode models from `opencode models` command
51
+ */
52
+ async function getOpenCodeModels() {
53
+ if (OPENCODE_MODELS_CACHE !== null) {
54
+ return OPENCODE_MODELS_CACHE;
55
+ }
56
+
57
+ try {
58
+ const output = execSync('opencode models', {
59
+ encoding: 'utf-8',
60
+ stdio: ['pipe', 'pipe', 'ignore'] // Suppress stderr
61
+ });
62
+ OPENCODE_MODELS_CACHE = output
63
+ .split('\n')
64
+ .map(line => line.trim())
65
+ .filter(line => line.length > 0);
66
+ return OPENCODE_MODELS_CACHE;
67
+ } catch (err) {
68
+ console.warn('⚠️ Could not fetch opencode models (is opencode installed?). Skipping OpenCode model validation.');
69
+ OPENCODE_MODELS_CACHE = []; // Empty cache = skip validation
70
+ return OPENCODE_MODELS_CACHE;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Check if file should be scanned
76
+ */
77
+ function shouldScan(filePath) {
78
+ const excludePatterns = [
79
+ 'node_modules',
80
+ '.git',
81
+ 'dist',
82
+ 'build',
83
+ 'coverage',
84
+ '/backups/', // Exclude backup directories
85
+ 'README.md', // README files don't need frontmatter
86
+ 'SETUP-', // Setup guide files
87
+ ];
88
+
89
+ return !excludePatterns.some(pattern => filePath.includes(pattern));
90
+ }
91
+
92
+ /**
93
+ * Extract frontmatter from markdown content
94
+ */
95
+ function extractFrontmatter(content, filePath) {
96
+ const lines = content.split('\n');
97
+
98
+ // Check if file starts with frontmatter delimiter
99
+ if (lines[0] !== '---') {
100
+ return { hasFrontmatter: false };
101
+ }
102
+
103
+ // Find closing delimiter
104
+ let closingIndex = -1;
105
+ for (let i = 1; i < lines.length; i++) {
106
+ if (lines[i] === '---') {
107
+ closingIndex = i;
108
+ break;
109
+ }
110
+ }
111
+
112
+ if (closingIndex === -1) {
113
+ return {
114
+ hasFrontmatter: true,
115
+ error: 'Missing closing frontmatter delimiter (---)',
116
+ line: 1,
117
+ };
118
+ }
119
+
120
+ // Extract YAML content
121
+ const yamlContent = lines.slice(1, closingIndex).join('\n');
122
+
123
+ try {
124
+ const parsed = yaml.parse(yamlContent);
125
+ return {
126
+ hasFrontmatter: true,
127
+ frontmatter: parsed,
128
+ yamlContent,
129
+ closingLine: closingIndex + 1,
130
+ };
131
+ } catch (err) {
132
+ return {
133
+ hasFrontmatter: true,
134
+ error: `Invalid YAML syntax: ${err.message}`,
135
+ line: 1,
136
+ yamlContent,
137
+ };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Detect file type (agent, spell, etc.)
143
+ */
144
+ function detectFileType(filePath) {
145
+ if (filePath.includes('/agents/')) return 'agent';
146
+ if (filePath.includes('/spells/')) return 'spell';
147
+ return 'other';
148
+ }
149
+
150
+ /**
151
+ * Validate frontmatter fields
152
+ */
153
+ async function validateFields(frontmatter, fileType, filePath) {
154
+ const issues = [];
155
+
156
+ // Check required fields
157
+ const required = REQUIRED_FIELDS[fileType] || [];
158
+ for (const field of required) {
159
+ if (!frontmatter[field]) {
160
+ issues.push({
161
+ type: 'missing_required',
162
+ field,
163
+ message: `Missing required field: ${field}`,
164
+ });
165
+ }
166
+ }
167
+
168
+ // Check forbidden fields (Amendment 7)
169
+ for (const field of FORBIDDEN_FIELDS) {
170
+ if (frontmatter[field]) {
171
+ issues.push({
172
+ type: 'amendment_7_violation',
173
+ field,
174
+ message: `Forbidden field (Amendment 7): ${field}`,
175
+ suggestion: 'Remove (git tracks this)',
176
+ });
177
+ }
178
+ }
179
+
180
+ // Check for nested genie structure (agents only)
181
+ if (fileType === 'agent' && frontmatter.genie) {
182
+ if (typeof frontmatter.genie !== 'object') {
183
+ issues.push({
184
+ type: 'invalid_structure',
185
+ field: 'genie',
186
+ message: 'genie field must be an object',
187
+ });
188
+ } else {
189
+ // Validate genie.executor and genie.model
190
+ const genieIssues = await validateGenieFields(frontmatter.genie);
191
+ issues.push(...genieIssues);
192
+ }
193
+ }
194
+
195
+ // Check for empty required fields (present but empty/null)
196
+ for (const field of required) {
197
+ if (frontmatter[field] !== undefined && !frontmatter[field]) {
198
+ issues.push({
199
+ type: 'empty_required',
200
+ field,
201
+ message: `Required field is empty: ${field}`,
202
+ suggestion: 'Provide a value or remove the field',
203
+ });
204
+ }
205
+ }
206
+
207
+ return issues;
208
+ }
209
+
210
+ /**
211
+ * Validate genie.executor and genie.model fields
212
+ */
213
+ async function validateGenieFields(genie) {
214
+ const issues = [];
215
+ const executor = genie.executor;
216
+ const model = genie.model;
217
+ const variant = genie.executorVariant || genie.variant || genie.executor_variant || genie.executorProfile;
218
+
219
+ // Validate executor format (should be uppercase)
220
+ // NOTE: We don't validate against a hardcoded list - executors are dynamic in Forge
221
+ // AgentRegistry.getSupportedExecutors() fetches the current list from Forge
222
+ if (executor) {
223
+ if (executor !== executor.toUpperCase()) {
224
+ issues.push({
225
+ type: 'executor_case',
226
+ field: 'genie.executor',
227
+ message: `Executor should be uppercase (Forge format): ${executor}`,
228
+ suggestion: `Change to: ${executor.toUpperCase()}`,
229
+ });
230
+ }
231
+ }
232
+
233
+ // Warn about executorVariant (deprecated field)
234
+ if (variant) {
235
+ issues.push({
236
+ type: 'deprecated_field',
237
+ field: 'genie.executorVariant',
238
+ message: `executorVariant is deprecated (conflicts with Forge profile-per-agent pattern)`,
239
+ suggestion: `Remove this field - Forge creates one profile per agent automatically`,
240
+ });
241
+ }
242
+
243
+ // Validate model based on executor
244
+ if (executor && model) {
245
+ if (executor === 'CLAUDE_CODE') {
246
+ // Claude models: haiku, sonnet, opus-4
247
+ if (!VALID_CLAUDE_MODELS.includes(model)) {
248
+ issues.push({
249
+ type: 'invalid_claude_model',
250
+ field: 'genie.model',
251
+ message: `Invalid Claude model: ${model}`,
252
+ suggestion: `Valid Claude models: ${VALID_CLAUDE_MODELS.join(', ')}`,
253
+ });
254
+ }
255
+ } else if (executor === 'OPENCODE') {
256
+ // OpenCode models: must be in `opencode models` output
257
+ const validModels = await getOpenCodeModels();
258
+ if (validModels.length > 0 && !validModels.includes(model)) {
259
+ issues.push({
260
+ type: 'invalid_opencode_model',
261
+ field: 'genie.model',
262
+ message: `OpenCode model not found: ${model}`,
263
+ suggestion: `Run 'opencode models' to see valid models`,
264
+ });
265
+ }
266
+ }
267
+ }
268
+
269
+ // Validate executor-specific permission flags (from Forge API 2025-10-26)
270
+ // See: .genie/product/docs/executor-configuration.md
271
+ const permissionIssues = validatePermissionFlags(genie, executor);
272
+ issues.push(...permissionIssues);
273
+
274
+ return issues;
275
+ }
276
+
277
+ /**
278
+ * Validate executor-specific permission flags
279
+ * Based on live Forge API query 2025-10-26
280
+ */
281
+ function validatePermissionFlags(genie, executor) {
282
+ const issues = [];
283
+
284
+ // Permission flags by executor (from Forge DEFAULT profiles)
285
+ const EXECUTOR_PERMISSION_FLAGS = {
286
+ CLAUDE_CODE: ['dangerously_skip_permissions'],
287
+ CODEX: ['sandbox'],
288
+ AMP: ['dangerously_allow_all'],
289
+ OPENCODE: [], // No permission flags
290
+ };
291
+
292
+ const VALID_PERMISSION_FLAGS = {
293
+ dangerously_skip_permissions: { executor: 'CLAUDE_CODE', type: 'boolean' },
294
+ sandbox: { executor: 'CODEX', type: 'string', values: ['danger-full-access', 'read-only', 'safe'] },
295
+ dangerously_allow_all: { executor: 'AMP', type: 'boolean' },
296
+ };
297
+
298
+ const VALID_ADDITIONAL_FIELDS = {
299
+ model_reasoning_effort: { executors: ['CODEX'], type: 'string', values: ['low', 'medium', 'high'] },
300
+ };
301
+
302
+ // Check for permission flags in frontmatter
303
+ for (const [flag, config] of Object.entries(VALID_PERMISSION_FLAGS)) {
304
+ if (genie[flag] !== undefined) {
305
+ // Flag is present - check if it matches the executor
306
+ if (executor !== config.executor) {
307
+ issues.push({
308
+ type: 'wrong_executor_permission_flag',
309
+ field: `genie.${flag}`,
310
+ message: `Permission flag '${flag}' is for ${config.executor}, but executor is ${executor}`,
311
+ suggestion: executor ?
312
+ `Remove this flag or use ${executor}-specific flag: ${EXECUTOR_PERMISSION_FLAGS[executor]?.join(', ') || 'none'}` :
313
+ `Remove this flag or set executor to ${config.executor}`,
314
+ });
315
+ } else {
316
+ // Correct executor - validate value type
317
+ if (config.type === 'boolean' && typeof genie[flag] !== 'boolean') {
318
+ issues.push({
319
+ type: 'invalid_permission_flag_type',
320
+ field: `genie.${flag}`,
321
+ message: `${flag} must be a boolean (true or false)`,
322
+ suggestion: `Change to: true or false (no quotes)`,
323
+ });
324
+ } else if (config.type === 'string' && typeof genie[flag] !== 'string') {
325
+ issues.push({
326
+ type: 'invalid_permission_flag_type',
327
+ field: `genie.${flag}`,
328
+ message: `${flag} must be a string`,
329
+ suggestion: `Valid values: ${config.values.join(', ')}`,
330
+ });
331
+ } else if (config.values && !config.values.includes(genie[flag])) {
332
+ issues.push({
333
+ type: 'invalid_permission_flag_value',
334
+ field: `genie.${flag}`,
335
+ message: `Invalid value for ${flag}: '${genie[flag]}'`,
336
+ suggestion: `Valid values: ${config.values.join(', ')}`,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ // Validate additional executor-specific fields
344
+ for (const [field, config] of Object.entries(VALID_ADDITIONAL_FIELDS)) {
345
+ if (genie[field] !== undefined) {
346
+ // Check if field is valid for this executor
347
+ if (executor && !config.executors.includes(executor)) {
348
+ issues.push({
349
+ type: 'wrong_executor_field',
350
+ field: `genie.${field}`,
351
+ message: `Field '${field}' is only valid for ${config.executors.join(', ')}, but executor is ${executor}`,
352
+ suggestion: `Remove this field or use one of: ${config.executors.join(', ')}`,
353
+ });
354
+ } else {
355
+ // Validate value type and values
356
+ if (config.type === 'string' && typeof genie[field] !== 'string') {
357
+ issues.push({
358
+ type: 'invalid_field_type',
359
+ field: `genie.${field}`,
360
+ message: `${field} must be a string`,
361
+ suggestion: `Valid values: ${config.values.join(', ')}`,
362
+ });
363
+ } else if (config.values && !config.values.includes(genie[field])) {
364
+ issues.push({
365
+ type: 'invalid_field_value',
366
+ field: `genie.${field}`,
367
+ message: `Invalid value for ${field}: '${genie[field]}'`,
368
+ suggestion: `Valid values: ${config.values.join(', ')}`,
369
+ });
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ // Warn about deprecated 'additional_params' usage
376
+ if (genie.additional_params !== undefined) {
377
+ issues.push({
378
+ type: 'deprecated_field',
379
+ field: 'genie.additional_params',
380
+ message: `additional_params is not used by Forge (defaults to empty array)`,
381
+ suggestion: `Remove this field and use executor-specific permission flags instead`,
382
+ });
383
+ }
384
+
385
+ return issues;
386
+ }
387
+
388
+ /**
389
+ * Scan single markdown file
390
+ */
391
+ async function scanFile(filePath) {
392
+ results.totalFiles++;
393
+
394
+ if (!shouldScan(filePath)) {
395
+ results.skipped.push({ file: filePath, reason: 'Excluded path' });
396
+ return;
397
+ }
398
+
399
+ try {
400
+ const content = fs.readFileSync(filePath, 'utf-8');
401
+ const extracted = extractFrontmatter(content, filePath);
402
+
403
+ results.scannedFiles++;
404
+
405
+ // No frontmatter (may be valid for some files)
406
+ if (!extracted.hasFrontmatter) {
407
+ const fileType = detectFileType(filePath);
408
+ if (fileType === 'agent' || fileType === 'spell') {
409
+ results.issues.push({
410
+ file: filePath,
411
+ line: 1,
412
+ type: 'missing_frontmatter',
413
+ message: `${fileType} file missing frontmatter`,
414
+ severity: 'error',
415
+ });
416
+ }
417
+ return;
418
+ }
419
+
420
+ // Frontmatter extraction error
421
+ if (extracted.error) {
422
+ results.issues.push({
423
+ file: filePath,
424
+ line: extracted.line,
425
+ type: 'invalid_frontmatter',
426
+ message: extracted.error,
427
+ severity: 'error',
428
+ yaml: extracted.yamlContent,
429
+ });
430
+ return;
431
+ }
432
+
433
+ // Validate fields
434
+ const fileType = detectFileType(filePath);
435
+ const fieldIssues = await validateFields(extracted.frontmatter, fileType, filePath);
436
+
437
+ if (fieldIssues.length > 0) {
438
+ fieldIssues.forEach(issue => {
439
+ results.issues.push({
440
+ file: filePath,
441
+ line: 2, // Approximate (inside frontmatter)
442
+ type: issue.type,
443
+ field: issue.field,
444
+ message: issue.message,
445
+ suggestion: issue.suggestion,
446
+ severity: [
447
+ 'amendment_7_violation',
448
+ 'deprecated_field',
449
+ 'executor_case',
450
+ 'wrong_executor_permission_flag',
451
+ 'invalid_permission_flag_type',
452
+ 'invalid_permission_flag_value',
453
+ 'wrong_executor_field',
454
+ 'invalid_field_type',
455
+ 'invalid_field_value'
456
+ ].includes(issue.type) ? 'warning' : 'error',
457
+ });
458
+ });
459
+ } else {
460
+ results.validFiles++;
461
+ }
462
+
463
+ } catch (err) {
464
+ results.issues.push({
465
+ file: filePath,
466
+ type: 'read_error',
467
+ message: `Failed to read file: ${err.message}`,
468
+ severity: 'error',
469
+ });
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Recursively scan directory
475
+ */
476
+ async function scanDirectory(dir) {
477
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
478
+
479
+ for (const entry of entries) {
480
+ const fullPath = path.join(dir, entry.name);
481
+
482
+ if (entry.isDirectory()) {
483
+ await scanDirectory(fullPath);
484
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
485
+ await scanFile(fullPath);
486
+ }
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Generate report
492
+ */
493
+ function generateReport() {
494
+ console.log('\n=== Frontmatter Validation Report ===\n');
495
+
496
+ console.log(`Total files: ${results.totalFiles}`);
497
+ console.log(`Scanned: ${results.scannedFiles}`);
498
+ console.log(`Valid: ${results.validFiles}`);
499
+ console.log(`Issues found: ${results.issues.length}`);
500
+ console.log(`Skipped: ${results.skipped.length}\n`);
501
+
502
+ if (results.issues.length === 0) {
503
+ console.log('✅ No frontmatter issues found!\n');
504
+ return 0;
505
+ }
506
+
507
+ // Group issues by severity
508
+ const errors = results.issues.filter(i => i.severity === 'error');
509
+ const warnings = results.issues.filter(i => i.severity === 'warning');
510
+
511
+ if (errors.length > 0) {
512
+ console.log(`🔴 ERRORS (${errors.length}):\n`);
513
+ errors.forEach(issue => {
514
+ console.log(` ${issue.file}:${issue.line || '?'}`);
515
+ console.log(` Type: ${issue.type}`);
516
+ console.log(` Message: ${issue.message}`);
517
+ if (issue.field) console.log(` Field: ${issue.field}`);
518
+ if (issue.suggestion) console.log(` Suggestion: ${issue.suggestion}`);
519
+ if (issue.yaml) console.log(` YAML:\n${issue.yaml.split('\n').map(l => ' ' + l).join('\n')}`);
520
+ console.log('');
521
+ });
522
+ }
523
+
524
+ if (warnings.length > 0) {
525
+ console.log(`⚠️ WARNINGS (${warnings.length}):\n`);
526
+ warnings.forEach(issue => {
527
+ console.log(` ${issue.file}:${issue.line || '?'}`);
528
+ console.log(` Type: ${issue.type}`);
529
+ console.log(` Message: ${issue.message}`);
530
+ if (issue.field) console.log(` Field: ${issue.field}`);
531
+ if (issue.suggestion) console.log(` Suggestion: ${issue.suggestion}`);
532
+ console.log('');
533
+ });
534
+ }
535
+
536
+ // Summary
537
+ console.log('\n=== Summary by Issue Type ===\n');
538
+ const byType = {};
539
+ results.issues.forEach(issue => {
540
+ byType[issue.type] = (byType[issue.type] || 0) + 1;
541
+ });
542
+
543
+ Object.entries(byType).forEach(([type, count]) => {
544
+ console.log(` ${type}: ${count}`);
545
+ });
546
+
547
+ console.log('');
548
+ return errors.length > 0 ? 1 : 0;
549
+ }
550
+
551
+ /**
552
+ * Main
553
+ */
554
+ async function main() {
555
+ const targetPath = process.argv[2] || '.genie';
556
+
557
+ if (!fs.existsSync(targetPath)) {
558
+ console.error(`Error: Path not found: ${targetPath}`);
559
+ process.exit(1);
560
+ }
561
+
562
+ console.log(`Scanning: ${targetPath}\n`);
563
+
564
+ const stat = fs.statSync(targetPath);
565
+ if (stat.isDirectory()) {
566
+ await scanDirectory(targetPath);
567
+ } else if (targetPath.endsWith('.md')) {
568
+ await scanFile(targetPath);
569
+ } else {
570
+ console.error('Error: Target must be a directory or .md file');
571
+ process.exit(1);
572
+ }
573
+
574
+ const exitCode = generateReport();
575
+ process.exit(exitCode);
576
+ }
577
+
578
+ main();