@dotsetlabs/bellwether 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +8 -2
  3. package/dist/baseline/accessors.d.ts +1 -1
  4. package/dist/baseline/accessors.js +1 -3
  5. package/dist/baseline/baseline-format.d.ts +287 -0
  6. package/dist/baseline/baseline-format.js +12 -0
  7. package/dist/baseline/comparator.js +249 -11
  8. package/dist/baseline/converter.d.ts +15 -15
  9. package/dist/baseline/converter.js +46 -34
  10. package/dist/baseline/diff.d.ts +1 -1
  11. package/dist/baseline/diff.js +45 -28
  12. package/dist/baseline/error-analyzer.d.ts +1 -1
  13. package/dist/baseline/error-analyzer.js +90 -17
  14. package/dist/baseline/incremental-checker.js +8 -5
  15. package/dist/baseline/index.d.ts +2 -12
  16. package/dist/baseline/index.js +3 -23
  17. package/dist/baseline/performance-tracker.d.ts +0 -1
  18. package/dist/baseline/performance-tracker.js +13 -20
  19. package/dist/baseline/response-fingerprint.js +39 -2
  20. package/dist/baseline/saver.js +41 -10
  21. package/dist/baseline/schema-compare.d.ts +22 -0
  22. package/dist/baseline/schema-compare.js +259 -16
  23. package/dist/baseline/types.d.ts +10 -7
  24. package/dist/cache/response-cache.d.ts +8 -0
  25. package/dist/cache/response-cache.js +110 -0
  26. package/dist/cli/commands/check.js +23 -6
  27. package/dist/cli/commands/explore.js +34 -14
  28. package/dist/cli/index.js +8 -0
  29. package/dist/config/template.js +8 -7
  30. package/dist/config/validator.d.ts +59 -59
  31. package/dist/config/validator.js +245 -90
  32. package/dist/constants/core.d.ts +4 -0
  33. package/dist/constants/core.js +8 -19
  34. package/dist/constants/registry.d.ts +17 -0
  35. package/dist/constants/registry.js +18 -0
  36. package/dist/constants/testing.d.ts +0 -369
  37. package/dist/constants/testing.js +18 -456
  38. package/dist/constants.d.ts +1 -1
  39. package/dist/constants.js +1 -1
  40. package/dist/docs/contract.js +131 -83
  41. package/dist/docs/report.js +8 -5
  42. package/dist/interview/insights.d.ts +17 -0
  43. package/dist/interview/insights.js +52 -0
  44. package/dist/interview/interviewer.js +52 -10
  45. package/dist/interview/prompt-test-generator.d.ts +12 -0
  46. package/dist/interview/prompt-test-generator.js +77 -0
  47. package/dist/interview/resource-test-generator.d.ts +12 -0
  48. package/dist/interview/resource-test-generator.js +20 -0
  49. package/dist/interview/schema-inferrer.js +26 -4
  50. package/dist/interview/schema-test-generator.js +278 -31
  51. package/dist/interview/stateful-test-runner.d.ts +3 -0
  52. package/dist/interview/stateful-test-runner.js +80 -0
  53. package/dist/interview/types.d.ts +12 -0
  54. package/dist/transport/mcp-client.js +1 -1
  55. package/dist/transport/sse-transport.d.ts +7 -3
  56. package/dist/transport/sse-transport.js +157 -67
  57. package/dist/version.js +1 -1
  58. package/man/bellwether.1 +1 -1
  59. package/man/bellwether.1.md +2 -2
  60. package/package.json +1 -1
  61. package/schemas/bellwether-check.schema.json +185 -0
  62. package/schemas/bellwether-explore.schema.json +837 -0
  63. package/scripts/completions/bellwether.bash +10 -4
  64. package/scripts/completions/bellwether.zsh +55 -2
@@ -3,6 +3,8 @@
3
3
  * Enables reuse of tool call results and LLM analysis across personas.
4
4
  */
5
5
  import { createHash } from 'crypto';
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
6
8
  import { getLogger } from '../logging/logger.js';
7
9
  import { TIME_CONSTANTS, CACHE } from '../constants.js';
8
10
  const logger = getLogger('response-cache');
@@ -18,13 +20,19 @@ export class ResponseCache {
18
20
  evictions: 0,
19
21
  };
20
22
  totalSizeBytes = 0;
