@covibes/zeroshot 5.2.1 → 5.4.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 (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
@@ -1,244 +1,10 @@
1
1
  /**
2
- * Stream JSON Parser for Claude Code output
3
- *
4
- * Parses NDJSON (newline-delimited JSON) streaming output from Claude Code.
5
- * Extracts: text output, tool calls, tool results, thinking, errors.
6
- *
7
- * Event types from Claude Code stream-json format:
8
- * - system: Session initialization
9
- * - stream_event: Real-time streaming (content_block_start, content_block_delta, etc.)
10
- * - assistant: Complete assistant message with content array
11
- * - user: Tool results
12
- * - result: Final task result
2
+ * Compatibility wrapper for Claude stream-json parsing.
3
+ * Prefer provider-specific parsers in src/providers.
13
4
  */
14
-
15
- /**
16
- * Parse a single JSON line and extract displayable content
17
- * @param {string} line - Single line of NDJSON
18
- * @returns {Object|null} Parsed event with type and content, or null if not displayable
19
- */
20
- function parseStreamLine(line) {
21
- const trimmed = line.trim();
22
- if (!trimmed || !trimmed.startsWith('{') || !trimmed.endsWith('}')) {
23
- return null;
24
- }
25
-
26
- let event;
27
- try {
28
- event = JSON.parse(trimmed);
29
- } catch {
30
- return null;
31
- }
32
-
33
- // stream_event - real-time streaming updates
34
- if (event.type === 'stream_event' && event.event) {
35
- return parseStreamEvent(event.event);
36
- }
37
-
38
- // assistant - complete message with content blocks
39
- if (event.type === 'assistant' && event.message?.content) {
40
- return parseAssistantMessage(event.message);
41
- }
42
-
43
- // user - tool result
44
- if (event.type === 'user' && event.message?.content) {
45
- return parseUserMessage(event.message);
46
- }
47
-
48
- // result - final task result (includes token usage and cost)
49
- if (event.type === 'result') {
50
- const usage = event.usage || {};
51
- return {
52
- type: 'result',
53
- success: event.subtype === 'success',
54
- result: event.result,
55
- error: event.is_error ? event.result : null,
56
- cost: event.total_cost_usd,
57
- duration: event.duration_ms,
58
- // Token usage from Claude API
59
- inputTokens: usage.input_tokens || 0,
60
- outputTokens: usage.output_tokens || 0,
61
- cacheReadInputTokens: usage.cache_read_input_tokens || 0,
62
- cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
63
- // Per-model breakdown (for multi-model tasks)
64
- modelUsage: event.modelUsage || null,
65
- };
66
- }
67
-
68
- // system - session init (skip, not user-facing)
69
- if (event.type === 'system') {
70
- return null;
71
- }
72
-
73
- return null;
74
- }
75
-
76
- /**
77
- * Parse stream_event inner event
78
- */
79
- function parseStreamEvent(inner) {
80
- // content_block_start - tool use or text block starting
81
- if (inner.type === 'content_block_start' && inner.content_block) {
82
- const block = inner.content_block;
83
-
84
- if (block.type === 'tool_use') {
85
- return {
86
- type: 'tool_start',
87
- toolName: block.name,
88
- toolId: block.id,
89
- };
90
- }
91
-
92
- if (block.type === 'thinking') {
93
- return {
94
- type: 'thinking_start',
95
- };
96
- }
97
-
98
- // text block start - usually empty, skip
99
- return null;
100
- }
101
-
102
- // content_block_delta - incremental content
103
- if (inner.type === 'content_block_delta' && inner.delta) {
104
- const delta = inner.delta;
105
-
106
- if (delta.type === 'text_delta' && delta.text) {
107
- return {
108
- type: 'text',
109
- text: delta.text,
110
- };
111
- }
112
-
113
- if (delta.type === 'thinking_delta' && delta.thinking) {
114
- return {
115
- type: 'thinking',
116
- text: delta.thinking,
117
- };
118
- }
119
-
120
- if (delta.type === 'input_json_delta' && delta.partial_json) {
121
- return {
122
- type: 'tool_input',
123
- json: delta.partial_json,
124
- };
125
- }
126
- }
127
-
128
- // content_block_stop - block ended
129
- if (inner.type === 'content_block_stop') {
130
- return {
131
- type: 'block_end',
132
- index: inner.index,
133
- };
134
- }
135
-
136
- // message_start - skip text extraction here since it will come via text_delta events
137
- // (extracting here causes duplicate text output)
138
-
139
- return null;
140
- }
141
-
142
- /**
143
- * Parse assistant message content blocks
144
- * NOTE: Skip text blocks here since they were already streamed via text_delta events.
145
- * Extracting text here would cause duplicate output.
146
- */
147
- function parseAssistantMessage(message) {
148
- const results = [];
149
-
150
- for (const block of message.content) {
151
- // Skip text blocks - already streamed via text_delta events
152
- // Extracting here causes duplicate output
153
-
154
- if (block.type === 'tool_use') {
155
- results.push({
156
- type: 'tool_call',
157
- toolName: block.name,
158
- toolId: block.id,
159
- input: block.input,
160
- });
161
- }
162
-
163
- if (block.type === 'thinking' && block.thinking) {
164
- // Skip thinking blocks too - already streamed via thinking_delta
165
- // But keep for non-streaming contexts (direct API responses)
166
- // Only emit if we haven't seen streaming deltas (detected by having results already)
167
- }
168
- }
169
-
170
- if (results.length === 1) {
171
- return results[0];
172
- }
173
-
174
- if (results.length > 1) {
175
- return {
176
- type: 'multi',
177
- events: results,
178
- };
179
- }
180
-
181
- return null;
182
- }
183
-
184
- /**
185
- * Parse user message (tool results)
186
- */
187
- function parseUserMessage(message) {
188
- const results = [];
189
-
190
- for (const block of message.content) {
191
- if (block.type === 'tool_result') {
192
- results.push({
193
- type: 'tool_result',
194
- toolId: block.tool_use_id,
195
- content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
196
- isError: block.is_error || false,
197
- });
198
- }
199
- }
200
-
201
- if (results.length === 1) {
202
- return results[0];
203
- }
204
-
205
- if (results.length > 1) {
206
- return {
207
- type: 'multi',
208
- events: results,
209
- };
210
- }
211
-
212
- return null;
213
- }
214
-
215
- /**
216
- * Parse multiple lines of NDJSON
217
- * @param {string} chunk - Chunk of text potentially containing multiple JSON lines
218
- * @returns {Array} Array of parsed events
219
- */
220
- function parseChunk(chunk) {
221
- const events = [];
222
- const lines = chunk.split('\n');
223
-
224
- for (const line of lines) {
225
- const event = parseStreamLine(line);
226
- if (event) {
227
- if (event.type === 'multi') {
228
- events.push(...event.events);
229
- } else {
230
- events.push(event);
231
- }
232
- }
233
- }
234
-
235
- return events;
236
- }
5
+ const { parseEvent, parseChunk } = require('../src/providers/anthropic/output-parser');
237
6
 
238
7
  module.exports = {
239
- parseStreamLine,
8
+ parseEvent,
240
9
  parseChunk,
241
- parseStreamEvent,
242
- parseAssistantMessage,
243
- parseUserMessage,
244
10
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@covibes/zeroshot",
3
- "version": "5.2.1",
4
- "description": "Multi-agent orchestration engine for Claude - cluster coordinator and CLI",
3
+ "version": "5.4.0",
4
+ "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
7
7
  "zeroshot": "./cli/index.js"
@@ -11,14 +11,18 @@
11
11
  "test": "tests"
12
12
  },
13
13
  "scripts": {
14
- "test": "mocha 'tests/**/*.test.js'",
15
- "test:coverage": "c8 npm test",
16
- "test:coverage:report": "c8 --reporter=html npm test && echo 'Coverage report generated at coverage/index.html'",
14
+ "test": "mocha 'tests/unit/**/*.test.js' 'tests/*.test.js' --parallel",
15
+ "test:unit": "mocha 'tests/unit/**/*.test.js' 'tests/*.test.js' --parallel",
16
+ "test:slow": "mocha 'tests/integration/**/*.test.js' --timeout 180000",
17
+ "test:all": "npm run test && npm run test:slow",
18
+ "test:coverage": "c8 npm run test:unit",
19
+ "test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
17
20
  "postinstall": "node scripts/fix-node-pty-permissions.js",
18
21
  "start": "node cli/index.js",
19
22
  "typecheck": "tsc --noEmit",
20
23
  "lint": "eslint .",
21
24
  "lint:fix": "eslint . --fix",
25
+ "validate:templates": "node scripts/validate-templates.js",
22
26
  "format": "prettier --write .",
23
27
  "format:check": "prettier --check .",
24
28
  "deadcode": "ts-prune --skip node_modules",
@@ -55,6 +59,8 @@
55
59
  "multi-agent",
56
60
  "orchestration",
57
61
  "claude",
62
+ "codex",
63
+ "gemini",
58
64
  "automation"
59
65
  ],
60
66
  "author": "Covibes",
@@ -112,10 +118,14 @@
112
118
  "depcheck": "^1.4.7",
113
119
  "eslint": "^9.39.1",
114
120
  "eslint-config-prettier": "^10.1.8",
121
+ "eslint-plugin-security": "^3.0.1",
122
+ "eslint-plugin-sonarjs": "^3.0.5",
115
123
  "eslint-plugin-unused-imports": "^4.3.0",
116
124
  "husky": "^9.1.7",
117
125
  "jscpd": "^3.5.10",
126
+ "lint-staged": "^16.2.7",
118
127
  "mocha": "^11.7.5",
128
+ "prettier": "^3.7.4",
119
129
  "semantic-release": "^25.0.2",
120
130
  "sinon": "^21.0.0",
121
131
  "ts-prune": "^0.10.3",
@@ -123,6 +133,17 @@
123
133
  "unimported": "^1.31.0"
124
134
  },
