bosun 0.41.2 → 0.41.4

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 (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
@@ -0,0 +1,493 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import {
4
+ normalizeExecutorKey,
5
+ getModelsForExecutor,
6
+ MODEL_ALIASES,
7
+ } from "../task/task-complexity.mjs";
8
+ import { CONFIG_FILES } from "./config-file-names.mjs";
9
+
10
+ function parseListValue(value) {
11
+ if (Array.isArray(value)) {
12
+ return value
13
+ .map((item) => String(item || "").trim())
14
+ .filter(Boolean);
15
+ }
16
+ return String(value || "")
17
+ .split(/[,|]/)
18
+ .map((item) => item.trim())
19
+ .filter(Boolean);
20
+ }
21
+
22
+ function inferExecutorModelsFromVariant(executor, variant) {
23
+ const normalizedExecutor = normalizeExecutorKey(executor);
24
+ if (!normalizedExecutor) return [];
25
+ const normalizedVariant = String(variant || "DEFAULT")
26
+ .trim()
27
+ .toUpperCase();
28
+ if (!normalizedVariant || normalizedVariant === "DEFAULT") return [];
29
+
30
+ const known = getModelsForExecutor(normalizedExecutor);
31
+ const inferred = known.filter((model) => {
32
+ const alias = MODEL_ALIASES[model];
33
+ return (
34
+ String(alias?.variant || "")
35
+ .trim()
36
+ .toUpperCase() === normalizedVariant
37
+ );
38
+ });
39
+ if (inferred.length > 0) return inferred;
40
+
41
+ // Fallback for variants encoded as model slug with underscores.
42
+ const slugGuess = normalizedVariant.toLowerCase().replaceAll("_", "-");
43
+ if (known.includes(slugGuess)) return [slugGuess];
44
+
45
+ return [];
46
+ }
47
+
48
+ function normalizeExecutorModels(executor, models, variant = "DEFAULT") {
49
+ const normalizedExecutor = normalizeExecutorKey(executor);
50
+ if (!normalizedExecutor) return [];
51
+ const input = parseListValue(models);
52
+ const known = new Set(getModelsForExecutor(normalizedExecutor));
53
+ if (input.length === 0) {
54
+ const inferred = inferExecutorModelsFromVariant(
55
+ normalizedExecutor,
56
+ variant,
57
+ );
58
+ return inferred.length > 0 ? inferred : [...known];
59
+ }
60
+ // Preserve custom/deployment slugs in addition to known models so user-provided
61
+ // model routing survives normalization (for example Azure deployment names).
62
+ return [...new Set(input.filter(Boolean))];
63
+ }
64
+
65
+ function normalizeExecutorEntry(entry, index = 0, total = 1) {
66
+ if (!entry || typeof entry !== "object") return null;
67
+ const executorType = String(entry.executor || "").trim().toUpperCase();
68
+ if (!executorType) return null;
69
+ const variant = String(entry.variant || "DEFAULT").trim() || "DEFAULT";
70
+ const normalized = normalizeExecutorKey(executorType) || "codex";
71
+ const weight = Number(entry.weight);
72
+ const safeWeight = Number.isFinite(weight) ? weight : Math.floor(100 / Math.max(1, total));
73
+ const role =
74
+ String(entry.role || "").trim() ||
75
+ (index === 0 ? "primary" : index === 1 ? "backup" : `executor-${index + 1}`);
76
+ const name =
77
+ String(entry.name || "").trim() ||
78
+ `${normalized}-${String(variant || "default").toLowerCase()}`;
79
+ const models = normalizeExecutorModels(executorType, entry.models, variant);
80
+ const codexProfile = String(
81
+ entry.codexProfile || entry.modelProfile || "",
82
+ ).trim();
83
+
84
+ // Provider configuration for the executor (e.g. opencode with specific provider)
85
+ const provider = String(entry.provider || "").trim() || null;
86
+ const providerConfig = entry.providerConfig && typeof entry.providerConfig === "object"
87
+ ? { ...entry.providerConfig }
88
+ : null;
89
+
90
+ return {
91
+ name,
92
+ executor: executorType,
93
+ variant,
94
+ weight: safeWeight,
95
+ role,
96
+ enabled: entry.enabled !== false,
97
+ models,
98
+ codexProfile,
99
+ provider,
100
+ providerConfig,
101
+ };
102
+ }
103
+
104
+
105
+ const DEFAULT_EXECUTORS = {
106
+ executors: [
107
+ {
108
+ name: "codex-default",
109
+ executor: "CODEX",
110
+ variant: "DEFAULT",
111
+ weight: 100,
112
+ role: "primary",
113
+ enabled: true,
114
+ },
115
+ ],
116
+ failover: {
117
+ strategy: "next-in-line",
118
+ maxRetries: 3,
119
+ cooldownMinutes: 5,
120
+ disableOnConsecutiveFailures: 3,
121
+ },
122
+ distribution: "primary-only",
123
+ };
124
+
125
+ function parseExecutorsFromEnv() {
126
+ // EXECUTORS=CODEX:DEFAULT:100:gpt-5.2-codex|gpt-5.1-codex-mini
127
+ const raw = process.env.EXECUTORS;
128
+ if (!raw) return null;
129
+ const entries = raw.split(",").map((e) => e.trim());
130
+ const executors = [];
131
+ const roles = ["primary", "backup", "tertiary"];
132
+ for (let i = 0; i < entries.length; i++) {
133
+ const parts = entries[i].split(":");
134
+ if (parts.length < 2) continue;
135
+ const executorType = parts[0].toUpperCase();
136
+ const models = normalizeExecutorModels(
137
+ executorType,
138
+ parts[3] || "",
139
+ parts[1] || "DEFAULT",
140
+ );
141
+ executors.push({
142
+ name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
143
+ executor: executorType,
144
+ variant: parts[1],
145
+ weight: parts[2] ? Number(parts[2]) : Math.floor(100 / entries.length),
146
+ role: roles[i] || `executor-${i + 1}`,
147
+ enabled: true,
148
+ models,
149
+ });
150
+ }
151
+ return executors.length ? executors : null;
152
+ }
153
+
154
+
155
+ function findExecutorMetadataMatch(entry, candidates, index = 0) {
156
+ const entryExecutor = normalizeExecutorKey(entry?.executor);
157
+ const entryVariant = String(entry?.variant || "DEFAULT")
158
+ .trim()
159
+ .toUpperCase();
160
+ const entryRole = String(entry?.role || "")
161
+ .trim()
162
+ .toLowerCase();
163
+
164
+ const exact = candidates.find((candidate) =>
165
+ normalizeExecutorKey(candidate?.executor) === entryExecutor &&
166
+ String(candidate?.variant || "DEFAULT").trim().toUpperCase() === entryVariant &&
167
+ String(candidate?.role || "").trim().toLowerCase() === entryRole
168
+ );
169
+ if (exact) return exact;
170
+
171
+ const byExecutorAndVariant = candidates.find((candidate) =>
172
+ normalizeExecutorKey(candidate?.executor) === entryExecutor &&
173
+ String(candidate?.variant || "DEFAULT").trim().toUpperCase() === entryVariant
174
+ );
175
+ if (byExecutorAndVariant) return byExecutorAndVariant;
176
+
177
+ return candidates[index] || null;
178
+ }
179
+
180
+ export function loadExecutorConfig(configDir, configData) {
181
+ // 1. Try env var
182
+ const fromEnv = parseExecutorsFromEnv();
183
+
184
+ // 2. Try config file
185
+ let fromFile = null;
186
+ if (configData && typeof configData === "object") {
187
+ fromFile = configData.executors ? configData : null;
188
+ }
189
+ if (!fromFile) {
190
+ for (const name of CONFIG_FILES) {
191
+ const p = resolve(configDir, name);
192
+ if (existsSync(p)) {
193
+ try {
194
+ const raw = JSON.parse(readFileSync(p, "utf8"));
195
+ fromFile = raw.executors ? raw : null;
196
+ break;
197
+ } catch {
198
+ /* invalid JSON — skip */
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ const baseExecutors =
205
+ fromEnv || fromFile?.executors || DEFAULT_EXECUTORS.executors;
206
+ const executors = (Array.isArray(baseExecutors) ? baseExecutors : [])
207
+ .map((entry, index, arr) => normalizeExecutorEntry(entry, index, arr.length))
208
+ .filter(Boolean);
209
+
210
+ // Preserve file-defined metadata (for example codexProfile) even when
211
+ // execution topology comes from EXECUTORS env.
212
+ if (fromEnv && Array.isArray(fromFile?.executors) && executors.length > 0) {
213
+ const fileExecutors = fromFile.executors
214
+ .map((entry, index, arr) => normalizeExecutorEntry(entry, index, arr.length))
215
+ .filter(Boolean);
216
+
217
+ for (let index = 0; index < executors.length; index++) {
218
+ const current = executors[index];
219
+ const match = findExecutorMetadataMatch(current, fileExecutors, index);
220
+ if (!match) continue;
221
+ const merged = { ...current };
222
+ if (typeof match.name === "string" && match.name.trim()) {
223
+ merged.name = match.name.trim();
224
+ }
225
+ if (typeof match.enabled === "boolean") {
226
+ merged.enabled = match.enabled;
227
+ }
228
+ if (Array.isArray(match.models) && match.models.length > 0) {
229
+ merged.models = [...new Set(match.models)];
230
+ }
231
+ if (match.codexProfile) {
232
+ merged.codexProfile = match.codexProfile;
233
+ }
234
+ executors[index] = {
235
+ ...merged,
236
+ };
237
+ }
238
+ }
239
+ const failover = fromFile?.failover || {
240
+ strategy:
241
+ process.env.FAILOVER_STRATEGY || DEFAULT_EXECUTORS.failover.strategy,
242
+ maxRetries: Number(
243
+ process.env.FAILOVER_MAX_RETRIES || DEFAULT_EXECUTORS.failover.maxRetries,
244
+ ),
245
+ cooldownMinutes: Number(
246
+ process.env.FAILOVER_COOLDOWN_MIN ||
247
+ DEFAULT_EXECUTORS.failover.cooldownMinutes,
248
+ ),
249
+ disableOnConsecutiveFailures: Number(
250
+ process.env.FAILOVER_DISABLE_AFTER ||
251
+ DEFAULT_EXECUTORS.failover.disableOnConsecutiveFailures,
252
+ ),
253
+ };
254
+ const distribution =
255
+ fromFile?.distribution ||
256
+ process.env.EXECUTOR_DISTRIBUTION ||
257
+ DEFAULT_EXECUTORS.distribution;
258
+
259
+ return { executors, failover, distribution };
260
+ }
261
+
262
+ // ── Executor Scheduler ───────────────────────────────────────────────────────
263
+
264
+ export class ExecutorScheduler {
265
+ constructor(config) {
266
+ this.executors = config.executors.filter((e) => e.enabled !== false);
267
+ this.failover = config.failover;
268
+ this.distribution = config.distribution;
269
+ this._roundRobinIndex = 0;
270
+ this._failureCounts = new Map(); // name → consecutive failures
271
+ this._disabledUntil = new Map(); // name → timestamp
272
+ this._workspaceActiveCount = new Map(); // workspaceId → current active executor count
273
+ this._workspaceConfigs = new Map(); // workspaceId → { maxConcurrent, pool, weight }
274
+ }
275
+
276
+ /**
277
+ * Register workspace executor config for concurrency tracking.
278
+ * @param {string} workspaceId
279
+ * @param {{ maxConcurrent?: number, pool?: string, weight?: number }} wsExecutorConfig
280
+ */
281
+ registerWorkspace(workspaceId, wsExecutorConfig = {}) {
282
+ if (!workspaceId) return;
283
+ this._workspaceConfigs.set(workspaceId, {
284
+ maxConcurrent: wsExecutorConfig.maxConcurrent ?? 3,
285
+ pool: wsExecutorConfig.pool ?? "shared",
286
+ weight: wsExecutorConfig.weight ?? 1.0,
287
+ executors: wsExecutorConfig.executors ?? null,
288
+ });
289
+ if (!this._workspaceActiveCount.has(workspaceId)) {
290
+ this._workspaceActiveCount.set(workspaceId, 0);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Check if a workspace has available executor slots.
296
+ * @param {string} [workspaceId]
297
+ * @returns {boolean}
298
+ */
299
+ hasAvailableSlot(workspaceId) {
300
+ if (!workspaceId) return true; // no workspace scope — always available
301
+ const config = this._workspaceConfigs.get(workspaceId);
302
+ if (!config) return true; // no config registered — no limit
303
+ const active = this._workspaceActiveCount.get(workspaceId) || 0;
304
+ return active < config.maxConcurrent;
305
+ }
306
+
307
+ /**
308
+ * Acquire an executor slot for a workspace.
309
+ * @param {string} [workspaceId]
310
+ * @returns {boolean} true if slot acquired, false if at limit
311
+ */
312
+ acquireSlot(workspaceId) {
313
+ if (!workspaceId) return true;
314
+ if (!this.hasAvailableSlot(workspaceId)) return false;
315
+ this._workspaceActiveCount.set(
316
+ workspaceId,
317
+ (this._workspaceActiveCount.get(workspaceId) || 0) + 1,
318
+ );
319
+ return true;
320
+ }
321
+
322
+ /**
323
+ * Release an executor slot for a workspace.
324
+ * @param {string} [workspaceId]
325
+ */
326
+ releaseSlot(workspaceId) {
327
+ if (!workspaceId) return;
328
+ const current = this._workspaceActiveCount.get(workspaceId) || 0;
329
+ this._workspaceActiveCount.set(workspaceId, Math.max(0, current - 1));
330
+ }
331
+
332
+ /**
333
+ * Get workspace executor usage summary.
334
+ * @returns {Array<{ workspaceId: string, active: number, maxConcurrent: number, pool: string, weight: number }>}
335
+ */
336
+ getWorkspaceSummary() {
337
+ const result = [];
338
+ for (const [wsId, config] of this._workspaceConfigs) {
339
+ result.push({
340
+ workspaceId: wsId,
341
+ active: this._workspaceActiveCount.get(wsId) || 0,
342
+ ...config,
343
+ });
344
+ }
345
+ return result;
346
+ }
347
+
348
+ /** Get the next executor based on distribution strategy */
349
+ next(workspaceId) {
350
+ // Check workspace slot availability before selecting
351
+ if (workspaceId && !this.hasAvailableSlot(workspaceId)) {
352
+ return null; // workspace at executor capacity
353
+ }
354
+
355
+ const available = this._getAvailable();
356
+ if (!available.length) {
357
+ // All disabled — reset and use primary
358
+ this._disabledUntil.clear();
359
+ this._failureCounts.clear();
360
+ return this.executors[0];
361
+ }
362
+
363
+ // For dedicated pools, filter to workspace-assigned executors
364
+ if (workspaceId) {
365
+ const wsConfig = this._workspaceConfigs.get(workspaceId);
366
+ if (wsConfig?.pool === "dedicated" && wsConfig.executors) {
367
+ const dedicated = available.filter((e) =>
368
+ wsConfig.executors.includes(e.name),
369
+ );
370
+ if (dedicated.length) {
371
+ return this._selectByStrategy(dedicated);
372
+ }
373
+ }
374
+ }
375
+
376
+ return this._selectByStrategy(available);
377
+ }
378
+
379
+ _selectByStrategy(available) {
380
+ switch (this.distribution) {
381
+ case "round-robin":
382
+ return this._roundRobin(available);
383
+ case "primary-only":
384
+ return available[0];
385
+ case "weighted":
386
+ default:
387
+ return this._weightedSelect(available);
388
+ }
389
+ }
390
+
391
+ /** Report a failure for an executor */
392
+ recordFailure(executorName) {
393
+ const count = (this._failureCounts.get(executorName) || 0) + 1;
394
+ this._failureCounts.set(executorName, count);
395
+ if (count >= this.failover.disableOnConsecutiveFailures) {
396
+ const until = Date.now() + this.failover.cooldownMinutes * 60 * 1000;
397
+ this._disabledUntil.set(executorName, until);
398
+ this._failureCounts.set(executorName, 0);
399
+ }
400
+ }
401
+
402
+ /** Report a success for an executor */
403
+ recordSuccess(executorName) {
404
+ this._failureCounts.set(executorName, 0);
405
+ this._disabledUntil.delete(executorName);
406
+ }
407
+
408
+ /** Get failover executor when current one fails */
409
+ getFailover(currentName) {
410
+ const available = this._getAvailable().filter(
411
+ (e) => e.name !== currentName,
412
+ );
413
+ if (!available.length) return null;
414
+
415
+ switch (this.failover.strategy) {
416
+ case "weighted-random":
417
+ return this._weightedSelect(available);
418
+ case "round-robin":
419
+ return available[0];
420
+ case "next-in-line":
421
+ default: {
422
+ // Find the next one by role priority
423
+ const roleOrder = [
424
+ "primary",
425
+ "backup",
426
+ "tertiary",
427
+ ...Array.from({ length: 20 }, (_, i) => `executor-${i + 1}`),
428
+ ];
429
+ available.sort(
430
+ (a, b) => roleOrder.indexOf(a.role) - roleOrder.indexOf(b.role),
431
+ );
432
+ return available[0];
433
+ }
434
+ }
435
+ }
436
+
437
+ /** Get summary for display */
438
+ getSummary() {
439
+ const total = this.executors.reduce((s, e) => s + e.weight, 0);
440
+ return this.executors.map((e) => {
441
+ const pct = total > 0 ? Math.round((e.weight / total) * 100) : 0;
442
+ const disabled = this._isDisabled(e.name);
443
+ return {
444
+ ...e,
445
+ percentage: pct,
446
+ status: disabled ? "cooldown" : e.enabled ? "active" : "disabled",
447
+ consecutiveFailures: this._failureCounts.get(e.name) || 0,
448
+ };
449
+ });
450
+ }
451
+
452
+ /** Format a display string like "COPILOT ⇄ CODEX (50/50)" */
453
+ toDisplayString() {
454
+ const summary = this.getSummary().filter((e) => e.status === "active");
455
+ if (!summary.length) return "No executors available";
456
+ return summary
457
+ .map((e) => `${e.executor}:${e.variant}(${e.percentage}%)`)
458
+ .join(" ⇄ ");
459
+ }
460
+
461
+ _getAvailable() {
462
+ return this.executors.filter(
463
+ (e) => e.enabled !== false && !this._isDisabled(e.name),
464
+ );
465
+ }
466
+
467
+ _isDisabled(name) {
468
+ const until = this._disabledUntil.get(name);
469
+ if (!until) return false;
470
+ if (Date.now() >= until) {
471
+ this._disabledUntil.delete(name);
472
+ return false;
473
+ }
474
+ return true;
475
+ }
476
+
477
+ _roundRobin(available) {
478
+ const idx = this._roundRobinIndex % available.length;
479
+ this._roundRobinIndex++;
480
+ return available[idx];
481
+ }
482
+
483
+ _weightedSelect(available) {
484
+ const totalWeight = available.reduce((s, e) => s + (e.weight || 1), 0);
485
+ let r = Math.random() * totalWeight;
486
+ for (const e of available) {
487
+ r -= e.weight || 1;
488
+ if (r <= 0) return e;
489
+ }
490
+ return available[available.length - 1];
491
+ }
492
+ }
493
+
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { resolve, dirname, isAbsolute } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { CONFIG_FILES } from "./config-file-names.mjs";
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
8
 
@@ -130,7 +131,6 @@ export function resolveRepoRoot(options = {}) {
130
131
  }
131
132
 
132
133
  // Check bosun config for workspace repos
133
- const CONFIG_FILES = ["bosun.config.json", ".bosun.json", "bosun.json"];
134
134
  const configDirs = [...getConfigSearchDirs(), __dirname];
135
135
  let fallbackRepo = null;
136
136
  for (const cfgName of CONFIG_FILES) {
@@ -192,7 +192,6 @@ export function resolveAgentRepoRoot(options = {}) {
192
192
  * @returns {string|null}
193
193
  */
194
194
  function _resolveWorkspacePrimaryRepo() {
195
- const CONFIG_FILES = ["bosun.config.json", ".bosun.json", "bosun.json"];
196
195
  const configDirs = [...getConfigSearchDirs(), __dirname];
197
196
  for (const cfgName of CONFIG_FILES) {
198
197
  for (const dir of configDirs) {