23
+ cacheDir;
21
24
  constructor(config = {}) {
22
25
  this.config = {
23
26
  defaultTTLMs: config.defaultTTLMs ?? TIME_CONSTANTS.DEFAULT_CACHE_TTL,
24
27
  maxEntries: config.maxEntries ?? CACHE.MAX_ENTRIES,
25
28
  maxSizeBytes: config.maxSizeBytes ?? 50 * 1024 * 1024, // 50MB
26
29
  enabled: config.enabled ?? true,
30
+ dir: config.dir ?? '',
27
31
  };
32
+ this.cacheDir = this.config.enabled ? this.config.dir || undefined : undefined;
33
+ if (this.cacheDir) {
34
+ this.ensureCacheDir(this.cacheDir);
35
+ }
28
36
  }
29
37
  /**
30
38
  * Generate a cache key from input data.
@@ -43,6 +51,13 @@ export class ResponseCache {
43
51
  }
44
52
  const entry = this.cache.get(key);
45
53
  if (!entry) {
54
+ const diskEntry = this.loadFromDisk(key);
55
+ if (diskEntry) {
56
+ this.cache.set(key, diskEntry);
57
+ this.totalSizeBytes += this.estimateSize(diskEntry.value);
58
+ this.stats.hits++;
59
+ return diskEntry.value;
60
+ }
46
61
  this.stats.misses++;
47
62
  return undefined;
48
63
  }
@@ -89,6 +104,7 @@ export class ResponseCache {
89
104
  this.totalSizeBytes += entrySize;
90
105
  this.cache.set(key, entry);
91
106
  logger.debug({ key, ttlMs: ttl, description: options?.description }, 'Cache entry set');
107
+ this.saveToDisk(entry);
92
108
  }
93
109
  /**
94
110
  * Check if key exists and is not expired.
@@ -115,8 +131,10 @@ export class ResponseCache {
115
131
  if (entry) {
116
132
  this.totalSizeBytes -= this.estimateSize(entry.value);
117
133
  this.cache.delete(key);
134
+ this.deleteFromDisk(key);
118
135
  return true;
119
136
  }
137
+ this.deleteFromDisk(key);
120
138
  return false;
121
139
  }
122
140
  /**
@@ -125,6 +143,16 @@ export class ResponseCache {
125
143
  clear() {
126
144
  this.cache.clear();
127
145
  this.totalSizeBytes = 0;
146
+ if (this.cacheDir && existsSync(this.cacheDir)) {
147
+ try {
148
+ for (const file of listCacheFiles(this.cacheDir)) {
149
+ unlinkSync(file);
150
+ }
151
+ }
152
+ catch {
153
+ // Ignore disk cleanup errors
154
+ }
155
+ }
128
156
  logger.debug('Cache cleared');
129
157
  }
130
158
  /**
@@ -195,6 +223,88 @@ export class ResponseCache {
195
223
  return 1000; // Default estimate for non-serializable values
196
224
  }
197
225
  }
226
+ ensureCacheDir(dir) {
227
+ try {
228
+ if (!existsSync(dir)) {
229
+ mkdirSync(dir, { recursive: true });
230
+ }
231
+ }
232
+ catch (error) {
233
+ logger.warn({ dir, error: String(error) }, 'Failed to create cache directory');
234
+ this.cacheDir = undefined;
235
+ }
236
+ }
237
+ getCachePath(key) {
238
+ if (!this.cacheDir)
239
+ return null;
240
+ return join(this.cacheDir, `${key}.json`);
241
+ }
242
+ saveToDisk(entry) {
243
+ const path = this.getCachePath(entry.key);
244
+ if (!path)
245
+ return;
246
+ try {
247
+ const serialized = JSON.stringify({
248
+ ...entry,
249
+ createdAt: entry.createdAt.toISOString(),
250
+ lastAccessedAt: entry.lastAccessedAt.toISOString(),
251
+ expiresAt: entry.expiresAt.toISOString(),
252
+ });
253
+ writeFileSync(path, serialized, 'utf-8');
254
+ }
255
+ catch (error) {
256
+ logger.debug({ key: entry.key, error: String(error) }, 'Failed to persist cache entry');
257
+ }
258
+ }
259
+ loadFromDisk(key) {
260
+ const path = this.getCachePath(key);
261
+ if (!path || !existsSync(path))
262
+ return null;
263
+ try {
264
+ const raw = readFileSync(path, 'utf-8');
265
+ const parsed = JSON.parse(raw);
266
+ const entry = {
267
+ ...parsed,
268
+ createdAt: new Date(parsed.createdAt),
269
+ lastAccessedAt: new Date(parsed.lastAccessedAt),
270
+ expiresAt: new Date(parsed.expiresAt),
271
+ };
272
+ if (new Date() > entry.expiresAt) {
273
+ this.deleteFromDisk(key);
274
+ return null;
275
+ }
276
+ entry.hitCount = (entry.hitCount ?? 0) + 1;
277
+ entry.lastAccessedAt = new Date();
278
+ this.saveToDisk(entry);
279
+ return entry;
280
+ }
281
+ catch (error) {
282
+ logger.debug({ key, error: String(error) }, 'Failed to load cache entry');
283
+ return null;
284
+ }
285
+ }
286
+ deleteFromDisk(key) {
287
+ const path = this.getCachePath(key);
288
+ if (!path || !existsSync(path))
289
+ return;
290
+ try {
291
+ unlinkSync(path);
292
+ }
293
+ catch {
294
+ // Ignore delete errors
295
+ }
296
+ }
297
+ }
298
+ function listCacheFiles(dir) {
299
+ try {
300
+ const entries = readdirSync(dir, { withFileTypes: true });
301
+ return entries
302
+ .filter((entry) => entry.isFile())
303
+ .map((entry) => join(dir, entry.name));
304
+ }
305
+ catch {
306
+ return [];
307
+ }
198
308
  }
199
309
  /**
200
310
  * Stable, deterministic JSON stringify with deep key sorting.
@@ -26,6 +26,7 @@ import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE,
26
26
  import * as output from '../output.js';
27
27
  import { extractServerContextFromArgs } from '../utils/server-context.js';
28
28
  import { configureLogger } from '../../logging/logger.js';
29
+ import { buildInterviewInsights } from '../../interview/insights.js';
29
30
  import { EXIT_CODES, SEVERITY_TO_EXIT_CODE, PATHS, SECURITY_TESTING, CHECK_SAMPLING, WORKFLOW, REPORT_SCHEMAS, PERCENTAGE_CONVERSION, } from '../../constants.js';
30
31
  export const checkCommand = new Command('check')
31
32
  .description('Check MCP server schema and detect drift (free, fast, deterministic)')
@@ -166,7 +167,7 @@ export const checkCommand = new Command('check')
166
167
  metricsCollector.startInterview();
167
168
  // Initialize cache
168
169
  resetGlobalCache();
169
- const cache = getGlobalCache({ enabled: cacheEnabled });
170
+ const cache = getGlobalCache({ enabled: cacheEnabled, dir: config.cache.dir });
170
171
  if (cacheEnabled && verbose) {
171
172
  output.info('Response caching enabled');
172
173
  }
@@ -382,6 +383,8 @@ export const checkCommand = new Command('check')
382
383
  };
383
384
  output.info('Checking schemas...\n');
384
385
  const result = await interviewer.interview(mcpClient, discovery, progressCallback);
386
+ const insights = buildInterviewInsights(result);
387
+ const enrichedResult = { ...result, ...insights };
385
388
  progressBar.stop();
386
389
  if (!verbose) {
387
390
  output.newline();
@@ -448,7 +451,7 @@ export const checkCommand = new Command('check')
448
451
  output.info(`Rate-limited tools: ${rateLimit.tools.slice(0, 5).join(', ')}${rateLimit.tools.length > 5 ? ' ...' : ''}`);
449
452
  }
450
453
  }
451
- const checkSummary = buildCheckSummary(result);
454
+ const checkSummary = buildCheckSummary(enrichedResult);
452
455
  output.newline();
453
456
  output.lines(...checkSummary.lines);
454
457
  if (checkSummary.nextSteps.length > 0) {
@@ -629,12 +632,25 @@ export const checkCommand = new Command('check')
629
632
  }
630
633
  // Generate documentation (after security testing so findings can be included)
631
634
  output.info('Generating documentation...');
632
- const writeDocs = outputFormat === 'both' || outputFormat === 'agents.md';
635
+ const writeDocs = outputFormat === 'both' || outputFormat === 'docs';
633
636
  const writeJson = outputFormat === 'both' || outputFormat === 'json';
634
637
  if (writeDocs) {
635
- const contractMd = generateContractMd(result, {
638
+ const semanticMap = insights.semanticInferences
639
+ ? new Map(Object.entries(insights.semanticInferences))
640
+ : undefined;
641
+ const schemaEvolutionMap = insights.schemaEvolution
642
+ ? new Map(Object.entries(insights.schemaEvolution))
643
+ : undefined;
644
+ const errorAnalysisMap = insights.errorAnalysisSummaries
645
+ ? new Map(Object.entries(insights.errorAnalysisSummaries))
646
+ : undefined;
647
+ const contractMd = generateContractMd(enrichedResult, {
636
648
  securityFingerprints: securityEnabled ? securityFingerprints : undefined,
637
649
  workflowResults: workflowResults.length > 0 ? workflowResults : undefined,
650
+ semanticInferences: semanticMap,
651
+ schemaEvolution: schemaEvolutionMap,
652
+ errorAnalysisSummaries: errorAnalysisMap,
653
+ documentationScore: insights.documentationScore,
638
654
  exampleLength,
639
655
  fullExamples,
640
656
  maxExamplesPerTool,
@@ -648,11 +664,12 @@ export const checkCommand = new Command('check')
648
664
  }
649
665
  if (writeJson) {
650
666
  // Add workflow results to the result object for the JSON report
651
- const resultWithWorkflows = workflowResults.length > 0 ? { ...result, workflowResults } : result;
667
+ const resultWithWorkflows = workflowResults.length > 0 ? { ...enrichedResult, workflowResults } : enrichedResult;
652
668
  let jsonReport;
653
669
  try {
654
670
  jsonReport = generateJsonReport(resultWithWorkflows, {
655
671
  schemaUrl: REPORT_SCHEMAS.CHECK_REPORT_SCHEMA_URL,
672
+ schemaPath: REPORT_SCHEMAS.CHECK_REPORT_SCHEMA_FILE,
656
673
  validate: true,
657
674
  });
658
675
  }
@@ -665,7 +682,7 @@ export const checkCommand = new Command('check')
665
682
  output.info(`Written: ${jsonPath}`);
666
683
  }
667
684
  // Create baseline from results
668
- let currentBaseline = createBaseline(result, fullServerCommand);
685
+ let currentBaseline = createBaseline(enrichedResult, fullServerCommand);
669
686
  // Attach security fingerprints to tool fingerprints if security testing was run
670
687
  if (securityEnabled && securityFingerprints.size > 0) {
671
688
  currentBaseline = {
@@ -13,22 +13,23 @@ import { MCPClient } from '../../transport/mcp-client.js';
13
13
  import { discover } from '../../discovery/discovery.js';
14
14
  import { Interviewer } from '../../interview/interviewer.js';
15
15
  import { generateAgentsMd, generateJsonReport } from '../../docs/generator.js';
16
- import { loadConfig, ConfigNotFoundError, parseCommandString } from '../../config/loader.js';
16
+ import { loadConfig, ConfigNotFoundError, parseCommandString, } from '../../config/loader.js';
17
17
  import { validateConfigForExplore } from '../../config/validator.js';
18
18
  import { CostTracker, estimateInterviewCost, estimateInterviewTime, formatCostAndTimeEstimate, suggestOptimizations, formatOptimizationSuggestions, } from '../../cost/index.js';
19
19
  import { getMetricsCollector, resetMetricsCollector } from '../../metrics/collector.js';
20
- import { EXIT_CODES, WORKFLOW, PATHS } from '../../constants.js';
20
+ import { EXIT_CODES, WORKFLOW, PATHS, REPORT_SCHEMAS } from '../../constants.js';
21
21
  import { FallbackLLMClient } from '../../llm/fallback.js';
22
22
  import { getGlobalCache, resetGlobalCache } from '../../cache/response-cache.js';
23
23
  import { InterviewProgressBar, formatExploreBanner } from '../utils/progress.js';
24
24
  import { parsePersonas } from '../../persona/builtins.js';
25
- import { loadScenariosFromFile, tryLoadDefaultScenarios, DEFAULT_SCENARIOS_FILE } from '../../scenarios/index.js';
26
- import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE } from '../../workflow/loader.js';
25
+ import { loadScenariosFromFile, tryLoadDefaultScenarios, DEFAULT_SCENARIOS_FILE, } from '../../scenarios/index.js';
26
+ import { loadWorkflowsFromFile, tryLoadDefaultWorkflows, DEFAULT_WORKFLOWS_FILE, } from '../../workflow/loader.js';
27
27
  import * as output from '../output.js';
28
28
  import { StreamingDisplay } from '../output.js';
29
- import { suppressLogs, restoreLogLevel, configureLogger } from '../../logging/logger.js';
29
+ import { suppressLogs, restoreLogLevel, configureLogger, } from '../../logging/logger.js';
30
30
  import { extractServerContextFromArgs } from '../utils/server-context.js';
31
31
  import { isCI } from '../utils/env.js';
32
+ import { buildInterviewInsights } from '../../interview/insights.js';
32
33
  /**
33
34
  * Wrapper to parse personas with warning output.
34
35
  */