125
135
  "overrides": {
126
- "xml2js": "^0.5.0"
136
+ "xml2js": "^0.5.0",
137
+ "diff": ">=8.0.3",
138
+ "tar": ">=7.5.3",
139
+ "undici": ">=7.18.2"
140
+ },
141
+ "lint-staged": {
142
+ "*.{js,mjs,cjs}": [
143
+ "eslint --fix"
144
+ ],
145
+ "*.{js,mjs,cjs,json,md}": [
146
+ "prettier --write"
147
+ ]
127
148
  }
128
149
  }
@@ -0,0 +1,170 @@
1
+ #!/bin/bash
2
+ # Setup GitHub merge queue and branch protection for zeroshot
3
+ # Run once after creating the repo or to update settings
4
+
5
+ set -e
6
+
7
+ REPO="covibes/zeroshot"
8
+
9
+ echo "╔═══════════════════════════════════════════════════════════════════╗"
10
+ echo "║ Setting up merge queue for $REPO ║"
11
+ echo "╚═══════════════════════════════════════════════════════════════════╝"
12
+ echo ""
13
+
14
+ # Check gh is authenticated
15
+ if ! gh auth status &>/dev/null; then
16
+ echo "❌ ERROR: Not authenticated with GitHub CLI"
17
+ echo " Run: gh auth login"
18
+ exit 1
19
+ fi
20
+
21
+ # Check we have admin access
22
+ if ! gh api "repos/$REPO" --jq '.permissions.admin' | grep -q true; then
23
+ echo "❌ ERROR: You need admin access to $REPO"
24
+ exit 1
25
+ fi
26
+
27
+ echo "✓ Authenticated with admin access"
28
+ echo ""
29
+
30
+ # ============================================================================
31
+ # Configure 'dev' branch protection (merge target)
32
+ # ============================================================================
33
+
34
+ echo "→ Configuring 'dev' branch protection..."
35
+
36
+ gh api --method PUT "repos/$REPO/branches/dev/protection" \
37
+ --input - <<EOF
38
+ {
39
+ "required_status_checks": {
40
+ "strict": true,
41
+ "contexts": ["check", "install-matrix (ubuntu-latest, 20)", "install-matrix (macos-latest, 20)"]
42
+ },
43
+ "enforce_admins": false,
44
+ "required_pull_request_reviews": null,
45
+ "restrictions": null,
46
+ "allow_force_pushes": false,
47
+ "allow_deletions": false,
48
+ "required_linear_history": true,
49
+ "required_conversation_resolution": false
50
+ }
51
+ EOF
52
+
53
+ echo "✓ 'dev' branch protection configured"
54
+
55
+ # Enable merge queue for dev branch
56
+ echo "→ Enabling merge queue for 'dev' branch..."
57
+
58
+ # Note: Merge queue requires GitHub Enterprise or public repos with Actions
59
+ # Using the ruleset API which supports merge queue
60
+ gh api --method POST "repos/$REPO/rulesets" \
61
+ --input - <<EOF 2>/dev/null || echo " (ruleset may already exist)"
62
+ {
63
+ "name": "dev-merge-queue",
64
+ "target": "branch",
65
+ "enforcement": "active",
66
+ "conditions": {
67
+ "ref_name": {
68
+ "include": ["refs/heads/dev"],
69
+ "exclude": []
70
+ }
71
+ },
72
+ "rules": [
73
+ {
74
+ "type": "merge_queue",
75
+ "parameters": {
76
+ "check_response_timeout_minutes": 30,
77
+ "grouping_strategy": "ALLGREEN",
78
+ "max_entries_to_build": 5,
79
+ "max_entries_to_merge": 5,
80
+ "merge_method": "SQUASH",
81
+ "min_entries_to_merge": 1,
82
+ "min_entries_to_merge_wait_minutes": 1
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ EOF
88
+
89
+ echo "✓ Merge queue enabled for 'dev'"
90
+
91
+ # ============================================================================
92
+ # Configure 'main' branch protection (release branch)
93
+ # ============================================================================
94
+
95
+ echo "→ Configuring 'main' branch protection..."
96
+
97
+ gh api --method PUT "repos/$REPO/branches/main/protection" \
98
+ --input - <<EOF
99
+ {
100
+ "required_status_checks": {
101
+ "strict": true,
102
+ "contexts": ["check", "install-matrix (ubuntu-latest, 20)", "install-matrix (macos-latest, 20)"]
103
+ },
104
+ "enforce_admins": false,
105
+ "required_pull_request_reviews": {
106
+ "required_approving_review_count": 1,
107
+ "dismiss_stale_reviews": true,
108
+ "require_code_owner_reviews": false
109
+ },
110
+ "restrictions": null,
111
+ "allow_force_pushes": false,
112
+ "allow_deletions": false,
113
+ "required_linear_history": true,
114
+ "required_conversation_resolution": false
115
+ }
116
+ EOF
117
+
118
+ echo "✓ 'main' branch protection configured"
119
+
120
+ # ============================================================================
121
+ # Configure repository settings
122
+ # ============================================================================
123
+
124
+ echo "→ Configuring repository settings..."
125
+
126
+ gh api --method PATCH "repos/$REPO" \
127
+ --input - <<EOF
128
+ {
129
+ "allow_squash_merge": true,
130
+ "allow_merge_commit": false,
131
+ "allow_rebase_merge": false,
132
+ "squash_merge_commit_title": "PR_TITLE",
133
+ "squash_merge_commit_message": "PR_BODY",
134
+ "delete_branch_on_merge": true,
135
+ "allow_auto_merge": true
136
+ }
137
+ EOF
138
+
139
+ echo "✓ Repository settings configured"
140
+
141
+ # ============================================================================
142
+ # Summary
143
+ # ============================================================================
144
+
145
+ echo ""
146
+ echo "╔═══════════════════════════════════════════════════════════════════╗"
147
+ echo "║ ✓ Merge queue setup complete! ║"
148
+ echo "╚═══════════════════════════════════════════════════════════════════╝"
149
+ echo ""
150
+ echo "Workflow:"
151
+ echo " feature-branch (local)"
152
+ echo " ↓"
153
+ echo " pre-push hook → lint + typecheck (~5s)"
154
+ echo " ↓"
155
+ echo " push to origin/feature-branch"
156
+ echo " ↓"
157
+ echo " gh pr create --base dev"
158
+ echo " ↓"
159
+ echo " CI runs tests on PR branch"
160
+ echo " ↓"
161
+ echo " gh pr merge --auto --squash → enters merge queue"
162
+ echo " ↓"
163
+ echo " Queue rebases PR on latest dev + runs CI again"
164
+ echo " ↓"
165
+ echo " Merge to dev (only if CI passes on rebased code)"
166
+ echo ""
167
+ echo "Release workflow:"
168
+ echo " gh pr create --base main --head dev --title \"Release\""
169
+ echo " → CI passes → merge → semantic-release publishes"
170
+ echo ""
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate all cluster templates for config errors
4
+ * Run in CI to prevent broken templates from being merged
5
+ *
6
+ * Usage: node scripts/validate-templates.js
7
+ * Exit codes: 0 = all valid, 1 = validation errors found
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { validateConfig } = require('../src/config-validator');
13
+
14
+ const TEMPLATES_DIR = path.join(__dirname, '../cluster-templates');
15
+
16
+ function findJsonFiles(dir) {
17
+ const files = [];
18
+ if (!fs.existsSync(dir)) return files;
19
+
20
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
21
+ const fullPath = path.join(dir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ files.push(...findJsonFiles(fullPath));
24
+ } else if (entry.name.endsWith('.json')) {
25
+ files.push(fullPath);
26
+ }
27
+ }
28
+ return files;
29
+ }
30
+
31
+ function validateTemplate(filePath) {
32
+ const relativePath = path.relative(process.cwd(), filePath);
33
+
34
+ try {
35
+ const content = fs.readFileSync(filePath, 'utf-8');
36
+ const config = JSON.parse(content);
37
+
38
+ // Skip non-cluster configs (like package.json)
39
+ if (!config.agents && !config.name) {
40
+ return { valid: true, skipped: true };
41
+ }
42
+
43
+ const result = validateConfig(config);
44
+
45
+ if (!result.valid) {
46
+ console.error(`\n❌ ${relativePath}`);
47
+ for (const error of result.errors) {
48
+ console.error(` ERROR: ${error}`);
49
+ }
50
+ } else if (result.warnings.length > 0) {
51
+ console.warn(`\n⚠️ ${relativePath}`);
52
+ for (const warning of result.warnings) {
53
+ console.warn(` WARN: ${warning}`);
54
+ }
55
+ } else {
56
+ console.log(`✓ ${relativePath}`);
57
+ }
58
+
59
+ return result;
60
+ } catch (err) {
61
+ console.error(`\n❌ ${relativePath}`);
62
+ console.error(` PARSE ERROR: ${err.message}`);
63
+ return { valid: false, errors: [err.message], warnings: [] };
64
+ }
65
+ }
66
+
67
+ function main() {
68
+ console.log('Validating cluster templates...\n');
69
+
70
+ const templateFiles = [...findJsonFiles(TEMPLATES_DIR)];
71
+
72
+ let hasErrors = false;
73
+ let validated = 0;
74
+ let skipped = 0;
75
+
76
+ for (const file of templateFiles) {
77
+ const result = validateTemplate(file);
78
+ if (result.skipped) {
79
+ skipped++;
80
+ } else {
81
+ validated++;
82
+ if (!result.valid) {
83
+ hasErrors = true;
84
+ }
85
+ }
86
+ }
87
+
88
+ console.log(`\n${'='.repeat(60)}`);
89
+ console.log(`Validated: ${validated} templates, Skipped: ${skipped} files`);
90
+
91
+ if (hasErrors) {
92
+ console.error('\n❌ VALIDATION FAILED - Fix errors above before merging\n');
93
+ process.exit(1);
94
+ } else {
95
+ console.log('\n✓ All templates valid\n');
96
+ process.exit(0);
97
+ }
98
+ }
99
+
100
+ main();