@probelabs/visor 0.1.124 → 0.1.126

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 (195) hide show
  1. package/dist/config.d.ts.map +1 -1
  2. package/dist/docs/DEPLOYMENT.md +117 -11
  3. package/dist/docs/GITHUB_CHECKS.md +18 -4
  4. package/dist/docs/NPM_USAGE.md +112 -39
  5. package/dist/docs/action-reference.md +63 -9
  6. package/dist/docs/advanced-ai.md +58 -51
  7. package/dist/docs/ai-configuration.md +99 -11
  8. package/dist/docs/ai-custom-tools-usage.md +70 -33
  9. package/dist/docs/ai-custom-tools.md +50 -27
  10. package/dist/docs/architecture.md +1232 -0
  11. package/dist/docs/bot-transports-rfc.md +13 -3
  12. package/dist/docs/ci-cli-mode.md +116 -8
  13. package/dist/docs/claude-code.md +111 -41
  14. package/dist/docs/command-provider.md +37 -15
  15. package/dist/docs/commands.md +252 -6
  16. package/dist/docs/configuration.md +138 -4
  17. package/dist/docs/contributing.md +737 -0
  18. package/dist/docs/custom-tools.md +39 -8
  19. package/dist/docs/dashboards/README.md +33 -19
  20. package/dist/docs/debug-visualizer-progress.md +14 -13
  21. package/dist/docs/debug-visualizer-rfc.md +14 -13
  22. package/dist/docs/debug-visualizer.md +30 -5
  23. package/dist/docs/debugging.md +73 -8
  24. package/dist/docs/default-output-schema.md +24 -20
  25. package/dist/docs/dependencies.md +75 -21
  26. package/dist/docs/dev-playbook.md +85 -9
  27. package/dist/docs/engine-pause-resume-rfc.md +11 -11
  28. package/dist/docs/engine-state-machine-plan.md +10 -3
  29. package/dist/docs/event-driven-github-integration-rfc.md +20 -11
  30. package/dist/docs/event-triggers.md +95 -6
  31. package/dist/docs/execution-statistics-rfc.md +16 -4
  32. package/dist/docs/fact-validator-gap-analysis.md +12 -1
  33. package/dist/docs/fact-validator-implementation-plan.md +19 -11
  34. package/dist/docs/fail-if.md +116 -11
  35. package/dist/docs/failure-conditions-implementation.md +40 -6
  36. package/dist/docs/failure-conditions-schema.md +243 -87
  37. package/dist/docs/failure-routing-rfc.md +43 -18
  38. package/dist/docs/failure-routing.md +80 -23
  39. package/dist/docs/faq.md +836 -0
  40. package/dist/docs/foreach-dependency-propagation.md +32 -15
  41. package/dist/docs/github-ops.md +6 -5
  42. package/dist/docs/glossary.md +322 -0
  43. package/dist/docs/goto-forward-run-plan.md +23 -10
  44. package/dist/docs/guides/criticality-modes.md +15 -13
  45. package/dist/docs/guides/fault-management-and-contracts.md +8 -5
  46. package/dist/docs/guides/workflow-style-guide.md +17 -8
  47. package/dist/docs/http.md +102 -3
  48. package/dist/docs/human-input-provider.md +20 -36
  49. package/dist/docs/index.md +206 -0
  50. package/dist/docs/lifecycle-hooks.md +322 -2
  51. package/dist/docs/limits.md +20 -5
  52. package/dist/docs/liquid-templates.md +86 -14
  53. package/dist/docs/loop-routing-refactor.md +4 -2
  54. package/dist/docs/mcp-provider.md +53 -19
  55. package/dist/docs/mcp.md +27 -1
  56. package/dist/docs/memory.md +7 -2
  57. package/dist/docs/migration.md +596 -0
  58. package/dist/docs/observability.md +227 -6
  59. package/dist/docs/output-formats.md +388 -9
  60. package/dist/docs/output-history.md +36 -6
  61. package/dist/docs/performance.md +510 -4
  62. package/dist/docs/pluggable.md +95 -4
  63. package/dist/docs/proposals/snapshot-scope-execution.md +6 -5
  64. package/dist/docs/providers/git-checkout.md +16 -14
  65. package/dist/docs/providers/noop.md +696 -0
  66. package/dist/docs/recipes.md +8 -9
  67. package/dist/docs/rfc/git-checkout-step.md +3 -1
  68. package/dist/docs/rfc/on_init-hook.md +18 -5
  69. package/dist/docs/rfc/workspace-isolation.md +16 -0
  70. package/dist/docs/roadmap/criticality-implementation-tasks.md +27 -27
  71. package/dist/docs/router-patterns.md +155 -43
  72. package/dist/docs/schema-templates.md +51 -15
  73. package/dist/docs/script.md +162 -13
  74. package/dist/docs/sdk.md +46 -12
  75. package/dist/docs/security.md +464 -5
  76. package/dist/docs/slack-integration.md +481 -0
  77. package/dist/docs/tag-filtering.md +60 -20
  78. package/dist/docs/telemetry-setup.md +157 -46
  79. package/dist/docs/test-framework-rfc.md +37 -36
  80. package/dist/docs/testing/assertions.md +92 -4
  81. package/dist/docs/testing/ci.md +56 -7
  82. package/dist/docs/testing/cli.md +57 -15
  83. package/dist/docs/testing/cookbook.md +53 -20
  84. package/dist/docs/testing/dsl-reference.md +110 -9
  85. package/dist/docs/testing/fixtures-and-mocks.md +28 -3
  86. package/dist/docs/testing/flows.md +59 -4
  87. package/dist/docs/testing/getting-started.md +14 -13
  88. package/dist/docs/testing/troubleshooting.md +39 -2
  89. package/dist/docs/timeouts.md +174 -18
  90. package/dist/docs/troubleshooting.md +176 -6
  91. package/dist/docs/workflow-creation-guide.md +101 -3
  92. package/dist/docs/workflows.md +138 -41
  93. package/dist/examples/README.md +169 -4
  94. package/dist/examples/ai-custom-tools-simple.yaml +2 -3
  95. package/dist/examples/cron-webhook-config.yaml +15 -0
  96. package/dist/examples/forEach-example.yaml +6 -0
  97. package/dist/examples/git-checkout-basic.yaml +4 -0
  98. package/dist/examples/git-checkout-compare.yaml +6 -0
  99. package/dist/examples/git-checkout-cross-repo.yaml +7 -0
  100. package/dist/examples/http-integration-config.yaml +30 -0
  101. package/dist/examples/https-server-config.yaml +15 -0
  102. package/dist/examples/mcp-provider-example.yaml +10 -10
  103. package/dist/examples/transform-example.yaml +3 -0
  104. package/dist/examples/webhook-pipeline-config.yaml +18 -0
  105. package/dist/examples/workflows/workflow-composition-example.yaml +4 -0
  106. package/dist/frontends/slack-frontend.d.ts +2 -0
  107. package/dist/frontends/slack-frontend.d.ts.map +1 -1
  108. package/dist/generated/config-schema.d.ts +11 -7
  109. package/dist/generated/config-schema.d.ts.map +1 -1
  110. package/dist/generated/config-schema.json +11 -7
  111. package/dist/index.js +3127 -974
  112. package/dist/output/traces/{run-2026-01-28T16-15-24-569Z.ndjson → run-2026-01-31T16-37-22-321Z.ndjson} +84 -84
  113. package/dist/output/traces/{run-2026-01-28T16-16-09-757Z.ndjson → run-2026-01-31T16-38-06-031Z.ndjson} +1013 -1013
  114. package/dist/providers/ai-check-provider.d.ts +9 -2
  115. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  116. package/dist/providers/command-check-provider.d.ts.map +1 -1
  117. package/dist/providers/mcp-custom-sse-server.d.ts +17 -1
  118. package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
  119. package/dist/providers/workflow-check-provider.d.ts.map +1 -1
  120. package/dist/providers/workflow-tool-executor.d.ts +68 -0
  121. package/dist/providers/workflow-tool-executor.d.ts.map +1 -0
  122. package/dist/sdk/{check-provider-registry-AQ3JETBG.mjs → check-provider-registry-3KI5RKXT.mjs} +6 -5
  123. package/dist/sdk/check-provider-registry-IYILYY35.mjs +28 -0
  124. package/dist/sdk/chunk-2CPMMNIX.mjs +1459 -0
  125. package/dist/sdk/chunk-2CPMMNIX.mjs.map +1 -0
  126. package/dist/sdk/chunk-5LI6T4O3.mjs +3600 -0
  127. package/dist/sdk/chunk-5LI6T4O3.mjs.map +1 -0
  128. package/dist/sdk/{chunk-YLQ4UN62.mjs → chunk-A4PGHURG.mjs} +6838 -6257
  129. package/dist/sdk/chunk-A4PGHURG.mjs.map +1 -0
  130. package/dist/sdk/chunk-EXFGO4FX.mjs +147 -0
  131. package/dist/sdk/chunk-EXFGO4FX.mjs.map +1 -0
  132. package/dist/sdk/chunk-PJ7K5UFC.mjs +17732 -0
  133. package/dist/sdk/chunk-PJ7K5UFC.mjs.map +1 -0
  134. package/dist/sdk/{chunk-BHZ4CKUS.mjs → chunk-PXFIALUH.mjs} +77 -8
  135. package/dist/sdk/chunk-PXFIALUH.mjs.map +1 -0
  136. package/dist/sdk/{chunk-PVITVJ6J.mjs → chunk-RTKJXNZS.mjs} +32 -9
  137. package/dist/sdk/chunk-RTKJXNZS.mjs.map +1 -0
  138. package/dist/sdk/chunk-VW2GBXQT.mjs +606 -0
  139. package/dist/sdk/chunk-VW2GBXQT.mjs.map +1 -0
  140. package/dist/sdk/{config-RQQPMLRD.mjs → config-5AUYQFHE.mjs} +2 -2
  141. package/dist/sdk/config-6CUVEH7H.mjs +16 -0
  142. package/dist/sdk/config-6CUVEH7H.mjs.map +1 -0
  143. package/dist/sdk/{github-frontend-6Q4BISZX.mjs → github-frontend-BZ4N3BFZ.mjs} +7 -3
  144. package/dist/sdk/github-frontend-BZ4N3BFZ.mjs.map +1 -0
  145. package/dist/sdk/host-4MT3EW2I.mjs +52 -0
  146. package/dist/sdk/{host-P5NQICP7.mjs → host-NYWXLIFC.mjs} +2 -2
  147. package/dist/sdk/host-NYWXLIFC.mjs.map +1 -0
  148. package/dist/sdk/{routing-DEY2AIXM.mjs → routing-6R42GXUO.mjs} +2 -2
  149. package/dist/sdk/routing-6R42GXUO.mjs.map +1 -0
  150. package/dist/sdk/routing-7FXPULTO.mjs +24 -0
  151. package/dist/sdk/routing-7FXPULTO.mjs.map +1 -0
  152. package/dist/sdk/sdk.d.mts +3 -1
  153. package/dist/sdk/sdk.d.ts +3 -1
  154. package/dist/sdk/sdk.js +12163 -11204
  155. package/dist/sdk/sdk.js.map +1 -1
  156. package/dist/sdk/sdk.mjs +14 -10
  157. package/dist/sdk/sdk.mjs.map +1 -1
  158. package/dist/sdk/slack-frontend-JUT3TYVC.mjs +821 -0
  159. package/dist/sdk/slack-frontend-JUT3TYVC.mjs.map +1 -0
  160. package/dist/sdk/workflow-check-provider-H3CUOLUD.mjs +28 -0
  161. package/dist/sdk/workflow-check-provider-H3CUOLUD.mjs.map +1 -0
  162. package/dist/sdk/workflow-check-provider-YUNNF4KC.mjs +28 -0
  163. package/dist/sdk/workflow-check-provider-YUNNF4KC.mjs.map +1 -0
  164. package/dist/sdk/workflow-registry-KFWSDSLM.mjs +12 -0
  165. package/dist/sdk/workflow-registry-KFWSDSLM.mjs.map +1 -0
  166. package/dist/slack/socket-runner.d.ts +2 -0
  167. package/dist/slack/socket-runner.d.ts.map +1 -1
  168. package/dist/state-machine/context/workflow-inputs.d.ts +20 -0
  169. package/dist/state-machine/context/workflow-inputs.d.ts.map +1 -0
  170. package/dist/state-machine/dispatch/execution-invoker.d.ts.map +1 -1
  171. package/dist/state-machine/dispatch/foreach-processor.d.ts.map +1 -1
  172. package/dist/state-machine/dispatch/stats-manager.d.ts.map +1 -1
  173. package/dist/state-machine/states/level-dispatch.d.ts.map +1 -1
  174. package/dist/state-machine/states/routing.d.ts +2 -1
  175. package/dist/state-machine/states/routing.d.ts.map +1 -1
  176. package/dist/traces/{run-2026-01-28T16-15-24-569Z.ndjson → run-2026-01-31T16-37-22-321Z.ndjson} +84 -84
  177. package/dist/traces/{run-2026-01-28T16-16-09-757Z.ndjson → run-2026-01-31T16-38-06-031Z.ndjson} +1013 -1013
  178. package/dist/types/config.d.ts +3 -1
  179. package/dist/types/config.d.ts.map +1 -1
  180. package/dist/utils/human-id.d.ts +12 -0
  181. package/dist/utils/human-id.d.ts.map +1 -0
  182. package/dist/utils/worktree-manager.d.ts +3 -0
  183. package/dist/utils/worktree-manager.d.ts.map +1 -1
  184. package/dist/workflow-executor.d.ts.map +1 -1
  185. package/dist/workflow-registry.d.ts +1 -0
  186. package/dist/workflow-registry.d.ts.map +1 -1
  187. package/package.json +2 -2
  188. package/dist/sdk/chunk-BHZ4CKUS.mjs.map +0 -1
  189. package/dist/sdk/chunk-PVITVJ6J.mjs.map +0 -1
  190. package/dist/sdk/chunk-YLQ4UN62.mjs.map +0 -1
  191. package/dist/sdk/github-frontend-6Q4BISZX.mjs.map +0 -1
  192. /package/dist/sdk/{check-provider-registry-AQ3JETBG.mjs.map → check-provider-registry-3KI5RKXT.mjs.map} +0 -0
  193. /package/dist/sdk/{config-RQQPMLRD.mjs.map → check-provider-registry-IYILYY35.mjs.map} +0 -0
  194. /package/dist/sdk/{routing-DEY2AIXM.mjs.map → config-5AUYQFHE.mjs.map} +0 -0
  195. /package/dist/sdk/{host-P5NQICP7.mjs.map → host-4MT3EW2I.mjs.map} +0 -0