@@ -120,7 +121,7 @@ export const exploreCommand = new Command('explore')
120
121
  metricsCollector.startInterview();
121
122
  // Initialize cache
122
123
  resetGlobalCache();
123
- const cache = getGlobalCache({ enabled: cacheEnabled });
124
+ const cache = getGlobalCache({ enabled: cacheEnabled, dir: config.cache.dir });
124
125
  if (cacheEnabled && verbose) {
125
126
  output.info('Response caching enabled');
126
127
  }
@@ -138,7 +139,13 @@ export const exploreCommand = new Command('explore')
138
139
  };
139
140
  try {
140
141
  llmClient = new FallbackLLMClient({
141
- providers: [{ provider, model, baseUrl: provider === 'ollama' ? config.llm.ollama.baseUrl : undefined }],
142
+ providers: [
143
+ {
144
+ provider,
145
+ model,
146
+ baseUrl: provider === 'ollama' ? config.llm.ollama.baseUrl : undefined,
147
+ },
148
+ ],
142
149
  useOllamaFallback: true,
143
150
  onUsage: onUsageCallback,
144
151
  });
@@ -166,9 +173,12 @@ export const exploreCommand = new Command('explore')
166
173
  }
167
174
  // Discovery phase
168
175
  output.info('Discovering capabilities...');
169
- const discovery = await discover(mcpClient, transport === 'stdio' ? serverCommand : remoteUrl ?? serverCommand, transport === 'stdio' ? args : []);
176
+ const discovery = await discover(mcpClient, transport === 'stdio' ? serverCommand : (remoteUrl ?? serverCommand), transport === 'stdio' ? args : []);
170
177
  const resourceCount = discovery.resources?.length ?? 0;
171
- const discoveryParts = [`${discovery.tools.length} tools`, `${discovery.prompts.length} prompts`];
178
+ const discoveryParts = [
179
+ `${discovery.tools.length} tools`,
180
+ `${discovery.prompts.length} prompts`,
181
+ ];
172
182
  if (resourceCount > 0) {
173
183
  discoveryParts.push(`${resourceCount} resources`);
174
184
  }
@@ -277,13 +287,17 @@ export const exploreCommand = new Command('explore')
277
287
  let prefix = '';
278
288
  switch (opType) {
279
289
  case 'generate-questions':
280
- prefix = context ? `\n Generating questions for ${context}... ` : '\n Generating questions... ';
290
+ prefix = context
291
+ ? `\n Generating questions for ${context}... `
292
+ : '\n Generating questions... ';
281
293
  break;
282
294
  case 'analyze':
283
295
  prefix = context ? `\n Analyzing ${context}... ` : '\n Analyzing... ';
284
296
  break;
285
297
  case 'synthesize-tool':
286
- prefix = context ? `\n Synthesizing profile for ${context}... ` : '\n Synthesizing profile... ';
298
+ prefix = context
299
+ ? `\n Synthesizing profile for ${context}... `
300
+ : '\n Synthesizing profile... ';
287
301
  break;
288
302
  case 'synthesize-overall':
289
303
  prefix = '\n Synthesizing overall findings... ';
@@ -365,6 +379,8 @@ export const exploreCommand = new Command('explore')
365
379
  };