@@ -0,0 +1,3600 @@
1
+ import {
2
+ ConfigMerger,
3
+ config_merger_exports,
4
+ init_config_merger
5
+ } from "./chunk-O5EZDNYL.mjs";
6
+ import {
7
+ init_sandbox,
8
+ validateJsSyntax
9
+ } from "./chunk-BOVFH3LI.mjs";
10
+ import {
11
+ init_logger,
12
+ logger
13
+ } from "./chunk-3NMLT3YS.mjs";
14
+ import {
15
+ __esm,
16
+ __export,
17
+ __require,
18
+ __toCommonJS
19
+ } from "./chunk-WMJKH4XE.mjs";
20
+
21
+ // src/utils/config-loader.ts
22
+ import * as fs from "fs";
23
+ import * as path from "path";
24
+ import * as yaml from "js-yaml";
25
+ var ConfigLoader;
26
+ var init_config_loader = __esm({
27
+ "src/utils/config-loader.ts"() {
28
+ "use strict";
29
+ ConfigLoader = class {
30
+ constructor(options = {}) {
31
+ this.options = options;
32
+ this.options = {
33
+ allowRemote: true,
34
+ cacheTTL: 5 * 60 * 1e3,
35
+ // 5 minutes
36
+ timeout: 30 * 1e3,
37
+ // 30 seconds
38
+ maxDepth: 10,
39
+ allowedRemotePatterns: [],
40
+ // Empty by default for security
41
+ projectRoot: this.findProjectRoot(),
42
+ ...options
43
+ };
44
+ }
45
+ cache = /* @__PURE__ */ new Map();
46
+ loadedConfigs = /* @__PURE__ */ new Set();
47
+ /**
48
+ * Determine the source type from a string
49
+ */
50
+ getSourceType(source) {
51
+ if (source === "default") {
52
+ return "default" /* DEFAULT */;
53
+ }
54
+ if (source.startsWith("http://") || source.startsWith("https://")) {
55
+ return "remote" /* REMOTE */;
56
+ }
57
+ return "local" /* LOCAL */;
58
+ }
59
+ /**
60
+ * Fetch configuration from any source
61
+ */
62
+ async fetchConfig(source, currentDepth = 0) {
63
+ if (currentDepth >= (this.options.maxDepth || 10)) {
64
+ throw new Error(
65
+ `Maximum extends depth (${this.options.maxDepth}) exceeded. Check for circular dependencies.`
66
+ );
67
+ }
68
+ const normalizedSource = this.normalizeSource(source);
69
+ if (this.loadedConfigs.has(normalizedSource)) {
70
+ throw new Error(
71
+ `Circular dependency detected: ${normalizedSource} is already in the extends chain`
72
+ );
73
+ }
74
+ const sourceType = this.getSourceType(source);
75
+ try {
76
+ this.loadedConfigs.add(normalizedSource);
77
+ switch (sourceType) {
78
+ case "default" /* DEFAULT */:
79
+ return await this.fetchDefaultConfig();
80
+ case "remote" /* REMOTE */:
81
+ if (!this.options.allowRemote) {
82
+ throw new Error(
83
+ "Remote extends are disabled. Enable with --allow-remote-extends or remove VISOR_NO_REMOTE_EXTENDS environment variable."
84
+ );
85
+ }
86
+ return await this.fetchRemoteConfig(source);
87
+ case "local" /* LOCAL */:
88
+ return await this.fetchLocalConfig(source);
89
+ default:
90
+ throw new Error(`Unknown configuration source: ${source}`);
91
+ }
92
+ } finally {
93
+ this.loadedConfigs.delete(normalizedSource);
94
+ }
95
+ }
96
+ /**
97
+ * Normalize source path/URL for comparison
98
+ */
99
+ normalizeSource(source) {
100
+ const sourceType = this.getSourceType(source);
101
+ switch (sourceType) {
102
+ case "default" /* DEFAULT */:
103
+ return "default";
104
+ case "remote" /* REMOTE */:
105
+ return source.toLowerCase();
106
+ case "local" /* LOCAL */:
107
+ const basePath = this.options.baseDir || process.cwd();
108
+ return path.resolve(basePath, source);
109
+ default:
110
+ return source;
111
+ }
112
+ }
113
+ /**
114
+ * Load configuration from local file system
115
+ */
116
+ async fetchLocalConfig(filePath) {
117
+ const basePath = this.options.baseDir || process.cwd();
118
+ const resolvedPath = path.resolve(basePath, filePath);
119
+ this.validateLocalPath(resolvedPath);
120
+ try {
121
+ const content = fs.readFileSync(resolvedPath, "utf8");
122
+ const config = yaml.load(content);
123
+ if (!config || typeof config !== "object") {
124
+ throw new Error(`Invalid YAML in configuration file: ${resolvedPath}`);
125
+ }
126
+ if (config.include && !config.extends) {
127
+ const inc = config.include;
128
+ config.extends = Array.isArray(inc) ? inc : [inc];
129
+ delete config.include;
130
+ }
131
+ const previousBaseDir = this.options.baseDir;
132
+ this.options.baseDir = path.dirname(resolvedPath);
133
+ try {
134
+ if (config.extends) {
135
+ const processedConfig = await this.processExtends(config);
136
+ return processedConfig;
137
+ }
138
+ return config;
139
+ } finally {
140
+ this.options.baseDir = previousBaseDir;
141
+ }
142
+ } catch (error) {
143
+ if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
144
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
145
+ }
146
+ if (error instanceof Error) {
147
+ throw new Error(`Failed to load configuration from ${resolvedPath}: ${error.message}`);
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+ /**
153
+ * Fetch configuration from remote URL
154
+ */
155
+ async fetchRemoteConfig(url) {
156
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
157
+ throw new Error(`Invalid URL: ${url}. Only HTTP and HTTPS protocols are supported.`);
158
+ }
159
+ this.validateRemoteURL(url);
160
+ const cacheEntry = this.cache.get(url);
161
+ if (cacheEntry && Date.now() - cacheEntry.timestamp < cacheEntry.ttl) {
162
+ const outputFormat2 = process.env.VISOR_OUTPUT_FORMAT;
163
+ const logFn2 = outputFormat2 === "json" || outputFormat2 === "sarif" ? console.error : console.log;
164
+ logFn2(`\u{1F4E6} Using cached configuration from: ${url}`);
165
+ return cacheEntry.config;
166
+ }
167
+ const outputFormat = process.env.VISOR_OUTPUT_FORMAT;
168
+ const logFn = outputFormat === "json" || outputFormat === "sarif" ? console.error : console.log;
169
+ logFn(`\u2B07\uFE0F Fetching remote configuration from: ${url}`);
170
+ const controller = new AbortController();
171
+ const timeoutMs = this.options.timeout ?? 3e4;
172
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
173
+ try {
174
+ const response = await fetch(url, {
175
+ signal: controller.signal,
176
+ headers: {
177
+ "User-Agent": "Visor/1.0"
178
+ }
179
+ });
180
+ if (!response.ok) {
181
+ throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
182
+ }
183
+ const content = await response.text();
184
+ const config = yaml.load(content);
185
+ if (!config || typeof config !== "object") {
186
+ throw new Error(`Invalid YAML in remote configuration: ${url}`);
187
+ }
188
+ this.cache.set(url, {
189
+ config,
190
+ timestamp: Date.now(),
191
+ ttl: this.options.cacheTTL || 5 * 60 * 1e3
192
+ });
193
+ if (config.extends) {
194
+ return await this.processExtends(config);
195
+ }
196
+ return config;
197
+ } catch (error) {
198
+ if (error instanceof Error) {
199
+ if (error.name === "AbortError") {
200
+ throw new Error(`Timeout fetching configuration from ${url} (${timeoutMs}ms)`);
201
+ }
202
+ throw new Error(`Failed to fetch remote configuration from ${url}: ${error.message}`);
203
+ }
204
+ throw error;
205
+ } finally {
206
+ clearTimeout(timeoutId);
207
+ }
208
+ }
209
+ /**
210
+ * Load bundled default configuration
211
+ */
212
+ async fetchDefaultConfig() {
213
+ const possiblePaths = [
214
+ // Only support new non-dot filename
215
+ path.join(__dirname, "defaults", "visor.yaml"),
216
+ // When running from source
217
+ path.join(__dirname, "..", "..", "defaults", "visor.yaml"),
218
+ // Try via package root
219
+ this.findPackageRoot() ? path.join(this.findPackageRoot(), "defaults", "visor.yaml") : "",
220
+ // GitHub Action environment variable
221
+ process.env.GITHUB_ACTION_PATH ? path.join(process.env.GITHUB_ACTION_PATH, "defaults", "visor.yaml") : "",
222
+ process.env.GITHUB_ACTION_PATH ? path.join(process.env.GITHUB_ACTION_PATH, "dist", "defaults", "visor.yaml") : ""
223
+ ].filter((p) => p);
224
+ let defaultConfigPath;
225
+ for (const possiblePath of possiblePaths) {
226
+ if (fs.existsSync(possiblePath)) {
227
+ defaultConfigPath = possiblePath;
228
+ break;
229
+ }
230
+ }
231
+ if (defaultConfigPath) {
232
+ console.error(`\u{1F4E6} Loading bundled default configuration from ${defaultConfigPath}`);
233
+ const content = fs.readFileSync(defaultConfigPath, "utf8");
234
+ let config = yaml.load(content);
235
+ if (!config || typeof config !== "object") {
236
+ throw new Error("Invalid default configuration");
237
+ }
238
+ if (config.include && !config.extends) {
239
+ const inc = config.include;
240
+ config.extends = Array.isArray(inc) ? inc : [inc];
241
+ delete config.include;
242
+ }
243
+ config = this.normalizeStepsAndChecks(config);
244
+ if (config.extends) {
245
+ const previousBaseDir = this.options.baseDir;
246
+ try {
247
+ this.options.baseDir = path.dirname(defaultConfigPath);
248
+ return await this.processExtends(config);
249
+ } finally {
250
+ this.options.baseDir = previousBaseDir;
251
+ }
252
+ }
253
+ return config;
254
+ }
255
+ console.warn("\u26A0\uFE0F Bundled default configuration not found, using minimal defaults");
256
+ return {
257
+ version: "1.0",
258
+ checks: {},
259
+ output: {
260
+ pr_comment: {
261
+ format: "markdown",
262
+ group_by: "check",
263
+ collapse: true
264
+ }
265
+ }
266
+ };
267
+ }
268
+ /**
269
+ * Process extends directive in a configuration
270
+ */
271
+ async processExtends(config) {
272
+ if (!config.extends) {
273
+ return config;
274
+ }
275
+ const extends_ = Array.isArray(config.extends) ? config.extends : [config.extends];
276
+ const { extends: _extendsField, ...configWithoutExtends } = config;
277
+ const parentConfigs = [];
278
+ for (const source of extends_) {
279
+ const parentConfig = await this.fetchConfig(source, this.loadedConfigs.size);
280
+ parentConfigs.push(parentConfig);
281
+ }
282
+ const { ConfigMerger: ConfigMerger2 } = await import("./config-merger-PX3WIT57.mjs");
283
+ const merger = new ConfigMerger2();
284
+ let mergedParents = {};
285
+ for (const parentConfig of parentConfigs) {
286
+ mergedParents = merger.merge(mergedParents, parentConfig);
287
+ }
288
+ return merger.merge(mergedParents, configWithoutExtends);
289
+ }
290
+ /**
291
+ * Find project root directory (for security validation)
292
+ */
293
+ findProjectRoot() {
294
+ try {
295
+ const { execSync } = __require("child_process");
296
+ const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
297
+ if (gitRoot) return gitRoot;
298
+ } catch {
299
+ }
300
+ const packageRoot = this.findPackageRoot();
301
+ if (packageRoot) return packageRoot;
302
+ return process.cwd();
303
+ }
304
+ /**
305
+ * Validate remote URL against allowlist
306
+ */
307
+ validateRemoteURL(url) {
308
+ const allowedPatterns = this.options.allowedRemotePatterns || [];
309
+ if (allowedPatterns.length === 0) {
310
+ return;
311
+ }
312
+ const isAllowed = allowedPatterns.some((pattern) => url.startsWith(pattern));
313
+ if (!isAllowed) {
314
+ throw new Error(
315
+ `Security error: URL ${url} is not in the allowed list. Allowed patterns: ${allowedPatterns.join(", ")}`
316
+ );
317
+ }
318
+ }
319
+ /**
320
+ * Validate local path against traversal attacks
321
+ */
322
+ validateLocalPath(resolvedPath) {
323
+ const projectRoot = this.options.projectRoot || process.cwd();
324
+ const normalizedPath = path.normalize(resolvedPath);
325
+ const normalizedRoot = path.normalize(projectRoot);
326
+ if (!normalizedPath.startsWith(normalizedRoot)) {
327
+ throw new Error(
328
+ `Security error: Path traversal detected. Cannot access files outside project root: ${projectRoot}`
329
+ );
330
+ }
331
+ const sensitivePatterns = [
332
+ "/etc/passwd",
333
+ "/etc/shadow",
334
+ "/.ssh/",
335
+ "/.aws/",
336
+ "/.env",
337
+ "/private/"
338
+ ];
339
+ const lowerPath = normalizedPath.toLowerCase();
340
+ for (const pattern of sensitivePatterns) {
341
+ if (lowerPath.includes(pattern)) {
342
+ throw new Error(`Security error: Cannot access potentially sensitive file: ${pattern}`);
343
+ }
344
+ }
345
+ }
346
+ /**
347
+ * Find package root directory
348
+ */
349
+ findPackageRoot() {
350
+ let currentDir = __dirname;
351
+ const root = path.parse(currentDir).root;
352
+ while (currentDir !== root) {
353
+ const packageJsonPath = path.join(currentDir, "package.json");
354
+ if (fs.existsSync(packageJsonPath)) {
355
+ try {
356
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
357
+ if (packageJson.name === "@probelabs/visor") {
358
+ return currentDir;
359
+ }
360
+ } catch {
361
+ }
362
+ }
363
+ currentDir = path.dirname(currentDir);
364
+ }
365
+ return null;
366
+ }
367
+ /**
368
+ * Clear the configuration cache
369
+ */
370
+ clearCache() {
371
+ this.cache.clear();
372
+ }
373
+ /**
374
+ * Reset the loaded configs tracking (for testing)
375
+ */
376
+ reset() {
377
+ this.loadedConfigs.clear();
378
+ this.clearCache();
379
+ }
380
+ /**
381
+ * Normalize 'checks' and 'steps' keys for backward compatibility
382
+ * Ensures both keys are present and contain the same data
383
+ */
384
+ normalizeStepsAndChecks(config) {
385
+ if (config.steps && config.checks) {
386
+ const merged = { ...config.checks, ...config.steps };
387
+ config.checks = merged;
388
+ config.steps = merged;
389
+ } else if (config.steps && !config.checks) {
390
+ config.checks = config.steps;
391
+ } else if (config.checks && !config.steps) {
392
+ config.steps = config.checks;
393
+ }
394
+ return config;
395
+ }
396
+ };
397
+ }
398
+ });
399
+
400
+ // src/generated/config-schema.ts
401
+ var config_schema_exports = {};
402
+ __export(config_schema_exports, {
403
+ configSchema: () => configSchema,
404
+ default: () => config_schema_default
405
+ });
406
+ var configSchema, config_schema_default;
407
+ var init_config_schema = __esm({
408
+ "src/generated/config-schema.ts"() {
409
+ "use strict";
410
+ configSchema = {
411
+ $schema: "http://json-schema.org/draft-07/schema#",
412
+ $ref: "#/definitions/VisorConfigSchema",
413
+ definitions: {
414
+ VisorConfigSchema: {
415
+ type: "object",
416
+ additionalProperties: false,
417
+ properties: {
418
+ hooks: {
419
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E"
420
+ },
421
+ version: {
422
+ type: "string",
423
+ description: "Configuration version"
424
+ },
425
+ extends: {
426
+ anyOf: [
427
+ {
428
+ type: "string"
429
+ },
430
+ {
431
+ type: "array",
432
+ items: {
433
+ type: "string"
434
+ }
435
+ }
436
+ ],
437
+ description: 'Extends from other configurations - can be file path, HTTP(S) URL, or "default"'
438
+ },
439
+ include: {
440
+ anyOf: [
441
+ {
442
+ type: "string"
443
+ },
444
+ {
445
+ type: "array",
446
+ items: {
447
+ type: "string"
448
+ }
449
+ }
450
+ ],
451
+ description: "Alias for extends - include from other configurations (backward compatibility)"
452
+ },
453
+ tools: {
454
+ $ref: "#/definitions/Record%3Cstring%2CCustomToolDefinition%3E",
455
+ description: "Custom tool definitions that can be used in MCP blocks"
456
+ },
457
+ imports: {
458
+ type: "array",
459
+ items: {
460
+ type: "string"
461
+ },
462
+ description: "Import workflow definitions from external files or URLs"
463
+ },
464
+ inputs: {
465
+ type: "array",
466
+ items: {
467
+ $ref: "#/definitions/WorkflowInput"
468
+ },
469
+ description: "Workflow inputs (for standalone reusable workflows)"
470
+ },
471
+ outputs: {
472
+ type: "array",
473
+ items: {
474
+ $ref: "#/definitions/WorkflowOutput"
475
+ },
476
+ description: "Workflow outputs (for standalone reusable workflows)"
477
+ },
478
+ steps: {
479
+ $ref: "#/definitions/Record%3Cstring%2CCheckConfig%3E",
480
+ description: "Step configurations (recommended)"
481
+ },
482
+ checks: {
483
+ $ref: "#/definitions/Record%3Cstring%2CCheckConfig%3E",
484
+ description: "Check configurations (legacy, use 'steps' instead) - always populated after normalization"
485
+ },
486
+ output: {
487
+ $ref: "#/definitions/OutputConfig",
488
+ description: "Output configuration (optional - defaults provided)"
489
+ },
490
+ http_server: {
491
+ $ref: "#/definitions/HttpServerConfig",
492
+ description: "HTTP server configuration for receiving webhooks"
493
+ },
494
+ memory: {
495
+ $ref: "#/definitions/MemoryConfig",
496
+ description: "Memory storage configuration"
497
+ },
498
+ env: {
499
+ $ref: "#/definitions/EnvConfig",
500
+ description: "Global environment variables"
501
+ },
502
+ ai_model: {
503
+ type: "string",
504
+ description: "Global AI model setting"
505
+ },
506
+ ai_provider: {
507
+ type: "string",
508
+ description: "Global AI provider setting"
509
+ },
510
+ ai_mcp_servers: {
511
+ $ref: "#/definitions/Record%3Cstring%2CMcpServerConfig%3E",
512
+ description: "Global MCP servers configuration for AI checks"
513
+ },
514
+ max_parallelism: {
515
+ type: "number",
516
+ description: "Maximum number of checks to run in parallel (default: 3)"
517
+ },
518
+ fail_fast: {
519
+ type: "boolean",
520
+ description: "Stop execution when any check fails (default: false)"
521
+ },
522
+ fail_if: {
523
+ type: "string",
524
+ description: "Simple global fail condition - fails if expression evaluates to true"
525
+ },
526
+ failure_conditions: {
527
+ $ref: "#/definitions/FailureConditions",
528
+ description: "Global failure conditions - optional (deprecated, use fail_if)"
529
+ },
530
+ tag_filter: {
531
+ $ref: "#/definitions/TagFilter",
532
+ description: "Tag filter for selective check execution"
533
+ },
534
+ routing: {
535
+ $ref: "#/definitions/RoutingDefaults",
536
+ description: "Optional routing defaults for retry/goto/run policies"
537
+ },
538
+ limits: {
539
+ $ref: "#/definitions/LimitsConfig",
540
+ description: "Global execution limits"
541
+ },
542
+ frontends: {
543
+ type: "array",
544
+ items: {
545
+ type: "object",
546
+ properties: {
547
+ name: {
548
+ type: "string",
549
+ description: "Frontend name, e.g., 'ndjson-sink', 'github'"
550
+ },
551
+ config: {
552
+ description: "Frontend-specific configuration"
553
+ }
554
+ },
555
+ required: ["name"],
556
+ additionalProperties: false
557
+ },
558
+ description: "Optional integrations: event-driven frontends (e.g., ndjson-sink, github)"
559
+ },
560
+ workspace: {
561
+ $ref: "#/definitions/WorkspaceConfig",
562
+ description: "Workspace isolation configuration for sandboxed execution"
563
+ },
564
+ slack: {
565
+ $ref: "#/definitions/SlackConfig",
566
+ description: "Slack configuration"
567
+ }
568
+ },
569
+ required: ["version"],
570
+ patternProperties: {
571
+ "^x-": {}
572
+ }
573
+ },
574
+ "Record<string,unknown>": {
575
+ type: "object",
576
+ additionalProperties: {}
577
+ },
578
+ "Record<string,CustomToolDefinition>": {
579
+ type: "object",
580
+ additionalProperties: {
581
+ $ref: "#/definitions/CustomToolDefinition"
582
+ }
583
+ },
584
+ CustomToolDefinition: {
585
+ type: "object",
586
+ properties: {
587
+ name: {
588
+ type: "string",
589
+ description: "Tool name - used to reference the tool in MCP blocks"
590
+ },
591
+ description: {
592
+ type: "string",
593
+ description: "Description of what the tool does"
594
+ },
595
+ inputSchema: {
596
+ type: "object",
597
+ properties: {
598
+ type: {
599
+ type: "string",
600
+ const: "object"
601
+ },
602
+ properties: {
603
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E"
604
+ },
605
+ required: {
606
+ type: "array",
607
+ items: {
608
+ type: "string"
609
+ }
610
+ },
611
+ additionalProperties: {
612
+ type: "boolean"
613
+ }
614
+ },
615
+ required: ["type"],
616
+ additionalProperties: false,
617
+ description: "Input schema for the tool (JSON Schema format)",
618
+ patternProperties: {
619
+ "^x-": {}
620
+ }
621
+ },
622
+ exec: {
623
+ type: "string",
624
+ description: "Command to execute - supports Liquid template"
625
+ },
626
+ stdin: {
627
+ type: "string",
628
+ description: "Optional stdin input - supports Liquid template"
629
+ },
630
+ transform: {
631
+ type: "string",
632
+ description: "Transform the raw output - supports Liquid template"
633
+ },
634
+ transform_js: {
635
+ type: "string",
636
+ description: "Transform the output using JavaScript - alternative to transform"
637
+ },
638
+ cwd: {
639
+ type: "string",
640
+ description: "Working directory for command execution"
641
+ },
642
+ env: {
643
+ $ref: "#/definitions/Record%3Cstring%2Cstring%3E",
644
+ description: "Environment variables for the command"
645
+ },
646
+ timeout: {
647
+ type: "number",
648
+ description: "Timeout in milliseconds"
649
+ },
650
+ parseJson: {
651
+ type: "boolean",
652
+ description: "Whether to parse output as JSON automatically"
653
+ },
654
+ outputSchema: {
655
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
656
+ description: "Expected output schema for validation"
657
+ }
658
+ },
659
+ required: ["name", "exec"],
660
+ additionalProperties: false,
661
+ description: "Custom tool definition for use in MCP blocks",
662
+ patternProperties: {
663
+ "^x-": {}
664
+ }
665
+ },
666
+ "Record<string,string>": {
667
+ type: "object",
668
+ additionalProperties: {
669
+ type: "string"
670
+ }
671
+ },
672
+ WorkflowInput: {
673
+ type: "object",
674
+ properties: {
675
+ name: {
676
+ type: "string",
677
+ description: "Input parameter name"
678
+ },
679
+ schema: {
680
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
681
+ description: "JSON Schema for the input"
682
+ },
683
+ required: {
684
+ type: "boolean",
685
+ description: "Whether this input is required"
686
+ },
687
+ default: {
688
+ description: "Default value if not provided"
689
+ },
690
+ description: {
691
+ type: "string",
692
+ description: "Human-readable description"
693
+ }
694
+ },
695
+ required: ["name"],
696
+ additionalProperties: false,
697
+ description: "Workflow input definition for standalone reusable workflows",
698
+ patternProperties: {
699
+ "^x-": {}
700
+ }
701
+ },
702
+ WorkflowOutput: {
703
+ type: "object",
704
+ properties: {
705
+ name: {
706
+ type: "string",
707
+ description: "Output name"
708
+ },
709
+ description: {
710
+ type: "string",
711
+ description: "Human-readable description"
712
+ },
713
+ value: {
714
+ type: "string",
715
+ description: "Value using Liquid template syntax (references step outputs)"
716
+ },
717
+ value_js: {
718
+ type: "string",
719
+ description: "Value using JavaScript expression (alternative to value)"
720
+ }
721
+ },
722
+ required: ["name"],
723
+ additionalProperties: false,
724
+ description: "Workflow output definition for standalone reusable workflows",
725
+ patternProperties: {
726
+ "^x-": {}
727
+ }
728
+ },
729
+ "Record<string,CheckConfig>": {
730
+ type: "object",
731
+ additionalProperties: {
732
+ $ref: "#/definitions/CheckConfig"
733
+ }
734
+ },
735
+ CheckConfig: {
736
+ type: "object",
737
+ properties: {
738
+ type: {
739
+ $ref: "#/definitions/ConfigCheckType",
740
+ description: "Type of check to perform (defaults to 'ai' if not specified)"
741
+ },
742
+ prompt: {
743
+ type: "string",
744
+ description: "AI prompt for the check - can be inline string or file path (auto-detected) - required for AI checks"
745
+ },
746
+ appendPrompt: {
747
+ type: "string",
748
+ description: "Additional prompt to append when extending configurations - merged with parent prompt"
749
+ },
750
+ exec: {
751
+ type: "string",
752
+ description: "Command execution with Liquid template support - required for command checks"
753
+ },
754
+ stdin: {
755
+ type: "string",
756
+ description: "Stdin input for tools with Liquid template support - optional for tool checks"
757
+ },
758
+ url: {
759
+ type: "string",
760
+ description: "HTTP URL - required for http output checks"
761
+ },
762
+ body: {
763
+ type: "string",
764
+ description: "HTTP body template (Liquid) - required for http output checks"
765
+ },
766
+ method: {
767
+ type: "string",
768
+ description: "HTTP method (defaults to POST)"
769
+ },
770
+ headers: {
771
+ $ref: "#/definitions/Record%3Cstring%2Cstring%3E",
772
+ description: "HTTP headers"
773
+ },
774
+ endpoint: {
775
+ type: "string",
776
+ description: "HTTP endpoint path - required for http_input checks"
777
+ },
778
+ transform: {
779
+ type: "string",
780
+ description: "Transform template for http_input data (Liquid) - optional"
781
+ },
782
+ transform_js: {
783
+ type: "string",
784
+ description: "Transform using JavaScript expressions (evaluated in secure sandbox) - optional"
785
+ },
786
+ content: {
787
+ type: "string",
788
+ description: "Script content to execute for script checks"
789
+ },
790
+ schedule: {
791
+ type: "string",
792
+ description: 'Cron schedule expression (e.g., "0 2 * * *") - optional for any check type'
793
+ },
794
+ focus: {
795
+ type: "string",
796
+ description: "Focus area for the check (security/performance/style/architecture/all) - optional"
797
+ },
798
+ command: {
799
+ type: "string",
800
+ description: 'Command that triggers this check (e.g., "review", "security-scan") - optional'
801
+ },
802
+ on: {
803
+ type: "array",
804
+ items: {
805
+ $ref: "#/definitions/EventTrigger"
806
+ },
807
+ description: "Events that trigger this check (defaults to ['manual'] if not specified)"
808
+ },
809
+ triggers: {
810
+ type: "array",
811
+ items: {
812
+ type: "string"
813
+ },
814
+ description: "File patterns that trigger this check (optional)"
815
+ },
816
+ ai: {
817
+ $ref: "#/definitions/AIProviderConfig",
818
+ description: "AI provider configuration (optional)"
819
+ },
820
+ ai_model: {
821
+ type: "string",
822
+ description: "AI model to use for this check - overrides global setting"
823
+ },
824
+ ai_provider: {
825
+ type: "string",
826
+ description: "AI provider to use for this check - overrides global setting"
827
+ },
828
+ ai_persona: {
829
+ type: "string",
830
+ description: "Optional persona hint, prepended to the prompt as 'Persona: <value>'"
831
+ },
832
+ ai_prompt_type: {
833
+ type: "string",
834
+ description: "Probe promptType for this check (underscore style)"
835
+ },
836
+ ai_system_prompt: {
837
+ type: "string",
838
+ description: "System prompt for this check (underscore style)"
839
+ },
840
+ ai_custom_prompt: {
841
+ type: "string",
842
+ description: "Legacy customPrompt (underscore style) \u2014 deprecated, use ai_system_prompt"
843
+ },
844
+ ai_mcp_servers: {
845
+ $ref: "#/definitions/Record%3Cstring%2CMcpServerConfig%3E",
846
+ description: "MCP servers for this AI check - overrides global setting"
847
+ },
848
+ ai_custom_tools: {
849
+ type: "array",
850
+ items: {
851
+ type: "string"
852
+ },
853
+ description: "List of custom tool names to expose to this AI check via ephemeral SSE MCP server"
854
+ },
855
+ claude_code: {
856
+ $ref: "#/definitions/ClaudeCodeConfig",
857
+ description: "Claude Code configuration (for claude-code type checks)"
858
+ },
859
+ env: {
860
+ $ref: "#/definitions/EnvConfig",
861
+ description: "Environment variables for this check"
862
+ },
863
+ timeout: {
864
+ type: "number",
865
+ description: "Timeout in milliseconds for command execution (default: 60000, i.e., 60 seconds)"
866
+ },
867
+ depends_on: {
868
+ anyOf: [
869
+ {
870
+ type: "string"
871
+ },
872
+ {
873
+ type: "array",
874
+ items: {
875
+ type: "string"
876
+ }
877
+ }
878
+ ],
879
+ description: "Check IDs that this check depends on (optional). Accepts single string or array."
880
+ },
881
+ group: {
882
+ type: "string",
883
+ description: 'Group name for comment separation (e.g., "code-review", "pr-overview") - optional'
884
+ },
885
+ schema: {
886
+ anyOf: [
887
+ {
888
+ type: "string"
889
+ },
890
+ {
891
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E"
892
+ }
893
+ ],
894
+ description: 'Schema type for template rendering (e.g., "code-review", "markdown") or inline JSON schema object - optional'
895
+ },
896
+ output_schema: {
897
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
898
+ description: "Optional JSON Schema to validate the produced output. If omitted and `schema` is an object, the engine will treat that object as the output_schema for validation purposes while still using string schemas (e.g., 'code-review') for template selection."
899
+ },
900
+ template: {
901
+ $ref: "#/definitions/CustomTemplateConfig",
902
+ description: "Custom template configuration - optional"
903
+ },
904
+ if: {
905
+ type: "string",
906
+ description: "Condition to determine if check should run - runs if expression evaluates to true"
907
+ },
908
+ reuse_ai_session: {
909
+ type: ["string", "boolean"],
910
+ description: "Check name to reuse AI session from, or true to use first dependency (only works with depends_on)"
911
+ },
912
+ session_mode: {
913
+ type: "string",
914
+ enum: ["clone", "append"],
915
+ description: "How to reuse AI session: 'clone' (default, copy history) or 'append' (share history)"
916
+ },
917
+ fail_if: {
918
+ type: "string",
919
+ description: "Simple fail condition - fails check if expression evaluates to true"
920
+ },
921
+ failure_conditions: {
922
+ $ref: "#/definitions/FailureConditions",
923
+ description: "Check-specific failure conditions - optional (deprecated, use fail_if)"
924
+ },
925
+ tags: {
926
+ type: "array",
927
+ items: {
928
+ type: "string"
929
+ },
930
+ description: 'Tags for categorizing and filtering checks (e.g., ["local", "fast", "security"])'
931
+ },
932
+ criticality: {
933
+ type: "string",
934
+ enum: ["external", "internal", "policy", "info"],
935
+ description: "Operational criticality of this step. Drives default safety policies (contracts, retries, loop budgets) at load time. Behavior can still be overridden explicitly per step via on_*, fail_if, assume/guarantee, etc.\n\n- 'external': interacts with external systems (side effects). Highest safety.\n- 'internal': modifies CI/config/state but not prod. High safety.\n- 'policy': organizational checks (linting, style, doc). Moderate safety.\n- 'info': informational checks. Lowest safety."
936
+ },
937
+ continue_on_failure: {
938
+ type: "boolean",
939
+ description: "Allow dependents to run even if this step fails. Defaults to false (dependents are gated when this step fails). Similar to GitHub Actions' continue-on-error."
940
+ },
941
+ forEach: {
942
+ type: "boolean",
943
+ description: "Process output as array and run dependent checks for each item"
944
+ },
945
+ fanout: {
946
+ type: "string",
947
+ enum: ["map", "reduce"],
948
+ description: "Control scheduling behavior when this check is triggered via routing (run/goto) from a forEach scope.\n- 'map': schedule once per item (fan-out) using item scopes.\n- 'reduce': schedule a single run at the parent scope (aggregation). If unset, the current default is a single run (reduce) for backward compatibility."
949
+ },
950
+ reduce: {
951
+ type: "boolean",
952
+ description: "Alias for fanout: 'reduce'"
953
+ },
954
+ on_init: {
955
+ $ref: "#/definitions/OnInitConfig",
956
+ description: "Init routing configuration for this check (runs before execution/preprocessing)"
957
+ },
958
+ on_fail: {
959
+ $ref: "#/definitions/OnFailConfig",
960
+ description: "Failure routing configuration for this check (retry/goto/run)"
961
+ },
962
+ on_success: {
963
+ $ref: "#/definitions/OnSuccessConfig",
964
+ description: "Success routing configuration for this check (post-actions and optional goto)"
965
+ },
966
+ on_finish: {
967
+ $ref: "#/definitions/OnFinishConfig",
968
+ description: "Finish routing configuration for forEach checks (runs after ALL iterations complete)"
969
+ },
970
+ assume: {
971
+ anyOf: [
972
+ {
973
+ type: "string"
974
+ },
975
+ {
976
+ type: "array",
977
+ items: {
978
+ type: "string"
979
+ }
980
+ }
981
+ ],
982
+ description: "Preconditions that must hold before executing the check. If any expression evaluates to false, the check is skipped (skipReason='assume')."
983
+ },
984
+ guarantee: {
985
+ anyOf: [
986
+ {
987
+ type: "string"
988
+ },
989
+ {
990
+ type: "array",
991
+ items: {
992
+ type: "string"
993
+ }
994
+ }
995
+ ],
996
+ description: 'Postconditions that should hold after executing the check. Expressions are evaluated against the produced result/output; violations are recorded as error issues with ruleId "contract/guarantee_failed".'
997
+ },
998
+ max_runs: {
999
+ type: "number",
1000
+ description: "Hard cap on how many times this check may execute within a single engine run. Overrides global limits.max_runs_per_check. Set to 0 or negative to disable for this step."
1001
+ },
1002
+ message: {
1003
+ type: "string",
1004
+ description: "Message template for log checks"
1005
+ },
1006
+ level: {
1007
+ type: "string",
1008
+ enum: ["debug", "info", "warn", "error"],
1009
+ description: "Log level for log checks"
1010
+ },
1011
+ include_pr_context: {
1012
+ type: "boolean",
1013
+ description: "Include PR context in log output"
1014
+ },
1015
+ include_dependencies: {
1016
+ type: "boolean",
1017
+ description: "Include dependency summaries in log output"
1018
+ },
1019
+ include_metadata: {
1020
+ type: "boolean",
1021
+ description: "Include execution metadata in log output"
1022
+ },
1023
+ output_format: {
1024
+ type: "string",
1025
+ enum: ["json", "text"],
1026
+ description: "Output parsing hint for command provider (optional) When set to 'json', command stdout is expected to be JSON. When 'text', treat as plain text. Note: command provider attempts JSON parsing heuristically; this flag mainly suppresses schema warnings and may be used by providers to alter parsing behavior in the future."
1027
+ },
1028
+ operation: {
1029
+ type: "string",
1030
+ enum: ["get", "set", "append", "increment", "delete", "clear", "list"],
1031
+ description: "Memory operation to perform. Use `type: 'script'` for custom JavaScript."
1032
+ },
1033
+ key: {
1034
+ type: "string",
1035
+ description: "Key for memory operation"
1036
+ },
1037
+ value: {
1038
+ description: "Value for set/append operations"
1039
+ },
1040
+ value_js: {
1041
+ type: "string",
1042
+ description: "JavaScript expression to compute value dynamically"
1043
+ },
1044
+ namespace: {
1045
+ type: "string",
1046
+ description: "Override namespace for this check"
1047
+ },
1048
+ op: {
1049
+ type: "string",
1050
+ description: "GitHub operation to perform (e.g., 'labels.add', 'labels.remove', 'comment.create')"
1051
+ },
1052
+ values: {
1053
+ anyOf: [
1054
+ {
1055
+ type: "array",
1056
+ items: {
1057
+ type: "string"
1058
+ }
1059
+ },
1060
+ {
1061
+ type: "string"
1062
+ }
1063
+ ],
1064
+ description: "Values for GitHub operations (can be array or single value)"
1065
+ },
1066
+ transport: {
1067
+ type: "string",
1068
+ enum: ["stdio", "sse", "http"],
1069
+ description: "Transport type for MCP: stdio (default), sse (legacy), or http (streamable HTTP)"
1070
+ },
1071
+ methodArgs: {
1072
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
1073
+ description: "Arguments to pass to the MCP method (supports Liquid templates)"
1074
+ },
1075
+ argsTransform: {
1076
+ type: "string",
1077
+ description: "Transform template for method arguments (Liquid)"
1078
+ },
1079
+ sessionId: {
1080
+ type: "string",
1081
+ description: "Session ID for HTTP transport (optional, server may generate one)"
1082
+ },
1083
+ command_args: {
1084
+ type: "array",
1085
+ items: {
1086
+ type: "string"
1087
+ },
1088
+ description: "Command arguments (for stdio transport in MCP checks)"
1089
+ },
1090
+ workingDirectory: {
1091
+ type: "string",
1092
+ description: "Working directory (for stdio transport in MCP checks)"
1093
+ },
1094
+ placeholder: {
1095
+ type: "string",
1096
+ description: "Placeholder text to show in input field"
1097
+ },
1098
+ allow_empty: {
1099
+ type: "boolean",
1100
+ description: "Allow empty input (default: false)"
1101
+ },
1102
+ multiline: {
1103
+ type: "boolean",
1104
+ description: "Support multiline input (default: false)"
1105
+ },
1106
+ default: {
1107
+ type: "string",
1108
+ description: "Default value if timeout occurs or empty input when allow_empty is true"
1109
+ },
1110
+ workflow: {
1111
+ type: "string",
1112
+ description: "Workflow ID or path to workflow file"
1113
+ },
1114
+ args: {
1115
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
1116
+ description: "Arguments/inputs for the workflow"
1117
+ },
1118
+ overrides: {
1119
+ $ref: "#/definitions/Record%3Cstring%2CPartial%3Cinterface-src_types_config.ts-11359-23582-src_types_config.ts-0-41281%3E%3E",
1120
+ description: "Override specific step configurations in the workflow"
1121
+ },
1122
+ output_mapping: {
1123
+ $ref: "#/definitions/Record%3Cstring%2Cstring%3E",
1124
+ description: "Map workflow outputs to check outputs"
1125
+ },
1126
+ workflow_inputs: {
1127
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
1128
+ description: "Alias for args - workflow inputs (backward compatibility)"
1129
+ },
1130
+ config: {
1131
+ type: "string",
1132
+ description: "Config file path - alternative to workflow ID (loads a Visor config file as workflow)"
1133
+ },
1134
+ workflow_overrides: {
1135
+ $ref: "#/definitions/Record%3Cstring%2CPartial%3Cinterface-src_types_config.ts-11359-23582-src_types_config.ts-0-41281%3E%3E",
1136
+ description: "Alias for overrides - workflow step overrides (backward compatibility)"
1137
+ },
1138
+ ref: {
1139
+ type: "string",
1140
+ description: "Git reference to checkout (branch, tag, commit SHA) - supports templates"
1141
+ },
1142
+ repository: {
1143
+ type: "string",
1144
+ description: "Repository URL or owner/repo format (defaults to current repository)"
1145
+ },
1146
+ token: {
1147
+ type: "string",
1148
+ description: "GitHub token for private repositories (defaults to GITHUB_TOKEN env)"
1149
+ },
1150
+ fetch_depth: {
1151
+ type: "number",
1152
+ description: "Number of commits to fetch (0 for full history, default: 1)"
1153
+ },
1154
+ fetch_tags: {
1155
+ type: "boolean",
1156
+ description: "Whether to fetch tags (default: false)"
1157
+ },
1158
+ submodules: {
1159
+ anyOf: [
1160
+ {
1161
+ type: "boolean"
1162
+ },
1163
+ {
1164
+ type: "string",
1165
+ const: "recursive"
1166
+ }
1167
+ ],
1168
+ description: "Checkout submodules: false, true, or 'recursive'"
1169
+ },
1170
+ working_directory: {
1171
+ type: "string",
1172
+ description: "Working directory for the checkout (defaults to temp directory)"
1173
+ },
1174
+ use_worktree: {
1175
+ type: "boolean",
1176
+ description: "Use git worktree for efficient parallel checkouts (default: true)"
1177
+ },
1178
+ clean: {
1179
+ type: "boolean",
1180
+ description: "Clean the working directory before checkout (default: true)"
1181
+ },
1182
+ sparse_checkout: {
1183
+ type: "array",
1184
+ items: {
1185
+ type: "string"
1186
+ },
1187
+ description: "Sparse checkout paths - only checkout specific directories/files"
1188
+ },
1189
+ lfs: {
1190
+ type: "boolean",
1191
+ description: "Enable Git LFS (Large File Storage)"
1192
+ },
1193
+ clone_timeout_ms: {
1194
+ type: "number",
1195
+ description: "Timeout in ms for cloning the bare repository (default: 300000 = 5 min)"
1196
+ },
1197
+ cleanup_on_failure: {
1198
+ type: "boolean",
1199
+ description: "Clean up worktree on failure (default: true)"
1200
+ },
1201
+ persist_worktree: {
1202
+ type: "boolean",
1203
+ description: "Keep worktree after workflow completion (default: false)"
1204
+ }
1205
+ },
1206
+ additionalProperties: false,
1207
+ description: "Configuration for a single check",
1208
+ patternProperties: {
1209
+ "^x-": {}
1210
+ }
1211
+ },
1212
+ ConfigCheckType: {
1213
+ type: "string",
1214
+ enum: [
1215
+ "ai",
1216
+ "command",
1217
+ "script",
1218
+ "http",
1219
+ "http_input",
1220
+ "http_client",
1221
+ "noop",
1222
+ "log",
1223
+ "memory",
1224
+ "github",
1225
+ "claude-code",
1226
+ "mcp",
1227
+ "human-input",
1228
+ "workflow",
1229
+ "git-checkout"
1230
+ ],
1231
+ description: "Valid check types in configuration"
1232
+ },
1233
+ EventTrigger: {
1234
+ type: "string",
1235
+ enum: [
1236
+ "pr_opened",
1237
+ "pr_updated",
1238
+ "pr_closed",
1239
+ "issue_opened",
1240
+ "issue_comment",
1241
+ "manual",
1242
+ "schedule",
1243
+ "webhook_received"
1244
+ ],
1245
+ description: "Valid event triggers for checks"
1246
+ },
1247
+ AIProviderConfig: {
1248
+ type: "object",
1249
+ properties: {
1250
+ provider: {
1251
+ type: "string",
1252
+ enum: ["google", "anthropic", "openai", "bedrock", "mock"],
1253
+ description: "AI provider to use"
1254
+ },
1255
+ model: {
1256
+ type: "string",
1257
+ description: "Model name to use"
1258
+ },
1259
+ apiKey: {
1260
+ type: "string",
1261
+ description: "API key (usually from environment variables)"
1262
+ },
1263
+ timeout: {
1264
+ type: "number",
1265
+ description: "Request timeout in milliseconds"
1266
+ },
1267
+ debug: {
1268
+ type: "boolean",
1269
+ description: "Enable debug mode"
1270
+ },
1271
+ prompt_type: {
1272
+ type: "string",
1273
+ description: "Probe promptType to use (e.g., engineer, code-review, architect)"
1274
+ },
1275
+ system_prompt: {
1276
+ type: "string",
1277
+ description: "System prompt (baseline preamble). Replaces legacy custom_prompt."
1278
+ },
1279
+ custom_prompt: {
1280
+ type: "string",
1281
+ description: "Probe customPrompt (baseline/system prompt) \u2014 deprecated, use system_prompt"
1282
+ },
1283
+ skip_code_context: {
1284
+ type: "boolean",
1285
+ description: "Skip adding code context (diffs, files, PR info) to the prompt"
1286
+ },
1287
+ skip_slack_context: {
1288
+ type: "boolean",
1289
+ description: "Skip adding Slack conversation context to the prompt (when running under Slack)"
1290
+ },
1291
+ skip_transport_context: {
1292
+ type: "boolean",
1293
+ description: "Skip adding transport-specific context (e.g., GitHub PR/issue XML, Slack conversation XML) to the prompt. When true, this behaves like setting both skip_code_context and skip_slack_context to true, unless those are explicitly overridden."
1294
+ },
1295
+ mcpServers: {
1296
+ $ref: "#/definitions/Record%3Cstring%2CMcpServerConfig%3E",
1297
+ description: "MCP servers configuration"
1298
+ },
1299
+ enableDelegate: {
1300
+ type: "boolean",
1301
+ description: "Enable the delegate tool for task distribution to subagents"
1302
+ },
1303
+ retry: {
1304
+ $ref: "#/definitions/AIRetryConfig",
1305
+ description: "Retry configuration for this provider"
1306
+ },
1307
+ fallback: {
1308
+ $ref: "#/definitions/AIFallbackConfig",
1309
+ description: "Fallback configuration for provider failures"
1310
+ },
1311
+ allowEdit: {
1312
+ type: "boolean",
1313
+ description: "Enable Edit and Create tools for file modification (disabled by default for security)"
1314
+ },
1315
+ allowedTools: {
1316
+ type: "array",
1317
+ items: {
1318
+ type: "string"
1319
+ },
1320
+ description: "Filter allowed tools - supports whitelist, exclusion (!prefix), or raw AI mode (empty array)"
1321
+ },
1322
+ disableTools: {
1323
+ type: "boolean",
1324
+ description: "Disable all tools for raw AI mode (alternative to allowedTools: [])"
1325
+ },
1326
+ allowBash: {
1327
+ type: "boolean",
1328
+ description: "Enable bash command execution (shorthand for bashConfig.enabled)"
1329
+ },
1330
+ bashConfig: {
1331
+ $ref: "#/definitions/BashConfig",
1332
+ description: "Advanced bash command execution configuration"
1333
+ },
1334
+ completion_prompt: {
1335
+ type: "string",
1336
+ description: "Completion prompt for post-completion validation/review (runs after attempt_completion)"
1337
+ }
1338
+ },
1339
+ additionalProperties: false,
1340
+ description: "AI provider configuration",
1341
+ patternProperties: {
1342
+ "^x-": {}
1343
+ }
1344
+ },
1345
+ "Record<string,McpServerConfig>": {
1346
+ type: "object",
1347
+ additionalProperties: {
1348
+ $ref: "#/definitions/McpServerConfig"
1349
+ }
1350
+ },
1351
+ McpServerConfig: {
1352
+ type: "object",
1353
+ properties: {
1354
+ command: {
1355
+ type: "string",
1356
+ description: "Command to execute for the MCP server"
1357
+ },
1358
+ args: {
1359
+ type: "array",
1360
+ items: {
1361
+ type: "string"
1362
+ },
1363
+ description: "Arguments to pass to the command"
1364
+ },
1365
+ env: {
1366
+ $ref: "#/definitions/Record%3Cstring%2Cstring%3E",
1367
+ description: "Environment variables for the MCP server"
1368
+ }
1369
+ },
1370
+ required: ["command"],
1371
+ additionalProperties: false,
1372
+ description: "MCP Server configuration",
1373
+ patternProperties: {
1374
+ "^x-": {}
1375
+ }
1376
+ },
1377
+ AIRetryConfig: {
1378
+ type: "object",
1379
+ properties: {
1380
+ maxRetries: {
1381
+ type: "number",
1382
+ description: "Maximum retry attempts (0-50)"
1383
+ },
1384
+ initialDelay: {
1385
+ type: "number",
1386
+ description: "Initial delay in milliseconds (0-60000)"
1387
+ },
1388
+ maxDelay: {
1389
+ type: "number",
1390
+ description: "Maximum delay cap in milliseconds (0-300000)"
1391
+ },
1392
+ backoffFactor: {
1393
+ type: "number",
1394
+ description: "Exponential backoff multiplier (1-10)"
1395
+ },
1396
+ retryableErrors: {
1397
+ type: "array",
1398
+ items: {
1399
+ type: "string"
1400
+ },
1401
+ description: "Custom error patterns to retry on"
1402
+ }
1403
+ },
1404
+ additionalProperties: false,
1405
+ description: "Retry configuration for AI provider calls",
1406
+ patternProperties: {
1407
+ "^x-": {}
1408
+ }
1409
+ },
1410
+ AIFallbackConfig: {
1411
+ type: "object",
1412
+ properties: {
1413
+ strategy: {
1414
+ type: "string",
1415
+ enum: ["same-model", "same-provider", "any", "custom"],
1416
+ description: "Fallback strategy: 'same-model', 'same-provider', 'any', or 'custom'"
1417
+ },
1418
+ providers: {
1419
+ type: "array",
1420
+ items: {
1421
+ $ref: "#/definitions/AIFallbackProviderConfig"
1422
+ },
1423
+ description: "Array of fallback provider configurations"
1424
+ },
1425
+ maxTotalAttempts: {
1426
+ type: "number",
1427
+ description: "Maximum total attempts across all providers"
1428
+ },
1429
+ auto: {
1430
+ type: "boolean",
1431
+ description: "Enable automatic fallback using available environment variables"
1432
+ }
1433
+ },
1434
+ additionalProperties: false,
1435
+ description: "Fallback configuration for AI providers",
1436
+ patternProperties: {
1437
+ "^x-": {}
1438
+ }
1439
+ },
1440
+ AIFallbackProviderConfig: {
1441
+ type: "object",
1442
+ properties: {
1443
+ provider: {
1444
+ type: "string",
1445
+ enum: ["google", "anthropic", "openai", "bedrock"],
1446
+ description: "AI provider to use"
1447
+ },
1448
+ model: {
1449
+ type: "string",
1450
+ description: "Model name to use"
1451
+ },
1452
+ apiKey: {
1453
+ type: "string",
1454
+ description: "API key for this provider"
1455
+ },
1456
+ maxRetries: {
1457
+ type: "number",
1458
+ description: "Per-provider retry override"
1459
+ },
1460
+ region: {
1461
+ type: "string",
1462
+ description: "AWS region (for Bedrock)"
1463
+ },
1464
+ accessKeyId: {
1465
+ type: "string",
1466
+ description: "AWS access key ID (for Bedrock)"
1467
+ },
1468
+ secretAccessKey: {
1469
+ type: "string",
1470
+ description: "AWS secret access key (for Bedrock)"
1471
+ }
1472
+ },
1473
+ required: ["provider", "model"],
1474
+ additionalProperties: false,
1475
+ description: "Fallback provider configuration",
1476
+ patternProperties: {
1477
+ "^x-": {}
1478
+ }
1479
+ },
1480
+ BashConfig: {
1481
+ type: "object",
1482
+ properties: {
1483
+ allow: {
1484
+ type: "array",
1485
+ items: {
1486
+ type: "string"
1487
+ },
1488
+ description: "Array of permitted command patterns (e.g., ['ls', 'git status'])"
1489
+ },
1490
+ deny: {
1491
+ type: "array",
1492
+ items: {
1493
+ type: "string"
1494
+ },
1495
+ description: "Array of blocked command patterns (e.g., ['rm -rf', 'sudo'])"
1496
+ },
1497
+ noDefaultAllow: {
1498
+ type: "boolean",
1499
+ description: "Disable default safe command list (use with caution)"
1500
+ },
1501
+ noDefaultDeny: {
1502
+ type: "boolean",
1503
+ description: "Disable default dangerous command blocklist (use with extreme caution)"
1504
+ },
1505
+ timeout: {
1506
+ type: "number",
1507
+ description: "Execution timeout in milliseconds"
1508
+ },
1509
+ workingDirectory: {
1510
+ type: "string",
1511
+ description: "Default working directory for command execution"
1512
+ }
1513
+ },
1514
+ additionalProperties: false,
1515
+ description: "Bash command execution configuration for ProbeAgent Note: Use 'allowBash: true' in AIProviderConfig to enable bash execution",
1516
+ patternProperties: {
1517
+ "^x-": {}
1518
+ }
1519
+ },
1520
+ ClaudeCodeConfig: {
1521
+ type: "object",
1522
+ properties: {
1523
+ allowedTools: {
1524
+ type: "array",
1525
+ items: {
1526
+ type: "string"
1527
+ },
1528
+ description: "List of allowed tools for Claude Code to use"
1529
+ },
1530
+ maxTurns: {
1531
+ type: "number",
1532
+ description: "Maximum number of turns in conversation"
1533
+ },
1534
+ systemPrompt: {
1535
+ type: "string",
1536
+ description: "System prompt for Claude Code"
1537
+ },
1538
+ mcpServers: {
1539
+ $ref: "#/definitions/Record%3Cstring%2CMcpServerConfig%3E",
1540
+ description: "MCP servers configuration"
1541
+ },
1542
+ subagent: {
1543
+ type: "string",
1544
+ description: "Path to subagent script"
1545
+ },
1546
+ enableDelegate: {
1547
+ type: "boolean",
1548
+ description: "Enable the delegate tool for task distribution to subagents"
1549
+ },
1550
+ hooks: {
1551
+ type: "object",
1552
+ properties: {
1553
+ onStart: {
1554
+ type: "string",
1555
+ description: "Called when check starts"
1556
+ },
1557
+ onEnd: {
1558
+ type: "string",
1559
+ description: "Called when check ends"
1560
+ },
1561
+ onError: {
1562
+ type: "string",
1563
+ description: "Called when check encounters an error"
1564
+ }
1565
+ },
1566
+ additionalProperties: false,
1567
+ description: "Event hooks for lifecycle management",
1568
+ patternProperties: {
1569
+ "^x-": {}
1570
+ }
1571
+ }
1572
+ },
1573
+ additionalProperties: false,
1574
+ description: "Claude Code configuration",
1575
+ patternProperties: {
1576
+ "^x-": {}
1577
+ }
1578
+ },
1579
+ EnvConfig: {
1580
+ type: "object",
1581
+ additionalProperties: {
1582
+ type: ["string", "number", "boolean"]
1583
+ },
1584
+ description: "Environment variable reference configuration"
1585
+ },
1586
+ CustomTemplateConfig: {
1587
+ type: "object",
1588
+ properties: {
1589
+ file: {
1590
+ type: "string",
1591
+ description: "Path to custom template file (relative to config file or absolute)"
1592
+ },
1593
+ content: {
1594
+ type: "string",
1595
+ description: "Raw template content as string"
1596
+ }
1597
+ },
1598
+ additionalProperties: false,
1599
+ description: "Custom template configuration",
1600
+ patternProperties: {
1601
+ "^x-": {}
1602
+ }
1603
+ },
1604
+ FailureConditions: {
1605
+ type: "object",
1606
+ additionalProperties: {
1607
+ $ref: "#/definitions/FailureCondition"
1608
+ },
1609
+ description: "Collection of failure conditions"
1610
+ },
1611
+ FailureCondition: {
1612
+ anyOf: [
1613
+ {
1614
+ $ref: "#/definitions/SimpleFailureCondition"
1615
+ },
1616
+ {
1617
+ $ref: "#/definitions/ComplexFailureCondition"
1618
+ }
1619
+ ],
1620
+ description: "Failure condition - can be a simple expression string or complex object"
1621
+ },
1622
+ SimpleFailureCondition: {
1623
+ type: "string",
1624
+ description: "Simple failure condition - just an expression string"
1625
+ },
1626
+ ComplexFailureCondition: {
1627
+ type: "object",
1628
+ properties: {
1629
+ condition: {
1630
+ type: "string",
1631
+ description: "Expression to evaluate using Function Constructor"
1632
+ },
1633
+ message: {
1634
+ type: "string",
1635
+ description: "Human-readable message when condition is met"
1636
+ },
1637
+ severity: {
1638
+ $ref: "#/definitions/FailureConditionSeverity",
1639
+ description: "Severity level of the failure"
1640
+ },
1641
+ halt_execution: {
1642
+ type: "boolean",
1643
+ description: "Whether this condition should halt execution"
1644
+ }
1645
+ },
1646
+ required: ["condition"],
1647
+ additionalProperties: false,
1648
+ description: "Complex failure condition with additional metadata",
1649
+ patternProperties: {
1650
+ "^x-": {}
1651
+ }
1652
+ },
1653
+ FailureConditionSeverity: {
1654
+ type: "string",
1655
+ enum: ["error", "warning", "info"],
1656
+ description: "Failure condition severity levels"
1657
+ },
1658
+ OnInitConfig: {
1659
+ type: "object",
1660
+ properties: {
1661
+ run: {
1662
+ type: "array",
1663
+ items: {
1664
+ $ref: "#/definitions/OnInitRunItem"
1665
+ },
1666
+ description: "Items to run before this check executes"
1667
+ },
1668
+ run_js: {
1669
+ type: "string",
1670
+ description: "Dynamic init items: JS expression returning OnInitRunItem[]"
1671
+ },
1672
+ transitions: {
1673
+ type: "array",
1674
+ items: {
1675
+ $ref: "#/definitions/TransitionRule"
1676
+ },
1677
+ description: "Declarative transitions (optional, for advanced use cases)"
1678
+ }
1679
+ },
1680
+ additionalProperties: false,
1681
+ description: "Init routing configuration per check Runs BEFORE the check executes (preprocessing/setup)",
1682
+ patternProperties: {
1683
+ "^x-": {}
1684
+ }
1685
+ },
1686
+ OnInitRunItem: {
1687
+ anyOf: [
1688
+ {
1689
+ $ref: "#/definitions/OnInitToolInvocation"
1690
+ },
1691
+ {
1692
+ $ref: "#/definitions/OnInitStepInvocation"
1693
+ },
1694
+ {
1695
+ $ref: "#/definitions/OnInitWorkflowInvocation"
1696
+ },
1697
+ {
1698
+ type: "string"
1699
+ }
1700
+ ],
1701
+ description: "Unified on_init run item - can be tool, step, workflow, or plain string"
1702
+ },
1703
+ OnInitToolInvocation: {
1704
+ type: "object",
1705
+ properties: {
1706
+ tool: {
1707
+ type: "string",
1708
+ description: "Tool name (must exist in tools: section)"
1709
+ },
1710
+ with: {
1711
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
1712
+ description: "Arguments to pass to the tool (Liquid templates supported)"
1713
+ },
1714
+ as: {
1715
+ type: "string",
1716
+ description: "Custom output name (defaults to tool name)"
1717
+ }
1718
+ },
1719
+ required: ["tool"],
1720
+ additionalProperties: false,
1721
+ description: "Invoke a custom tool (from tools: section)",
1722
+ patternProperties: {
1723
+ "^x-": {}
1724
+ }
1725
+ },
1726
+ OnInitStepInvocation: {
1727
+ type: "object",
1728
+ properties: {
1729
+ step: {
1730
+ type: "string",
1731
+ description: "Step name (must exist in steps: section)"
1732
+ },
1733
+ with: {
1734
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
1735
+ description: "Arguments to pass to the step (Liquid templates supported)"
1736
+ },
1737
+ as: {
1738
+ type: "string",
1739
+ description: "Custom output name (defaults to step name)"
1740
+ }
1741
+ },
1742
+ required: ["step"],
1743
+ additionalProperties: false,
1744
+ description: "Invoke a helper step (regular check)",
1745
+ patternProperties: {
1746
+ "^x-": {}
1747
+ }
1748
+ },
1749
+ OnInitWorkflowInvocation: {
1750
+ type: "object",
1751
+ properties: {
1752
+ workflow: {
1753
+ type: "string",
1754
+ description: "Workflow ID or path"
1755
+ },
1756
+ with: {
1757
+ $ref: "#/definitions/Record%3Cstring%2Cunknown%3E",
1758
+ description: "Workflow inputs (Liquid templates supported)"
1759
+ },
1760
+ as: {
1761
+ type: "string",
1762
+ description: "Custom output name (defaults to workflow name)"
1763
+ },
1764
+ overrides: {
1765
+ $ref: "#/definitions/Record%3Cstring%2CPartial%3Cinterface-src_types_config.ts-11359-23582-src_types_config.ts-0-41281%3E%3E",
1766
+ description: "Step overrides"
1767
+ },
1768
+ output_mapping: {
1769
+ $ref: "#/definitions/Record%3Cstring%2Cstring%3E",
1770
+ description: "Output mapping"
1771
+ }
1772
+ },
1773
+ required: ["workflow"],
1774
+ additionalProperties: false,
1775
+ description: "Invoke a reusable workflow",
1776
+ patternProperties: {
1777
+ "^x-": {}
1778
+ }
1779
+ },
1780
+ "Record<string,Partial<interface-src_types_config.ts-11359-23582-src_types_config.ts-0-41281>>": {
1781
+ type: "object",
1782
+ additionalProperties: {
1783
+ $ref: "#/definitions/Partial%3Cinterface-src_types_config.ts-11359-23582-src_types_config.ts-0-41281%3E"
1784
+ }
1785
+ },
1786
+ "Partial<interface-src_types_config.ts-11359-23582-src_types_config.ts-0-41281>": {
1787
+ type: "object",
1788
+ additionalProperties: false
1789
+ },
1790
+ TransitionRule: {
1791
+ type: "object",
1792
+ properties: {
1793
+ when: {
1794
+ type: "string",
1795
+ description: "JavaScript expression evaluated in the same sandbox as goto_js; truthy enables the rule."
1796
+ },
1797
+ to: {
1798
+ type: ["string", "null"],
1799
+ description: "Target step ID, or null to explicitly prevent goto."
1800
+ },
1801
+ goto_event: {
1802
+ $ref: "#/definitions/EventTrigger",
1803
+ description: "Optional event override when performing goto."
1804
+ }
1805
+ },
1806
+ required: ["when"],
1807
+ additionalProperties: false,
1808
+ description: "Declarative transition rule for on_* blocks.",
1809
+ patternProperties: {
1810
+ "^x-": {}
1811
+ }
1812
+ },
1813
+ OnFailConfig: {
1814
+ type: "object",
1815
+ properties: {
1816
+ retry: {
1817
+ $ref: "#/definitions/RetryPolicy",
1818
+ description: "Retry policy"
1819
+ },
1820
+ run: {
1821
+ type: "array",
1822
+ items: {
1823
+ type: "string"
1824
+ },
1825
+ description: "Remediation steps to run before reattempt"
1826
+ },
1827
+ goto: {
1828
+ type: "string",
1829
+ description: "Jump back to an ancestor step (by id)"
1830
+ },
1831
+ goto_event: {
1832
+ $ref: "#/definitions/EventTrigger",
1833
+ description: "Simulate a different event when performing goto (e.g., 'pr_updated')"
1834
+ },
1835
+ goto_js: {
1836
+ type: "string",
1837
+ description: "Dynamic goto: JS expression returning step id or null"
1838
+ },
1839
+ run_js: {
1840
+ type: "string",
1841
+ description: "Dynamic remediation list: JS expression returning string[]"
1842
+ },
1843
+ transitions: {
1844
+ type: "array",
1845
+ items: {
1846
+ $ref: "#/definitions/TransitionRule"
1847
+ },
1848
+ description: "Declarative transitions. Evaluated in order; first matching rule wins. If a rule's `to` is null, no goto occurs. When omitted or none match, the engine falls back to goto_js/goto for backward compatibility."
1849
+ }
1850
+ },
1851
+ additionalProperties: false,
1852
+ description: "Failure routing configuration per check",
1853
+ patternProperties: {
1854
+ "^x-": {}
1855
+ }
1856
+ },
1857
+ RetryPolicy: {
1858
+ type: "object",
1859
+ properties: {
1860
+ max: {
1861
+ type: "number",
1862
+ description: "Maximum retry attempts (excluding the first attempt)"
1863
+ },
1864
+ backoff: {
1865
+ $ref: "#/definitions/BackoffPolicy",
1866
+ description: "Backoff policy"
1867
+ }
1868
+ },
1869
+ additionalProperties: false,
1870
+ description: "Retry policy for a step",
1871
+ patternProperties: {
1872
+ "^x-": {}
1873
+ }
1874
+ },
1875
+ BackoffPolicy: {
1876
+ type: "object",
1877
+ properties: {
1878
+ mode: {
1879
+ type: "string",
1880
+ enum: ["fixed", "exponential"],
1881
+ description: "Backoff mode"
1882
+ },
1883
+ delay_ms: {
1884
+ type: "number",
1885
+ description: "Initial delay in milliseconds"
1886
+ }
1887
+ },
1888
+ additionalProperties: false,
1889
+ description: "Backoff policy for retries",
1890
+ patternProperties: {
1891
+ "^x-": {}
1892
+ }
1893
+ },
1894
+ OnSuccessConfig: {
1895
+ type: "object",
1896
+ properties: {
1897
+ run: {
1898
+ type: "array",
1899
+ items: {
1900
+ type: "string"
1901
+ },
1902
+ description: "Post-success steps to run"
1903
+ },
1904
+ goto: {
1905
+ type: "string",
1906
+ description: "Optional jump back to ancestor step (by id)"
1907
+ },
1908
+ goto_event: {
1909
+ $ref: "#/definitions/EventTrigger",
1910
+ description: "Simulate a different event when performing goto (e.g., 'pr_updated')"
1911
+ },
1912
+ goto_js: {
1913
+ type: "string",
1914
+ description: "Dynamic goto: JS expression returning step id or null"
1915
+ },
1916
+ run_js: {
1917
+ type: "string",
1918
+ description: "Dynamic post-success steps: JS expression returning string[]"
1919
+ },
1920
+ transitions: {
1921
+ type: "array",
1922
+ items: {
1923
+ $ref: "#/definitions/TransitionRule"
1924
+ },
1925
+ description: "Declarative transitions (see OnFailConfig.transitions)."
1926
+ }
1927
+ },
1928
+ additionalProperties: false,
1929
+ description: "Success routing configuration per check",
1930
+ patternProperties: {
1931
+ "^x-": {}
1932
+ }
1933
+ },
1934
+ OnFinishConfig: {
1935
+ type: "object",
1936
+ properties: {
1937
+ run: {
1938
+ type: "array",
1939
+ items: {
1940
+ type: "string"
1941
+ },
1942
+ description: "Post-finish steps to run"
1943
+ },
1944
+ goto: {
1945
+ type: "string",
1946
+ description: "Optional jump back to ancestor step (by id)"
1947
+ },
1948
+ goto_event: {
1949
+ $ref: "#/definitions/EventTrigger",
1950
+ description: "Simulate a different event when performing goto (e.g., 'pr_updated')"
1951
+ },
1952
+ goto_js: {
1953
+ type: "string",
1954
+ description: "Dynamic goto: JS expression returning step id or null"
1955
+ },
1956
+ run_js: {
1957
+ type: "string",
1958
+ description: "Dynamic post-finish steps: JS expression returning string[]"
1959
+ },
1960
+ transitions: {
1961
+ type: "array",
1962
+ items: {
1963
+ $ref: "#/definitions/TransitionRule"
1964
+ },
1965
+ description: "Declarative transitions (see OnFailConfig.transitions)."
1966
+ }
1967
+ },
1968
+ additionalProperties: false,
1969
+ description: "Finish routing configuration for forEach checks Runs once after ALL iterations of forEach and ALL dependent checks complete",
1970
+ patternProperties: {
1971
+ "^x-": {}
1972
+ }
1973
+ },
1974
+ OutputConfig: {
1975
+ type: "object",
1976
+ properties: {
1977
+ pr_comment: {
1978
+ $ref: "#/definitions/PrCommentOutput",
1979
+ description: "PR comment configuration"
1980
+ },
1981
+ file_comment: {
1982
+ $ref: "#/definitions/FileCommentOutput",
1983
+ description: "File comment configuration (optional)"
1984
+ },
1985
+ github_checks: {
1986
+ $ref: "#/definitions/GitHubCheckOutput",
1987
+ description: "GitHub check runs configuration (optional)"
1988
+ },
1989
+ suppressionEnabled: {
1990
+ type: "boolean",
1991
+ description: "Whether to enable issue suppression via visor-disable comments (default: true)"
1992
+ }
1993
+ },
1994
+ required: ["pr_comment"],
1995
+ additionalProperties: false,
1996
+ description: "Output configuration",
1997
+ patternProperties: {
1998
+ "^x-": {}
1999
+ }
2000
+ },
2001
+ PrCommentOutput: {
2002
+ type: "object",
2003
+ properties: {
2004
+ enabled: {
2005
+ type: "boolean",
2006
+ description: "Whether PR comments are enabled"
2007
+ },
2008
+ format: {
2009
+ $ref: "#/definitions/ConfigOutputFormat",
2010
+ description: "Format of the output"
2011
+ },
2012
+ group_by: {
2013
+ $ref: "#/definitions/GroupByOption",
2014
+ description: "How to group the results"
2015
+ },
2016
+ collapse: {
2017
+ type: "boolean",
2018
+ description: "Whether to collapse sections by default"
2019
+ },
2020
+ debug: {
2021
+ $ref: "#/definitions/DebugConfig",
2022
+ description: "Debug mode configuration (optional)"
2023
+ }
2024
+ },
2025
+ required: ["format", "group_by", "collapse"],
2026
+ additionalProperties: false,
2027
+ description: "PR comment output configuration",
2028
+ patternProperties: {
2029
+ "^x-": {}
2030
+ }
2031
+ },
2032
+ ConfigOutputFormat: {
2033
+ type: "string",
2034
+ enum: ["table", "json", "markdown", "sarif"],
2035
+ description: "Valid output formats"
2036
+ },
2037
+ GroupByOption: {
2038
+ type: "string",
2039
+ enum: ["check", "file", "severity", "group"],
2040
+ description: "Valid grouping options"
2041
+ },
2042
+ DebugConfig: {
2043
+ type: "object",
2044
+ properties: {
2045
+ enabled: {
2046
+ type: "boolean",
2047
+ description: "Enable debug mode"
2048
+ },
2049
+ includePrompts: {
2050
+ type: "boolean",
2051
+ description: "Include AI prompts in debug output"
2052
+ },
2053
+ includeRawResponses: {
2054
+ type: "boolean",
2055
+ description: "Include raw AI responses in debug output"
2056
+ },
2057
+ includeTiming: {
2058
+ type: "boolean",
2059
+ description: "Include timing information"
2060
+ },
2061
+ includeProviderInfo: {
2062
+ type: "boolean",
2063
+ description: "Include provider information"
2064
+ }
2065
+ },
2066
+ required: [
2067
+ "enabled",
2068
+ "includePrompts",
2069
+ "includeRawResponses",
2070
+ "includeTiming",
2071
+ "includeProviderInfo"
2072
+ ],
2073
+ additionalProperties: false,
2074
+ description: "Debug mode configuration",
2075
+ patternProperties: {
2076
+ "^x-": {}
2077
+ }
2078
+ },
2079
+ FileCommentOutput: {
2080
+ type: "object",
2081
+ properties: {
2082
+ enabled: {
2083
+ type: "boolean",
2084
+ description: "Whether file comments are enabled"
2085
+ },
2086
+ inline: {
2087
+ type: "boolean",
2088
+ description: "Whether to show inline comments"
2089
+ }
2090
+ },
2091
+ required: ["enabled", "inline"],
2092
+ additionalProperties: false,
2093
+ description: "File comment output configuration",
2094
+ patternProperties: {
2095
+ "^x-": {}
2096
+ }
2097
+ },
2098
+ GitHubCheckOutput: {
2099
+ type: "object",
2100
+ properties: {
2101
+ enabled: {
2102
+ type: "boolean",
2103
+ description: "Whether GitHub check runs are enabled"
2104
+ },
2105
+ per_check: {
2106
+ type: "boolean",
2107
+ description: "Whether to create individual check runs per configured check"
2108
+ },
2109
+ name_prefix: {
2110
+ type: "string",
2111
+ description: "Custom name prefix for check runs"
2112
+ }
2113
+ },
2114
+ required: ["enabled", "per_check"],
2115
+ additionalProperties: false,
2116
+ description: "GitHub Check Runs output configuration",
2117
+ patternProperties: {
2118
+ "^x-": {}
2119
+ }
2120
+ },
2121
+ HttpServerConfig: {
2122
+ type: "object",
2123
+ properties: {
2124
+ enabled: {
2125
+ type: "boolean",
2126
+ description: "Whether HTTP server is enabled"
2127
+ },
2128
+ port: {
2129
+ type: "number",
2130
+ description: "Port to listen on"
2131
+ },
2132
+ host: {
2133
+ type: "string",
2134
+ description: "Host/IP to bind to (defaults to 0.0.0.0)"
2135
+ },
2136
+ tls: {
2137
+ $ref: "#/definitions/TlsConfig",
2138
+ description: "TLS/SSL configuration for HTTPS"
2139
+ },
2140
+ auth: {
2141
+ $ref: "#/definitions/HttpAuthConfig",
2142
+ description: "Authentication configuration"
2143
+ },
2144
+ endpoints: {
2145
+ type: "array",
2146
+ items: {
2147
+ $ref: "#/definitions/HttpEndpointConfig"
2148
+ },
2149
+ description: "HTTP endpoints configuration"
2150
+ }
2151
+ },
2152
+ required: ["enabled", "port"],
2153
+ additionalProperties: false,
2154
+ description: "HTTP server configuration for receiving webhooks",
2155
+ patternProperties: {
2156
+ "^x-": {}
2157
+ }
2158
+ },
2159
+ TlsConfig: {
2160
+ type: "object",
2161
+ properties: {
2162
+ enabled: {
2163
+ type: "boolean",
2164
+ description: "Enable TLS/HTTPS"
2165
+ },
2166
+ cert: {
2167
+ type: "string",
2168
+ description: "Path to TLS certificate file or certificate content"
2169
+ },
2170
+ key: {
2171
+ type: "string",
2172
+ description: "Path to TLS key file or key content"
2173
+ },
2174
+ ca: {
2175
+ type: "string",
2176
+ description: "Path to CA certificate file or CA content (optional)"
2177
+ },
2178
+ rejectUnauthorized: {
2179
+ type: "boolean",
2180
+ description: "Reject unauthorized connections (default: true)"
2181
+ }
2182
+ },
2183
+ required: ["enabled"],
2184
+ additionalProperties: false,
2185
+ description: "TLS/SSL configuration for HTTPS server",
2186
+ patternProperties: {
2187
+ "^x-": {}
2188
+ }
2189
+ },
2190
+ HttpAuthConfig: {
2191
+ type: "object",
2192
+ properties: {
2193
+ type: {
2194
+ type: "string",
2195
+ enum: ["bearer_token", "hmac", "basic", "none"],
2196
+ description: "Authentication type"
2197
+ },
2198
+ secret: {
2199
+ type: "string",
2200
+ description: "Secret or token for authentication"
2201
+ },
2202
+ username: {
2203
+ type: "string",
2204
+ description: "Username for basic auth"
2205
+ },
2206
+ password: {
2207
+ type: "string",
2208
+ description: "Password for basic auth"
2209
+ }
2210
+ },
2211
+ required: ["type"],
2212
+ additionalProperties: false,
2213
+ description: "HTTP server authentication configuration",
2214
+ patternProperties: {
2215
+ "^x-": {}
2216
+ }
2217
+ },
2218
+ HttpEndpointConfig: {
2219
+ type: "object",
2220
+ properties: {
2221
+ path: {
2222
+ type: "string",
2223
+ description: "Path for the webhook endpoint"
2224
+ },
2225
+ transform: {
2226
+ type: "string",
2227
+ description: "Optional transform template (Liquid) for the received data"
2228
+ },
2229
+ name: {
2230
+ type: "string",
2231
+ description: "Optional name/ID for this endpoint"
2232
+ }
2233
+ },
2234
+ required: ["path"],
2235
+ additionalProperties: false,
2236
+ description: "HTTP server endpoint configuration",
2237
+ patternProperties: {
2238
+ "^x-": {}
2239
+ }
2240
+ },
2241
+ MemoryConfig: {
2242
+ type: "object",
2243
+ properties: {
2244
+ storage: {
2245
+ type: "string",
2246
+ enum: ["memory", "file"],
2247
+ description: 'Storage mode: "memory" (in-memory, default) or "file" (persistent)'
2248
+ },
2249
+ format: {
2250
+ type: "string",
2251
+ enum: ["json", "csv"],
2252
+ description: "Storage format (only for file storage, default: json)"
2253
+ },
2254
+ file: {
2255
+ type: "string",
2256
+ description: "File path (required if storage: file)"
2257
+ },
2258
+ namespace: {
2259
+ type: "string",
2260
+ description: 'Default namespace (default: "default")'
2261
+ },
2262
+ auto_load: {
2263
+ type: "boolean",
2264
+ description: "Auto-load on startup (default: true if storage: file)"
2265
+ },
2266
+ auto_save: {
2267
+ type: "boolean",
2268
+ description: "Auto-save after operations (default: true if storage: file)"
2269
+ }
2270
+ },
2271
+ additionalProperties: false,
2272
+ description: "Memory storage configuration",
2273
+ patternProperties: {
2274
+ "^x-": {}
2275
+ }
2276
+ },
2277
+ TagFilter: {
2278
+ type: "object",
2279
+ properties: {
2280
+ include: {
2281
+ type: "array",
2282
+ items: {
2283
+ type: "string"
2284
+ },
2285
+ description: "Tags that checks must have to be included (ANY match)"
2286
+ },
2287
+ exclude: {
2288
+ type: "array",
2289
+ items: {
2290
+ type: "string"
2291
+ },
2292
+ description: "Tags that will exclude checks if present (ANY match)"
2293
+ }
2294
+ },
2295
+ additionalProperties: false,
2296
+ description: "Tag filter configuration for selective check execution",
2297
+ patternProperties: {
2298
+ "^x-": {}
2299
+ }
2300
+ },
2301
+ RoutingDefaults: {
2302
+ type: "object",
2303
+ properties: {
2304
+ max_loops: {
2305
+ type: "number",
2306
+ description: "Per-scope cap on routing transitions (success + failure)"
2307
+ },
2308
+ defaults: {
2309
+ type: "object",
2310
+ properties: {
2311
+ on_fail: {
2312
+ $ref: "#/definitions/OnFailConfig"
2313
+ }
2314
+ },
2315
+ additionalProperties: false,
2316
+ description: "Default policies applied to checks (step-level overrides take precedence)",
2317
+ patternProperties: {
2318
+ "^x-": {}
2319
+ }
2320
+ }
2321
+ },
2322
+ additionalProperties: false,
2323
+ description: "Global routing defaults",
2324
+ patternProperties: {
2325
+ "^x-": {}
2326
+ }
2327
+ },
2328
+ LimitsConfig: {
2329
+ type: "object",
2330
+ properties: {
2331
+ max_runs_per_check: {
2332
+ type: "number",
2333
+ description: "Maximum number of executions per check within a single engine run. Applies to each distinct scope independently for forEach item executions. Set to 0 or negative to disable. Default: 50."
2334
+ },
2335
+ max_workflow_depth: {
2336
+ type: "number",
2337
+ description: "Maximum nesting depth for workflows executed by the state machine engine. Nested workflows are invoked by the workflow provider; this limit prevents accidental infinite recursion. Default: 3."
2338
+ }
2339
+ },
2340
+ additionalProperties: false,
2341
+ description: "Global engine limits",
2342
+ patternProperties: {
2343
+ "^x-": {}
2344
+ }
2345
+ },
2346
+ WorkspaceConfig: {
2347
+ type: "object",
2348
+ properties: {
2349
+ enabled: {
2350
+ type: "boolean",
2351
+ description: "Enable workspace isolation (default: true when config present)"
2352
+ },
2353
+ base_path: {
2354
+ type: "string",
2355
+ description: "Base path for workspaces (default: /tmp/visor-workspaces)"
2356
+ },
2357
+ name: {
2358
+ type: "string",
2359
+ description: "Workspace directory name (defaults to session id)"
2360
+ },
2361
+ main_project_name: {
2362
+ type: "string",
2363
+ description: "Main project folder name inside the workspace (defaults to original directory name)"
2364
+ },
2365
+ cleanup_on_exit: {
2366
+ type: "boolean",
2367
+ description: "Clean up workspace on exit (default: true)"
2368
+ },
2369
+ include_main_project: {
2370
+ type: "boolean",
2371
+ description: "Include main project worktree in AI allowed folders (default: false)"
2372
+ }
2373
+ },
2374
+ additionalProperties: false,
2375
+ description: "Workspace isolation configuration",
2376
+ patternProperties: {
2377
+ "^x-": {}
2378
+ }
2379
+ },
2380
+ SlackConfig: {
2381
+ type: "object",
2382
+ properties: {
2383
+ version: {
2384
+ type: "string",
2385
+ description: "Slack API version"
2386
+ },
2387
+ mentions: {
2388
+ type: "string",
2389
+ description: "Mention handling: 'all', 'direct', etc."
2390
+ },
2391
+ threads: {
2392
+ type: "string",
2393
+ description: "Thread handling: 'required', 'optional', etc."
2394
+ },
2395
+ allow_bot_messages: {
2396
+ type: "boolean",
2397
+ description: "Allow bot_message events to trigger runs (default: false)"
2398
+ },
2399
+ show_raw_output: {
2400
+ type: "boolean",
2401
+ description: "Show raw output in Slack responses"
2402
+ },
2403
+ telemetry: {
2404
+ $ref: "#/definitions/SlackTelemetryConfig",
2405
+ description: "Append telemetry identifiers to Slack replies."
2406
+ }
2407
+ },
2408
+ additionalProperties: false,
2409
+ description: "Slack configuration",
2410
+ patternProperties: {
2411
+ "^x-": {}
2412
+ }
2413
+ },
2414
+ SlackTelemetryConfig: {
2415
+ type: "object",
2416
+ properties: {
2417
+ enabled: {
2418
+ type: "boolean",
2419
+ description: "Enable telemetry ID suffix in Slack messages"
2420
+ }
2421
+ },
2422
+ additionalProperties: false,
2423
+ patternProperties: {
2424
+ "^x-": {}
2425
+ }
2426
+ }
2427
+ }
2428
+ };
2429
+ config_schema_default = configSchema;
2430
+ }
2431
+ });
2432
+
2433
+ // src/config.ts
2434
+ var config_exports = {};
2435
+ __export(config_exports, {
2436
+ ConfigManager: () => ConfigManager,
2437
+ VALID_EVENT_TRIGGERS: () => VALID_EVENT_TRIGGERS
2438
+ });
2439
+ import * as yaml2 from "js-yaml";
2440
+ import * as fs2 from "fs";
2441
+ import * as path2 from "path";
2442
+ import simpleGit from "simple-git";
2443
+ import Ajv from "ajv";
2444
+ import addFormats from "ajv-formats";
2445
+ var VALID_EVENT_TRIGGERS, ConfigManager, __ajvValidate, __ajvErrors;
2446
+ var init_config = __esm({
2447
+ "src/config.ts"() {
2448
+ init_logger();
2449
+ init_config_loader();
2450
+ init_config_merger();
2451
+ init_sandbox();
2452
+ VALID_EVENT_TRIGGERS = [
2453
+ "pr_opened",
2454
+ "pr_updated",
2455
+ "pr_closed",
2456
+ "issue_opened",
2457
+ "issue_comment",
2458
+ "manual",
2459
+ "schedule",
2460
+ "webhook_received"
2461
+ ];
2462
+ ConfigManager = class {
2463
+ validCheckTypes = [
2464
+ "ai",
2465
+ "claude-code",
2466
+ "mcp",
2467
+ "command",
2468
+ "script",
2469
+ "http",
2470
+ "http_input",
2471
+ "http_client",
2472
+ "memory",
2473
+ "noop",
2474
+ "log",
2475
+ "github",
2476
+ "human-input",
2477
+ "workflow",
2478
+ "git-checkout"
2479
+ ];
2480
+ validEventTriggers = [...VALID_EVENT_TRIGGERS];
2481
+ validOutputFormats = ["table", "json", "markdown", "sarif"];
2482
+ validGroupByOptions = ["check", "file", "severity", "group"];
2483
+ /**
2484
+ * Load configuration from a file
2485
+ */
2486
+ async loadConfig(configPath, options = {}) {
2487
+ const { validate = true, mergeDefaults = true, allowedRemotePatterns } = options;
2488
+ const resolvedPath = path2.isAbsolute(configPath) ? configPath : path2.resolve(process.cwd(), configPath);
2489
+ try {
2490
+ let configContent;
2491
+ try {
2492
+ configContent = fs2.readFileSync(resolvedPath, "utf8");
2493
+ } catch (readErr) {
2494
+ if (readErr && (readErr.code === "ENOENT" || readErr.code === "ENOTDIR")) {
2495
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
2496
+ }
2497
+ throw new Error(
2498
+ `Failed to read configuration file ${resolvedPath}: ${readErr?.message || String(readErr)}`
2499
+ );
2500
+ }
2501
+ let parsedConfig;
2502
+ try {
2503
+ parsedConfig = yaml2.load(configContent);
2504
+ } catch (yamlError) {
2505
+ const errorMessage = yamlError instanceof Error ? yamlError.message : String(yamlError);
2506
+ throw new Error(`Invalid YAML syntax in ${resolvedPath}: ${errorMessage}`);
2507
+ }
2508
+ if (!parsedConfig || typeof parsedConfig !== "object") {
2509
+ throw new Error("Configuration file must contain a valid YAML object");
2510
+ }
2511
+ const extendsValue = parsedConfig.extends || parsedConfig.include;
2512
+ if (extendsValue) {
2513
+ const loaderOptions = {
2514
+ baseDir: path2.dirname(resolvedPath),
2515
+ allowRemote: this.isRemoteExtendsAllowed(),
2516
+ maxDepth: 10,
2517
+ allowedRemotePatterns
2518
+ };
2519
+ const loader = new ConfigLoader(loaderOptions);
2520
+ const merger = new ConfigMerger();
2521
+ const extends_ = Array.isArray(extendsValue) ? extendsValue : [extendsValue];
2522
+ const { extends: _, include: __, ...configWithoutExtends } = parsedConfig;
2523
+ let mergedConfig = {};
2524
+ for (const source of extends_) {
2525
+ console.log(`\u{1F4E6} Extending from: ${source}`);
2526
+ const parentConfig = await loader.fetchConfig(source);
2527
+ mergedConfig = merger.merge(mergedConfig, parentConfig);
2528
+ }
2529
+ parsedConfig = merger.merge(mergedConfig, configWithoutExtends);
2530
+ parsedConfig = merger.removeDisabledChecks(parsedConfig);
2531
+ }
2532
+ if (parsedConfig.id && typeof parsedConfig.id === "string") {
2533
+ parsedConfig = await this.convertWorkflowToConfig(parsedConfig, path2.dirname(resolvedPath));
2534
+ }
2535
+ parsedConfig = this.normalizeStepsAndChecks(parsedConfig, !!extendsValue);
2536
+ await this.loadWorkflows(parsedConfig, path2.dirname(resolvedPath));
2537
+ if (validate) {
2538
+ this.validateConfig(parsedConfig);
2539
+ }
2540
+ let finalConfig = parsedConfig;
2541
+ if (mergeDefaults) {
2542
+ finalConfig = this.mergeWithDefaults(parsedConfig);
2543
+ }
2544
+ return finalConfig;
2545
+ } catch (error) {
2546
+ if (error instanceof Error) {
2547
+ if (error.message.includes("not found") || error.message.includes("Invalid YAML") || error.message.includes("extends") || error.message.includes("EACCES") || error.message.includes("EISDIR")) {
2548
+ throw error;
2549
+ }
2550
+ if (error.message.includes("ENOENT")) {
2551
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
2552
+ }
2553
+ if (error.message.includes("EPERM")) {
2554
+ throw new Error(`Permission denied reading configuration file: ${resolvedPath}`);
2555
+ }
2556
+ throw new Error(`Failed to read configuration file ${resolvedPath}: ${error.message}`);
2557
+ }
2558
+ throw error;
2559
+ }
2560
+ }
2561
+ /**
2562
+ * Load configuration from an in-memory object (used by the test runner to
2563
+ * handle co-located config + tests without writing temp files).
2564
+ */
2565
+ async loadConfigFromObject(obj, options = {}) {
2566
+ const { validate = true, mergeDefaults = true, allowedRemotePatterns, baseDir } = options;
2567
+ try {
2568
+ let parsedConfig = JSON.parse(JSON.stringify(obj || {}));
2569
+ if (!parsedConfig || typeof parsedConfig !== "object") {
2570
+ throw new Error("Configuration must be a YAML/JSON object");
2571
+ }
2572
+ const extendsValue = parsedConfig.extends || parsedConfig.include;
2573
+ if (extendsValue) {
2574
+ const loaderOptions = {
2575
+ baseDir: baseDir || process.cwd(),
2576
+ allowRemote: this.isRemoteExtendsAllowed(),
2577
+ maxDepth: 10,
2578
+ allowedRemotePatterns
2579
+ };
2580
+ const loader = new ConfigLoader(loaderOptions);
2581
+ const extends_ = Array.isArray(extendsValue) ? extendsValue : [extendsValue];
2582
+ const { extends: _, include: __, ...configWithoutExtends } = parsedConfig;
2583
+ let mergedConfig = {};
2584
+ for (const source of extends_) {
2585
+ console.log(`\u{1F4E6} Extending from: ${source}`);
2586
+ const parentConfig = await loader.fetchConfig(String(source));
2587
+ mergedConfig = new ConfigMerger().merge(mergedConfig, parentConfig);
2588
+ }
2589
+ parsedConfig = new ConfigMerger().merge(mergedConfig, configWithoutExtends);
2590
+ parsedConfig = new ConfigMerger().removeDisabledChecks(parsedConfig);
2591
+ }
2592
+ if (parsedConfig.id && typeof parsedConfig.id === "string") {
2593
+ parsedConfig = await this.convertWorkflowToConfig(parsedConfig, baseDir || process.cwd());
2594
+ }
2595
+ parsedConfig = this.normalizeStepsAndChecks(parsedConfig, !!extendsValue);
2596
+ await this.loadWorkflows(parsedConfig, baseDir || process.cwd());
2597
+ if (validate) this.validateConfig(parsedConfig);
2598
+ let finalConfig = parsedConfig;
2599
+ if (mergeDefaults) finalConfig = this.mergeWithDefaults(parsedConfig);
2600
+ return finalConfig;
2601
+ } catch (error) {
2602
+ if (error instanceof Error) throw new Error(`Failed to load configuration: ${error.message}`);
2603
+ throw error;
2604
+ }
2605
+ }
2606
+ /**
2607
+ * Find and load configuration from default locations
2608
+ */
2609
+ async findAndLoadConfig(options = {}) {
2610
+ const gitRoot = await this.findGitRepositoryRoot();
2611
+ const searchDirs = [gitRoot, process.cwd()].filter(Boolean);
2612
+ for (const baseDir of searchDirs) {
2613
+ const candidates = ["visor.yaml", "visor.yml", ".visor.yaml", ".visor.yml"].map(
2614
+ (p) => path2.join(baseDir, p)
2615
+ );
2616
+ for (const p of candidates) {
2617
+ try {
2618
+ const st = fs2.statSync(p);
2619
+ if (!st.isFile()) continue;
2620
+ const isLegacy = path2.basename(p).startsWith(".");
2621
+ if (isLegacy) {
2622
+ if (process.env.VISOR_STRICT_CONFIG_NAME === "true") {
2623
+ const rel = path2.relative(baseDir, p);
2624
+ throw new Error(
2625
+ `Legacy config detected: ${rel}. Please rename to visor.yaml (or visor.yml).`
2626
+ );
2627
+ }
2628
+ return this.loadConfig(p, options);
2629
+ }
2630
+ return this.loadConfig(p, options);
2631
+ } catch (e) {
2632
+ if (e && e.code === "ENOENT") continue;
2633
+ if (e) throw e;
2634
+ }
2635
+ }
2636
+ }
2637
+ const bundledConfig = this.loadBundledDefaultConfig();
2638
+ if (bundledConfig) {
2639
+ return bundledConfig;
2640
+ }
2641
+ return this.getDefaultConfig();
2642
+ }
2643
+ /**
2644
+ * Find the git repository root directory
2645
+ */
2646
+ async findGitRepositoryRoot() {
2647
+ try {
2648
+ const git = simpleGit();
2649
+ const isRepo = await git.checkIsRepo();
2650
+ if (!isRepo) {
2651
+ return null;
2652
+ }
2653
+ const rootDir = await git.revparse(["--show-toplevel"]);
2654
+ return rootDir.trim();
2655
+ } catch {
2656
+ return null;
2657
+ }
2658
+ }
2659
+ /**
2660
+ * Get default configuration
2661
+ */
2662
+ async getDefaultConfig() {
2663
+ return {
2664
+ version: "1.0",
2665
+ steps: {},
2666
+ checks: {},
2667
+ // Keep for backward compatibility
2668
+ max_parallelism: 3,
2669
+ output: {
2670
+ pr_comment: {
2671
+ format: "markdown",
2672
+ group_by: "check",
2673
+ collapse: true
2674
+ }
2675
+ }
2676
+ };
2677
+ }
2678
+ /**
2679
+ * Load bundled default configuration from the package
2680
+ */
2681
+ loadBundledDefaultConfig() {
2682
+ try {
2683
+ const possiblePaths = [];
2684
+ if (typeof __dirname !== "undefined") {
2685
+ possiblePaths.push(
2686
+ path2.join(__dirname, "defaults", "visor.yaml"),
2687
+ path2.join(__dirname, "..", "defaults", "visor.yaml")
2688
+ );
2689
+ }
2690
+ const pkgRoot = this.findPackageRoot();
2691
+ if (pkgRoot) {
2692
+ possiblePaths.push(path2.join(pkgRoot, "defaults", "visor.yaml"));
2693
+ }
2694
+ if (process.env.GITHUB_ACTION_PATH) {
2695
+ possiblePaths.push(
2696
+ path2.join(process.env.GITHUB_ACTION_PATH, "defaults", "visor.yaml"),
2697
+ path2.join(process.env.GITHUB_ACTION_PATH, "dist", "defaults", "visor.yaml")
2698
+ );
2699
+ }
2700
+ let bundledConfigPath;
2701
+ for (const possiblePath of possiblePaths) {
2702
+ if (fs2.existsSync(possiblePath)) {
2703
+ bundledConfigPath = possiblePath;
2704
+ break;
2705
+ }
2706
+ }
2707
+ if (bundledConfigPath) {
2708
+ console.error(`\u{1F4E6} Loading bundled default configuration from ${bundledConfigPath}`);
2709
+ const readAndParse = (p) => {
2710
+ const raw = fs2.readFileSync(p, "utf8");
2711
+ const obj = yaml2.load(raw);
2712
+ if (!obj || typeof obj !== "object") return {};
2713
+ if (obj.include && !obj.extends) {
2714
+ const inc = obj.include;
2715
+ obj.extends = Array.isArray(inc) ? inc : [inc];
2716
+ delete obj.include;
2717
+ }
2718
+ return obj;
2719
+ };
2720
+ const baseDir = path2.dirname(bundledConfigPath);
2721
+ const merger = new (init_config_merger(), __toCommonJS(config_merger_exports)).ConfigMerger();
2722
+ const loadWithExtendsSync = (p) => {
2723
+ const current = readAndParse(p);
2724
+ const extVal = current.extends || current.include;
2725
+ if (current.extends !== void 0) delete current.extends;
2726
+ if (current.include !== void 0) delete current.include;
2727
+ if (!extVal) return current;
2728
+ const list = Array.isArray(extVal) ? extVal : [extVal];
2729
+ let acc = {};
2730
+ for (const src of list) {
2731
+ const rel = typeof src === "string" ? src : String(src);
2732
+ const abs = path2.isAbsolute(rel) ? rel : path2.resolve(baseDir, rel);
2733
+ const parentCfg = loadWithExtendsSync(abs);
2734
+ acc = merger.merge(acc, parentCfg);
2735
+ }
2736
+ return merger.merge(acc, current);
2737
+ };
2738
+ let parsedConfig = loadWithExtendsSync(bundledConfigPath);
2739
+ parsedConfig = this.normalizeStepsAndChecks(parsedConfig);
2740
+ this.validateConfig(parsedConfig);
2741
+ return this.mergeWithDefaults(parsedConfig);
2742
+ }
2743
+ } catch (error) {
2744
+ console.warn(
2745
+ "Failed to load bundled default config:",
2746
+ error instanceof Error ? error.message : String(error)
2747
+ );
2748
+ }
2749
+ return null;
2750
+ }
2751
+ /**
2752
+ * Find the root directory of the Visor package
2753
+ */
2754
+ findPackageRoot() {
2755
+ let currentDir = __dirname;
2756
+ while (currentDir !== path2.dirname(currentDir)) {
2757
+ const packageJsonPath = path2.join(currentDir, "package.json");
2758
+ if (fs2.existsSync(packageJsonPath)) {
2759
+ try {
2760
+ const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
2761
+ if (packageJson.name === "@probelabs/visor") {
2762
+ return currentDir;
2763
+ }
2764
+ } catch {
2765
+ }
2766
+ }
2767
+ currentDir = path2.dirname(currentDir);
2768
+ }
2769
+ return null;
2770
+ }
2771
+ /**
2772
+ * Convert a workflow definition file to a visor config
2773
+ * When a workflow YAML is run standalone, register the workflow and use its tests as checks
2774
+ */
2775
+ async convertWorkflowToConfig(workflowData, basePath) {
2776
+ const { WorkflowRegistry } = await import("./workflow-registry-KFWSDSLM.mjs");
2777
+ const registry = WorkflowRegistry.getInstance();
2778
+ const workflowId = workflowData.id;
2779
+ logger.info(`Detected standalone workflow file: ${workflowId}`);
2780
+ if (workflowData.imports && Array.isArray(workflowData.imports)) {
2781
+ for (const source of workflowData.imports) {
2782
+ try {
2783
+ const results = await registry.import(source, { basePath, validate: true });
2784
+ for (const result2 of results) {
2785
+ if (!result2.valid && result2.errors) {
2786
+ const errors = result2.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
2787
+ throw new Error(`Failed to import workflow from '${source}':
2788
+ ${errors}`);
2789
+ }
2790
+ }
2791
+ logger.info(`Imported workflows from: ${source}`);
2792
+ } catch (err) {
2793
+ const errMsg = err instanceof Error ? err.message : String(err);
2794
+ if (errMsg.includes("already exists")) {
2795
+ logger.debug(`Workflow from '${source}' already imported, skipping`);
2796
+ } else {
2797
+ throw err;
2798
+ }
2799
+ }
2800
+ }
2801
+ }
2802
+ const tests = workflowData.tests || {};
2803
+ const workflowDefinition = { ...workflowData };
2804
+ delete workflowDefinition.tests;
2805
+ delete workflowDefinition.imports;
2806
+ const result = registry.register(workflowDefinition, "standalone", { override: true });
2807
+ if (!result.valid && result.errors) {
2808
+ const errors = result.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
2809
+ throw new Error(`Failed to register workflow '${workflowId}':
2810
+ ${errors}`);
2811
+ }
2812
+ logger.info(`Registered workflow '${workflowId}' for standalone execution`);
2813
+ const workflowSteps = workflowData.steps || {};
2814
+ const visorConfig = {
2815
+ version: "1.0",
2816
+ steps: workflowSteps,
2817
+ checks: workflowSteps,
2818
+ tests
2819
+ // Preserve test harness config (may be empty if stripped by test runner)
2820
+ };
2821
+ if (workflowData.outputs) {
2822
+ visorConfig.outputs = workflowData.outputs;
2823
+ }
2824
+ if (workflowData.inputs) {
2825
+ visorConfig.inputs = workflowData.inputs;
2826
+ }
2827
+ logger.debug(
2828
+ `Standalone workflow config has ${Object.keys(workflowSteps).length} workflow steps as checks`
2829
+ );
2830
+ logger.debug(`Workflow step names: ${Object.keys(workflowSteps).join(", ")}`);
2831
+ logger.debug(`Config keys after conversion: ${Object.keys(visorConfig).join(", ")}`);
2832
+ return visorConfig;
2833
+ }
2834
+ /**
2835
+ * Load and register workflows from configuration
2836
+ */
2837
+ async loadWorkflows(config, basePath) {
2838
+ if (!config.imports || config.imports.length === 0) {
2839
+ return;
2840
+ }
2841
+ const { WorkflowRegistry } = await import("./workflow-registry-KFWSDSLM.mjs");
2842
+ const registry = WorkflowRegistry.getInstance();
2843
+ for (const source of config.imports) {
2844
+ const results = await registry.import(source, { basePath, validate: true });
2845
+ for (const result of results) {
2846
+ if (!result.valid && result.errors) {
2847
+ const isAlreadyExists = result.errors.every((e) => e.message.includes("already exists"));
2848
+ if (isAlreadyExists) {
2849
+ logger.debug(`Workflow from '${source}' already imported, skipping`);
2850
+ continue;
2851
+ }
2852
+ const errors = result.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
2853
+ throw new Error(`Failed to import workflow from '${source}':
2854
+ ${errors}`);
2855
+ }
2856
+ }
2857
+ logger.info(`Imported workflows from: ${source}`);
2858
+ }
2859
+ }
2860
+ /**
2861
+ * Normalize 'checks' and 'steps' keys for backward compatibility
2862
+ * Ensures both keys are present and contain the same data
2863
+ */
2864
+ normalizeStepsAndChecks(config, preferChecks = false) {
2865
+ if (config.steps && config.checks) {
2866
+ if (preferChecks) {
2867
+ const merged = { ...config.steps, ...config.checks };
2868
+ config.steps = merged;
2869
+ config.checks = merged;
2870
+ } else {
2871
+ config.checks = config.steps;
2872
+ config.steps = config.steps;
2873
+ }
2874
+ } else if (config.steps && !config.checks) {
2875
+ config.checks = config.steps;
2876
+ } else if (config.checks && !config.steps) {
2877
+ config.steps = config.checks;
2878
+ }
2879
+ return config;
2880
+ }
2881
+ /**
2882
+ * Merge configuration with CLI options
2883
+ */
2884
+ mergeWithCliOptions(config, cliOptions) {
2885
+ const mergedConfig = { ...config };
2886
+ if (cliOptions.maxParallelism !== void 0) {
2887
+ mergedConfig.max_parallelism = cliOptions.maxParallelism;
2888
+ }
2889
+ if (cliOptions.failFast !== void 0) {
2890
+ mergedConfig.fail_fast = cliOptions.failFast;
2891
+ }
2892
+ return {
2893
+ config: mergedConfig,
2894
+ cliChecks: cliOptions.checks || [],
2895
+ cliOutput: cliOptions.output || "table"
2896
+ };
2897
+ }
2898
+ /**
2899
+ * Load configuration with environment variable overrides
2900
+ */
2901
+ async loadConfigWithEnvOverrides() {
2902
+ const environmentOverrides = {};
2903
+ if (process.env.VISOR_CONFIG_PATH) {
2904
+ environmentOverrides.configPath = process.env.VISOR_CONFIG_PATH;
2905
+ }
2906
+ if (process.env.VISOR_OUTPUT_FORMAT) {
2907
+ environmentOverrides.outputFormat = process.env.VISOR_OUTPUT_FORMAT;
2908
+ }
2909
+ let config;
2910
+ if (environmentOverrides.configPath) {
2911
+ try {
2912
+ config = await this.loadConfig(environmentOverrides.configPath);
2913
+ } catch {
2914
+ config = await this.findAndLoadConfig();
2915
+ }
2916
+ } else {
2917
+ config = await this.findAndLoadConfig();
2918
+ }
2919
+ return { config, environmentOverrides };
2920
+ }
2921
+ /**
2922
+ * Validate configuration against schema
2923
+ * @param config The config to validate
2924
+ * @param strict If true, treat warnings as errors (default: false)
2925
+ */
2926
+ validateConfig(config, strict = false) {
2927
+ const errors = [];
2928
+ const warnings = [];
2929
+ this.validateWithAjvSchema(config, errors, warnings);
2930
+ if (!config.version) {
2931
+ errors.push({
2932
+ field: "version",
2933
+ message: "Missing required field: version"
2934
+ });
2935
+ }
2936
+ if (!config.checks && !config.steps) {
2937
+ errors.push({
2938
+ field: "checks/steps",
2939
+ message: 'Missing required field: either "checks" or "steps" must be defined. "steps" is recommended for new configurations.'
2940
+ });
2941
+ }
2942
+ const checksToValidate = config.checks || config.steps;
2943
+ if (checksToValidate) {
2944
+ for (const [checkName, checkConfig] of Object.entries(checksToValidate)) {
2945
+ if (!checkConfig.type) {
2946
+ checkConfig.type = "ai";
2947
+ }
2948
+ this.validateCheckConfig(checkName, checkConfig, errors, config, warnings);
2949
+ if (checkConfig.ai_mcp_servers) {
2950
+ this.validateMcpServersObject(
2951
+ checkConfig.ai_mcp_servers,
2952
+ `checks.${checkName}.ai_mcp_servers`,
2953
+ errors,
2954
+ warnings
2955
+ );
2956
+ }
2957
+ if (checkConfig.ai?.mcpServers) {
2958
+ this.validateMcpServersObject(
2959
+ checkConfig.ai.mcpServers,
2960
+ `checks.${checkName}.ai.mcpServers`,
2961
+ errors,
2962
+ warnings
2963
+ );
2964
+ }
2965
+ if (checkConfig.ai_mcp_servers && checkConfig.ai?.mcpServers) {
2966
+ const lower = Object.keys(checkConfig.ai_mcp_servers);
2967
+ const higher = Object.keys(checkConfig.ai.mcpServers);
2968
+ const overridden = lower.filter((k) => higher.includes(k));
2969
+ warnings.push({
2970
+ field: `checks.${checkName}.ai.mcpServers`,
2971
+ message: overridden.length > 0 ? `Both ai_mcp_servers and ai.mcpServers are set; ai.mcpServers overrides these servers: ${overridden.join(
2972
+ ", "
2973
+ )}` : "Both ai_mcp_servers and ai.mcpServers are set; ai.mcpServers takes precedence for this check."
2974
+ });
2975
+ }
2976
+ try {
2977
+ const anyCheck = checkConfig;
2978
+ const aiObj = anyCheck.ai || void 0;
2979
+ const hasBareMcpAtCheck = Object.prototype.hasOwnProperty.call(anyCheck, "mcpServers");
2980
+ const hasAiMcp = aiObj && Object.prototype.hasOwnProperty.call(aiObj, "mcpServers");
2981
+ const hasClaudeCodeMcp = anyCheck.claude_code && typeof anyCheck.claude_code === "object" && Object.prototype.hasOwnProperty.call(
2982
+ anyCheck.claude_code,
2983
+ "mcpServers"
2984
+ );
2985
+ if (checkConfig.type === "ai") {
2986
+ if (hasBareMcpAtCheck) {
2987
+ warnings.push({
2988
+ field: `checks.${checkName}.mcpServers`,
2989
+ message: "'mcpServers' at the check root is ignored for type 'ai'. Use 'ai.mcpServers' or 'ai_mcp_servers' instead.",
2990
+ value: anyCheck.mcpServers
2991
+ });
2992
+ }
2993
+ if (hasClaudeCodeMcp) {
2994
+ warnings.push({
2995
+ field: `checks.${checkName}.claude_code.mcpServers`,
2996
+ message: "'claude_code.mcpServers' is ignored for type 'ai'. Use 'ai.mcpServers' or 'ai_mcp_servers' instead."
2997
+ });
2998
+ }
2999
+ }
3000
+ if (checkConfig.type === "claude-code") {
3001
+ if (hasAiMcp || checkConfig.ai_mcp_servers) {
3002
+ warnings.push({
3003
+ field: hasAiMcp ? `checks.${checkName}.ai.mcpServers` : `checks.${checkName}.ai_mcp_servers`,
3004
+ message: "For type 'claude-code', MCP must be configured under 'claude_code.mcpServers'. 'ai.mcpServers' and 'ai_mcp_servers' are ignored for this check."
3005
+ });
3006
+ }
3007
+ }
3008
+ } catch {
3009
+ }
3010
+ }
3011
+ }
3012
+ if (config.ai_mcp_servers) {
3013
+ this.validateMcpServersObject(config.ai_mcp_servers, "ai_mcp_servers", errors, warnings);
3014
+ }
3015
+ if (config.output) {
3016
+ this.validateOutputConfig(config.output, errors);
3017
+ }
3018
+ if (config.http_server) {
3019
+ this.validateHttpServerConfig(
3020
+ config.http_server,
3021
+ errors
3022
+ );
3023
+ }
3024
+ if (config.max_parallelism !== void 0) {
3025
+ if (typeof config.max_parallelism !== "number" || config.max_parallelism < 1 || !Number.isInteger(config.max_parallelism)) {
3026
+ errors.push({
3027
+ field: "max_parallelism",
3028
+ message: "max_parallelism must be a positive integer (minimum 1)",
3029
+ value: config.max_parallelism
3030
+ });
3031
+ }
3032
+ }
3033
+ if (config.tag_filter) {
3034
+ this.validateTagFilter(config.tag_filter, errors);
3035
+ }
3036
+ if (strict && warnings.length > 0) {
3037
+ errors.push(...warnings);
3038
+ }
3039
+ if (errors.length > 0) {
3040
+ throw new Error(errors[0].message);
3041
+ }
3042
+ if (!strict && warnings.length > 0) {
3043
+ for (const w of warnings) {
3044
+ logger.warn(`\u26A0\uFE0F Config warning [${w.field}]: ${w.message}`);
3045
+ }
3046
+ }
3047
+ }
3048
+ /**
3049
+ * Validate individual check configuration
3050
+ */
3051
+ validateCheckConfig(checkName, checkConfig, errors, config, _warnings) {
3052
+ if (!checkConfig.type) {
3053
+ checkConfig.type = "ai";
3054
+ }
3055
+ if (checkConfig.type === "logger") {
3056
+ checkConfig.type = "log";
3057
+ }
3058
+ if (!this.validCheckTypes.includes(checkConfig.type)) {
3059
+ errors.push({
3060
+ field: `checks.${checkName}.type`,
3061
+ message: `Invalid check type "${checkConfig.type}". Must be: ${this.validCheckTypes.join(", ")}`,
3062
+ value: checkConfig.type
3063
+ });
3064
+ }
3065
+ if (checkConfig.type === "ai" && !checkConfig.prompt) {
3066
+ errors.push({
3067
+ field: `checks.${checkName}.prompt`,
3068
+ message: `Invalid check configuration for "${checkName}": missing prompt (required for AI checks)`
3069
+ });
3070
+ }
3071
+ try {
3072
+ const externalTypes = /* @__PURE__ */ new Set(["github", "http", "http_client", "http_input", "workflow"]);
3073
+ if (externalTypes.has(checkConfig.type) && !checkConfig.criticality) {
3074
+ errors.push({
3075
+ field: `checks.${checkName}.criticality`,
3076
+ message: `Missing required criticality for step "${checkName}" (type: ${checkConfig.type}). Set criticality: 'external' or 'internal' to enable safe defaults for side-effecting steps.`
3077
+ });
3078
+ }
3079
+ } catch {
3080
+ }
3081
+ try {
3082
+ const crit = checkConfig.criticality || "policy";
3083
+ const isCritical = crit === "external" || crit === "internal";
3084
+ if (isCritical) {
3085
+ const hasAssume = typeof checkConfig.assume === "string" || Array.isArray(checkConfig.assume) && checkConfig.assume.length > 0;
3086
+ const hasIf = typeof checkConfig.if === "string" && checkConfig.if.trim().length > 0;
3087
+ if (!hasAssume && !hasIf) {
3088
+ errors.push({
3089
+ field: `checks.${checkName}.assume`,
3090
+ message: `Critical step "${checkName}" (criticality: ${crit}) requires a precondition: set 'assume:' (preferred) or 'if:' to guard execution.`
3091
+ });
3092
+ }
3093
+ const outputProviders = /* @__PURE__ */ new Set([
3094
+ "ai",
3095
+ "script",
3096
+ "command",
3097
+ "http",
3098
+ "http_client",
3099
+ "http_input"
3100
+ ]);
3101
+ if (outputProviders.has(checkConfig.type)) {
3102
+ const hasSchema = typeof checkConfig.schema !== "undefined";
3103
+ const hasGuarantee = typeof checkConfig.guarantee === "string" && checkConfig.guarantee.trim().length > 0;
3104
+ if (!hasSchema && !hasGuarantee) {
3105
+ errors.push({
3106
+ field: `checks.${checkName}.schema/guarantee`,
3107
+ message: `Critical step "${checkName}" (type: ${checkConfig.type}) requires an output contract: provide 'schema:' (renderer name or JSON Schema) or 'guarantee:' expression.`
3108
+ });
3109
+ }
3110
+ }
3111
+ }
3112
+ } catch {
3113
+ }
3114
+ if (checkConfig.type === "command" && !checkConfig.exec) {
3115
+ errors.push({
3116
+ field: `checks.${checkName}.exec`,
3117
+ message: `Invalid check configuration for "${checkName}": missing exec field (required for command checks)`
3118
+ });
3119
+ }
3120
+ if (checkConfig.type === "http") {
3121
+ if (!checkConfig.url) {
3122
+ errors.push({
3123
+ field: `checks.${checkName}.url`,
3124
+ message: `Invalid check configuration for "${checkName}": missing url field (required for http checks)`
3125
+ });
3126
+ }
3127
+ if (!checkConfig.body) {
3128
+ errors.push({
3129
+ field: `checks.${checkName}.body`,
3130
+ message: `Invalid check configuration for "${checkName}": missing body field (required for http checks)`
3131
+ });
3132
+ }
3133
+ }
3134
+ if (checkConfig.type === "http_input" && !checkConfig.endpoint) {
3135
+ errors.push({
3136
+ field: `checks.${checkName}.endpoint`,
3137
+ message: `Invalid check configuration for "${checkName}": missing endpoint field (required for http_input checks)`
3138
+ });
3139
+ }
3140
+ try {
3141
+ const hasObjSchema = checkConfig?.schema && typeof checkConfig.schema === "object";
3142
+ const hasOutputSchema = checkConfig?.output_schema && typeof checkConfig.output_schema === "object";
3143
+ if (hasObjSchema && hasOutputSchema) {
3144
+ (_warnings || errors).push({
3145
+ field: `checks.${checkName}.schema`,
3146
+ message: `Both 'schema' (object) and 'output_schema' are set; 'schema' will be used for validation. 'output_schema' is deprecated.`
3147
+ });
3148
+ }
3149
+ } catch {
3150
+ }
3151
+ if (checkConfig.type === "http_client" && !checkConfig.url) {
3152
+ errors.push({
3153
+ field: `checks.${checkName}.url`,
3154
+ message: `Invalid check configuration for "${checkName}": missing url field (required for http_client checks)`
3155
+ });
3156
+ }
3157
+ if (checkConfig.schedule) {
3158
+ const cronParts = checkConfig.schedule.split(" ");
3159
+ if (cronParts.length < 5 || cronParts.length > 6) {
3160
+ errors.push({
3161
+ field: `checks.${checkName}.schedule`,
3162
+ message: `Invalid cron expression for "${checkName}": ${checkConfig.schedule}`,
3163
+ value: checkConfig.schedule
3164
+ });
3165
+ }
3166
+ }
3167
+ if (checkConfig.on) {
3168
+ if (!Array.isArray(checkConfig.on)) {
3169
+ errors.push({
3170
+ field: `checks.${checkName}.on`,
3171
+ message: `Invalid check configuration for "${checkName}": 'on' field must be an array`
3172
+ });
3173
+ } else {
3174
+ for (const event of checkConfig.on) {
3175
+ if (!this.validEventTriggers.includes(event)) {
3176
+ errors.push({
3177
+ field: `checks.${checkName}.on`,
3178
+ message: `Invalid event "${event}". Must be one of: ${this.validEventTriggers.join(", ")}`,
3179
+ value: event
3180
+ });
3181
+ }
3182
+ }
3183
+ }
3184
+ }
3185
+ if (checkConfig.reuse_ai_session !== void 0) {
3186
+ const reuseValue = checkConfig.reuse_ai_session;
3187
+ const isString = typeof reuseValue === "string";
3188
+ const isBoolean = typeof reuseValue === "boolean";
3189
+ const isSelf = reuseValue === "self";
3190
+ if (!isString && !isBoolean) {
3191
+ errors.push({
3192
+ field: `checks.${checkName}.reuse_ai_session`,
3193
+ message: `Invalid reuse_ai_session value for "${checkName}": must be string (check name) or boolean`,
3194
+ value: reuseValue
3195
+ });
3196
+ } else if (isString && !isSelf) {
3197
+ const targetCheckName = reuseValue;
3198
+ if (!config?.checks || !config.checks[targetCheckName]) {
3199
+ errors.push({
3200
+ field: `checks.${checkName}.reuse_ai_session`,
3201
+ message: `Check "${checkName}" references non-existent check "${targetCheckName}" for session reuse`,
3202
+ value: reuseValue
3203
+ });
3204
+ }
3205
+ } else if (reuseValue === true) {
3206
+ if (!checkConfig.depends_on || !Array.isArray(checkConfig.depends_on) || checkConfig.depends_on.length === 0) {
3207
+ errors.push({
3208
+ field: `checks.${checkName}.reuse_ai_session`,
3209
+ message: `Check "${checkName}" has reuse_ai_session=true but missing or empty depends_on. Session reuse requires dependency on another check.`,
3210
+ value: reuseValue
3211
+ });
3212
+ }
3213
+ }
3214
+ }
3215
+ if (checkConfig.session_mode !== void 0) {
3216
+ if (checkConfig.session_mode !== "clone" && checkConfig.session_mode !== "append") {
3217
+ errors.push({
3218
+ field: `checks.${checkName}.session_mode`,
3219
+ message: `Invalid session_mode value for "${checkName}": must be 'clone' or 'append'`,
3220
+ value: checkConfig.session_mode
3221
+ });
3222
+ }
3223
+ if (!checkConfig.reuse_ai_session) {
3224
+ errors.push({
3225
+ field: `checks.${checkName}.session_mode`,
3226
+ message: `Check "${checkName}" has session_mode but no reuse_ai_session. session_mode requires reuse_ai_session to be set.`,
3227
+ value: checkConfig.session_mode
3228
+ });
3229
+ }
3230
+ }
3231
+ if (checkConfig.tags !== void 0) {
3232
+ if (!Array.isArray(checkConfig.tags)) {
3233
+ errors.push({
3234
+ field: `checks.${checkName}.tags`,
3235
+ message: `Invalid tags value for "${checkName}": must be an array of strings`,
3236
+ value: checkConfig.tags
3237
+ });
3238
+ } else {
3239
+ const validTagPattern = /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/;
3240
+ checkConfig.tags.forEach((tag, index) => {
3241
+ if (typeof tag !== "string") {
3242
+ errors.push({
3243
+ field: `checks.${checkName}.tags[${index}]`,
3244
+ message: `Invalid tag at index ${index} for "${checkName}": must be a string`,
3245
+ value: tag
3246
+ });
3247
+ } else if (!validTagPattern.test(tag)) {
3248
+ errors.push({
3249
+ field: `checks.${checkName}.tags[${index}]`,
3250
+ message: `Invalid tag "${tag}" for "${checkName}": tags must be alphanumeric with hyphens or underscores (start with alphanumeric)`,
3251
+ value: tag
3252
+ });
3253
+ }
3254
+ });
3255
+ }
3256
+ }
3257
+ if (checkConfig.on_finish !== void 0) {
3258
+ if (!checkConfig.forEach) {
3259
+ errors.push({
3260
+ field: `checks.${checkName}.on_finish`,
3261
+ message: `Check "${checkName}" has on_finish but forEach is not true. on_finish is only valid on forEach checks.`,
3262
+ value: checkConfig.on_finish
3263
+ });
3264
+ }
3265
+ }
3266
+ try {
3267
+ const transformJs = checkConfig.transform_js;
3268
+ if (typeof transformJs === "string" && transformJs.trim().length > 0) {
3269
+ const result = validateJsSyntax(transformJs);
3270
+ if (!result.valid) {
3271
+ errors.push({
3272
+ field: `checks.${checkName}.transform_js`,
3273
+ message: `JavaScript syntax error in "${checkName}" transform_js: ${result.error}`,
3274
+ value: transformJs.slice(0, 100) + (transformJs.length > 100 ? "..." : "")
3275
+ });
3276
+ }
3277
+ }
3278
+ if (checkConfig.type === "script") {
3279
+ const content = checkConfig.content;
3280
+ if (typeof content === "string" && content.trim().length > 0) {
3281
+ const result = validateJsSyntax(content);
3282
+ if (!result.valid) {
3283
+ errors.push({
3284
+ field: `checks.${checkName}.content`,
3285
+ message: `JavaScript syntax error in "${checkName}" script: ${result.error}`,
3286
+ value: content.slice(0, 100) + (content.length > 100 ? "..." : "")
3287
+ });
3288
+ }
3289
+ }
3290
+ }
3291
+ } catch {
3292
+ }
3293
+ }
3294
+ /**
3295
+ * Validate MCP servers object shape and values (basic shape only)
3296
+ */
3297
+ validateMcpServersObject(mcpServers, fieldPrefix, errors, _warnings) {
3298
+ if (typeof mcpServers !== "object" || mcpServers === null) {
3299
+ errors.push({
3300
+ field: fieldPrefix,
3301
+ message: `${fieldPrefix} must be an object mapping server names to { command, args?, env? }`,
3302
+ value: mcpServers
3303
+ });
3304
+ return;
3305
+ }
3306
+ for (const [serverName, cfg] of Object.entries(mcpServers)) {
3307
+ const pathStr = `${fieldPrefix}.${serverName}`;
3308
+ if (!cfg || typeof cfg !== "object") {
3309
+ errors.push({ field: pathStr, message: `${pathStr} must be an object`, value: cfg });
3310
+ continue;
3311
+ }
3312
+ const { command, args, env } = cfg;
3313
+ if (typeof command !== "string" || command.trim() === "") {
3314
+ errors.push({
3315
+ field: `${pathStr}.command`,
3316
+ message: `${pathStr}.command must be a non-empty string`,
3317
+ value: command
3318
+ });
3319
+ }
3320
+ if (args !== void 0 && !Array.isArray(args)) {
3321
+ errors.push({
3322
+ field: `${pathStr}.args`,
3323
+ message: `${pathStr}.args must be an array of strings`,
3324
+ value: args
3325
+ });
3326
+ }
3327
+ if (env !== void 0) {
3328
+ if (typeof env !== "object" || env === null) {
3329
+ errors.push({
3330
+ field: `${pathStr}.env`,
3331
+ message: `${pathStr}.env must be an object of string values`,
3332
+ value: env
3333
+ });
3334
+ } else {
3335
+ for (const [k, v] of Object.entries(env)) {
3336
+ if (typeof v !== "string") {
3337
+ errors.push({
3338
+ field: `${pathStr}.env.${k}`,
3339
+ message: `${pathStr}.env.${k} must be a string`,
3340
+ value: v
3341
+ });
3342
+ }
3343
+ }
3344
+ }
3345
+ }
3346
+ }
3347
+ }
3348
+ /**
3349
+ * Validate configuration using generated JSON Schema via Ajv, if available.
3350
+ * Adds to errors/warnings but does not throw directly.
3351
+ */
3352
+ validateWithAjvSchema(config, errors, warnings) {
3353
+ try {
3354
+ if (!__ajvValidate) {
3355
+ try {
3356
+ const jsonPath = path2.resolve(__dirname, "generated", "config-schema.json");
3357
+ const jsonSchema = __require(jsonPath);
3358
+ if (jsonSchema) {
3359
+ const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: false });
3360
+ addFormats(ajv);
3361
+ const validate = ajv.compile(jsonSchema);
3362
+ __ajvValidate = (data) => validate(data);
3363
+ __ajvErrors = () => validate.errors;
3364
+ }
3365
+ } catch {
3366
+ }
3367
+ if (!__ajvValidate) {
3368
+ try {
3369
+ const mod = (init_config_schema(), __toCommonJS(config_schema_exports));
3370
+ const schema = mod?.configSchema || mod?.default || mod;
3371
+ if (schema) {
3372
+ const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, strict: false });
3373
+ addFormats(ajv);
3374
+ const validate = ajv.compile(schema);
3375
+ __ajvValidate = (data) => validate(data);
3376
+ __ajvErrors = () => validate.errors;
3377
+ } else {
3378
+ return;
3379
+ }
3380
+ } catch {
3381
+ return;
3382
+ }
3383
+ }
3384
+ }
3385
+ const ok = __ajvValidate(config);
3386
+ const errs = __ajvErrors ? __ajvErrors() : null;
3387
+ if (!ok && Array.isArray(errs)) {
3388
+ for (const e of errs) {
3389
+ const pathStr = e.instancePath ? e.instancePath.replace(/^\//, "").replace(/\//g, ".") : "";
3390
+ const msg = e.message || "Invalid configuration";
3391
+ if (e.keyword === "additionalProperties") {
3392
+ const addl = e.params && e.params.additionalProperty || "unknown";
3393
+ const fullField = pathStr ? `${pathStr}.${addl}` : addl;
3394
+ const topLevel = !pathStr;
3395
+ if (topLevel && (addl === "tests" || addl === "slack")) {
3396
+ continue;
3397
+ }
3398
+ warnings.push({
3399
+ field: fullField || "config",
3400
+ message: topLevel ? `Unknown top-level key '${addl}' will be ignored.` : `Unknown key '${addl}' will be ignored`
3401
+ });
3402
+ } else {
3403
+ logger.debug(`Ajv note [${pathStr || "config"}]: ${msg}`);
3404
+ }
3405
+ }
3406
+ }
3407
+ } catch (err) {
3408
+ logger.debug(`Ajv validation skipped: ${err instanceof Error ? err.message : String(err)}`);
3409
+ }
3410
+ }
3411
+ // Unknown-key warnings are fully handled by Ajv using the generated schema
3412
+ // Unknown-key hints are produced by Ajv (additionalProperties=false)
3413
+ /**
3414
+ * Validate tag filter configuration
3415
+ */
3416
+ validateTagFilter(tagFilter, errors) {
3417
+ const validTagPattern = /^[a-zA-Z0-9][a-zA-Z0-9-_]*$/;
3418
+ if (tagFilter.include !== void 0) {
3419
+ if (!Array.isArray(tagFilter.include)) {
3420
+ errors.push({
3421
+ field: "tag_filter.include",
3422
+ message: "tag_filter.include must be an array of strings",
3423
+ value: tagFilter.include
3424
+ });
3425
+ } else {
3426
+ tagFilter.include.forEach((tag, index) => {
3427
+ if (typeof tag !== "string") {
3428
+ errors.push({
3429
+ field: `tag_filter.include[${index}]`,
3430
+ message: `Invalid tag at index ${index}: must be a string`,
3431
+ value: tag
3432
+ });
3433
+ } else if (!validTagPattern.test(tag)) {
3434
+ errors.push({
3435
+ field: `tag_filter.include[${index}]`,
3436
+ message: `Invalid tag "${tag}": tags must be alphanumeric with hyphens or underscores`,
3437
+ value: tag
3438
+ });
3439
+ }
3440
+ });
3441
+ }
3442
+ }
3443
+ if (tagFilter.exclude !== void 0) {
3444
+ if (!Array.isArray(tagFilter.exclude)) {
3445
+ errors.push({
3446
+ field: "tag_filter.exclude",
3447
+ message: "tag_filter.exclude must be an array of strings",
3448
+ value: tagFilter.exclude
3449
+ });
3450
+ } else {
3451
+ tagFilter.exclude.forEach((tag, index) => {
3452
+ if (typeof tag !== "string") {
3453
+ errors.push({
3454
+ field: `tag_filter.exclude[${index}]`,
3455
+ message: `Invalid tag at index ${index}: must be a string`,
3456
+ value: tag
3457
+ });
3458
+ } else if (!validTagPattern.test(tag)) {
3459
+ errors.push({
3460
+ field: `tag_filter.exclude[${index}]`,
3461
+ message: `Invalid tag "${tag}": tags must be alphanumeric with hyphens or underscores`,
3462
+ value: tag
3463
+ });
3464
+ }
3465
+ });
3466
+ }
3467
+ }
3468
+ }
3469
+ /**
3470
+ * Validate HTTP server configuration
3471
+ */
3472
+ validateHttpServerConfig(httpServerConfig, errors) {
3473
+ if (typeof httpServerConfig.enabled !== "boolean") {
3474
+ errors.push({
3475
+ field: "http_server.enabled",
3476
+ message: "http_server.enabled must be a boolean",
3477
+ value: httpServerConfig.enabled
3478
+ });
3479
+ }
3480
+ if (httpServerConfig.enabled === true) {
3481
+ if (typeof httpServerConfig.port !== "number" || httpServerConfig.port < 1 || httpServerConfig.port > 65535) {
3482
+ errors.push({
3483
+ field: "http_server.port",
3484
+ message: "http_server.port must be a number between 1 and 65535",
3485
+ value: httpServerConfig.port
3486
+ });
3487
+ }
3488
+ if (httpServerConfig.auth) {
3489
+ const auth = httpServerConfig.auth;
3490
+ const validAuthTypes = ["bearer_token", "hmac", "basic", "none"];
3491
+ if (!auth.type || !validAuthTypes.includes(auth.type)) {
3492
+ errors.push({
3493
+ field: "http_server.auth.type",
3494
+ message: `Invalid auth type. Must be one of: ${validAuthTypes.join(", ")}`,
3495
+ value: auth.type
3496
+ });
3497
+ }
3498
+ }
3499
+ if (httpServerConfig.tls && typeof httpServerConfig.tls === "object") {
3500
+ const tls = httpServerConfig.tls;
3501
+ if (tls.enabled === true) {
3502
+ if (!tls.cert) {
3503
+ errors.push({
3504
+ field: "http_server.tls.cert",
3505
+ message: "TLS certificate is required when TLS is enabled"
3506
+ });
3507
+ }
3508
+ if (!tls.key) {
3509
+ errors.push({
3510
+ field: "http_server.tls.key",
3511
+ message: "TLS key is required when TLS is enabled"
3512
+ });
3513
+ }
3514
+ }
3515
+ }
3516
+ if (httpServerConfig.endpoints && Array.isArray(httpServerConfig.endpoints)) {
3517
+ for (let i = 0; i < httpServerConfig.endpoints.length; i++) {
3518
+ const endpoint = httpServerConfig.endpoints[i];
3519
+ if (!endpoint.path || typeof endpoint.path !== "string") {
3520
+ errors.push({
3521
+ field: `http_server.endpoints[${i}].path`,
3522
+ message: "Endpoint path must be a string",
3523
+ value: endpoint.path
3524
+ });
3525
+ }
3526
+ }
3527
+ }
3528
+ }
3529
+ }
3530
+ /**
3531
+ * Validate output configuration
3532
+ */
3533
+ validateOutputConfig(outputConfig, errors) {
3534
+ if (outputConfig.pr_comment) {
3535
+ const prComment = outputConfig.pr_comment;
3536
+ if (typeof prComment.format === "string" && !this.validOutputFormats.includes(prComment.format)) {
3537
+ errors.push({
3538
+ field: "output.pr_comment.format",
3539
+ message: `Invalid output format "${prComment.format}". Must be one of: ${this.validOutputFormats.join(", ")}`,
3540
+ value: prComment.format
3541
+ });
3542
+ }
3543
+ if (typeof prComment.group_by === "string" && !this.validGroupByOptions.includes(prComment.group_by)) {
3544
+ errors.push({
3545
+ field: "output.pr_comment.group_by",
3546
+ message: `Invalid group_by option "${prComment.group_by}". Must be one of: ${this.validGroupByOptions.join(", ")}`,
3547
+ value: prComment.group_by
3548
+ });
3549
+ }
3550
+ }
3551
+ }
3552
+ /**
3553
+ * Check if remote extends are allowed
3554
+ */
3555
+ isRemoteExtendsAllowed() {
3556
+ if (process.env.VISOR_NO_REMOTE_EXTENDS === "true" || process.env.VISOR_NO_REMOTE_EXTENDS === "1") {
3557
+ return false;
3558
+ }
3559
+ return true;
3560
+ }
3561
+ /**
3562
+ * Merge configuration with default values
3563
+ */
3564
+ mergeWithDefaults(config) {
3565
+ const defaultConfig = {
3566
+ version: "1.0",
3567
+ checks: {},
3568
+ max_parallelism: 3,
3569
+ output: {
3570
+ pr_comment: {
3571
+ format: "markdown",
3572
+ group_by: "check",
3573
+ collapse: true
3574
+ }
3575
+ }
3576
+ };
3577
+ const merged = { ...defaultConfig, ...config };
3578
+ if (merged.output) {
3579
+ merged.output.pr_comment = {
3580
+ ...defaultConfig.output.pr_comment,
3581
+ ...merged.output.pr_comment
3582
+ };
3583
+ } else {
3584
+ merged.output = defaultConfig.output;
3585
+ }
3586
+ return merged;
3587
+ }
3588
+ };
3589
+ __ajvValidate = null;
3590
+ __ajvErrors = null;
3591
+ }
3592
+ });
3593
+
3594
+ export {
3595
+ VALID_EVENT_TRIGGERS,
3596
+ ConfigManager,
3597
+ config_exports,
3598
+ init_config
3599
+ };
3600
+ //# sourceMappingURL=chunk-5LI6T4O3.mjs.map