366
380
  output.info('Starting exploration...\n');
367
381
  const result = await interviewer.interview(mcpClient, discovery, progressCallback);
382
+ const insights = buildInterviewInsights(result);
383
+ const enrichedResult = { ...result, ...insights };
368
384
  progressBar.stop();
369
385
  if (!verbose) {
370
386
  output.newline();
@@ -375,16 +391,20 @@ export const exploreCommand = new Command('explore')
375
391
  if (docsDir !== outputDir) {
376
392
  mkdirSync(docsDir, { recursive: true });
377
393
  }
378
- const writeDocs = outputFormat === 'both' || outputFormat === 'agents.md';
394
+ const writeDocs = outputFormat === 'both' || outputFormat === 'docs';
379
395
  const writeJson = outputFormat === 'both' || outputFormat === 'json';
380
396
  if (writeDocs) {
381
- const agentsMd = generateAgentsMd(result);
397
+ const agentsMd = generateAgentsMd(enrichedResult);
382
398
  const agentsMdPath = join(docsDir, config.output.files.agentsDoc);
383
399
  writeFileSync(agentsMdPath, agentsMd);
384
400
  output.info(`Written: ${agentsMdPath}`);
385
401
  }
386
402
  if (writeJson) {
387
- const jsonReport = generateJsonReport(result);
403
+ const jsonReport = generateJsonReport(enrichedResult, {
404
+ schemaUrl: REPORT_SCHEMAS.EXPLORE_REPORT_SCHEMA_URL,
405
+ schemaPath: REPORT_SCHEMAS.EXPLORE_REPORT_SCHEMA_FILE,
406
+ validate: true,
407
+ });
388
408
  const jsonPath = join(outputDir, config.output.files.exploreReport);
389
409
  writeFileSync(jsonPath, jsonReport);
390
410
  output.info(`Written: ${jsonPath}`);
package/dist/cli/index.js CHANGED
@@ -115,6 +115,14 @@ Check MCP servers for drift. Explore behavior. Generate documentation.
115
115
  Commands:
116
116
  check - Schema validation and drift detection (free, fast, deterministic)
117
117
  explore - LLM-powered behavioral exploration and documentation
118
+ discover - Quick capability discovery (no tests)
119
+ registry - Search the MCP Registry
120
+ baseline - Manage baselines (save/compare/accept/diff/show)
121
+ golden - Golden output regression testing
122
+ contract - Contract validation (generate/validate/show)
123
+ watch - Continuous checking on file changes
124
+ auth - Manage LLM provider API keys
125
+ validate-config - Validate bellwether.yaml without running tests
118
126
 
119
127
  For more information on a specific command, use:
120
128
  bellwether <command> --help`)
@@ -23,13 +23,11 @@ export function generateConfigTemplate(options = {}) {
23
23
  // Override security.enabled if preset specifies it
24
24
  const securityEnabledValue = securityEnabled ? 'true' : String(defaults.check.security.enabled);
25
25
  const serverArgsYaml = serverArgs.length > 0
26
- ? `\n args:\n${serverArgs.map(arg => ` - "${arg}"`).join('\n')}`
26
+ ? `\n args:\n${serverArgs.map((arg) => ` - "${arg}"`).join('\n')}`
27
27
  : '\n args: []';
28
28
  const presetComment = preset ? `# Generated with: bellwether init --preset ${preset}\n` : '';
29
29
  // Generate env section if env vars were detected
30
- const envVarsYaml = envVars.length > 0
31
- ? `\n env:\n${envVars.map(v => ` ${v}: "\${${v}}"`).join('\n')}`
32
- : '';
30
+ const envVarsYaml = envVars.length > 0 ? `\n env:\n${envVars.map((v) => ` ${v}: "\${${v}}"`).join('\n')}` : '';
33
31
  return `# Bellwether Configuration
34
32
  # Generated by: bellwether init
35
33
  # Docs: https://docs.bellwether.sh/guides/configuration
@@ -61,10 +59,12 @@ server:
61
59
  timeout: ${defaults.server.timeout}
62
60
 
63
61
  # Additional environment variables for the server process
64
- # Use \${VAR} syntax to reference environment variables${envVarsYaml}${envVars.length === 0 ? `
62
+ # Use \${VAR} syntax to reference environment variables${envVarsYaml}${envVars.length === 0
63
+ ? `
65
64
  # env:
66
65
  # NODE_ENV: production
67
- # API_KEY: "\${API_KEY}"` : ''}
66
+ # API_KEY: "\${API_KEY}"`
67
+ : ''}
68
68
 
69
69
  # =============================================================================
70
70
  # SCENARIOS (used by both commands)
@@ -91,7 +91,8 @@ output:
91
91
  # These are kept in root by default for visibility
92
92
  docsDir: "${defaults.output.docsDir}"
93
93
 
94
- # Output format: agents.md (markdown only), json (JSON only), or both
94
+ # Output format: docs (markdown only), json (JSON only), or both
95
+ # Legacy alias: agents.md (treated as docs)
95
96
  format: ${defaults.output.format}
96
97
 
97
98
  # Example output settings (for CONTRACT.md and AGENTS.md)