@brutalist/mcp 0.9.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +12 -6
  2. package/dist/brutalist-server.d.ts +13 -4
  3. package/dist/brutalist-server.d.ts.map +1 -1
  4. package/dist/brutalist-server.js +357 -151
  5. package/dist/brutalist-server.js.map +1 -1
  6. package/dist/cli-agents.d.ts +1 -1
  7. package/dist/cli-agents.d.ts.map +1 -1
  8. package/dist/cli-agents.js +63 -48
  9. package/dist/cli-agents.js.map +1 -1
  10. package/dist/domains/argument-space.d.ts +3 -3
  11. package/dist/domains/argument-space.js +9 -9
  12. package/dist/domains/argument-space.js.map +1 -1
  13. package/dist/domains/critique-domain.d.ts +12 -0
  14. package/dist/domains/critique-domain.d.ts.map +1 -1
  15. package/dist/domains/critique-domain.js +12 -1
  16. package/dist/domains/critique-domain.js.map +1 -1
  17. package/dist/generators/tool-generator.d.ts.map +1 -1
  18. package/dist/generators/tool-generator.js +5 -5
  19. package/dist/generators/tool-generator.js.map +1 -1
  20. package/dist/handlers/tool-handler.d.ts.map +1 -1
  21. package/dist/handlers/tool-handler.js +18 -10
  22. package/dist/handlers/tool-handler.js.map +1 -1
  23. package/dist/registry/domains.d.ts +10 -0
  24. package/dist/registry/domains.d.ts.map +1 -1
  25. package/dist/registry/domains.js +153 -11
  26. package/dist/registry/domains.js.map +1 -1
  27. package/dist/system-prompts.d.ts +8 -0
  28. package/dist/system-prompts.d.ts.map +1 -0
  29. package/dist/system-prompts.js +596 -0
  30. package/dist/system-prompts.js.map +1 -0
  31. package/dist/tool-definitions.d.ts +20 -1
  32. package/dist/tool-definitions.d.ts.map +1 -1
  33. package/dist/tool-definitions.js +42 -213
  34. package/dist/tool-definitions.js.map +1 -1
  35. package/dist/tool-router.d.ts +12 -0
  36. package/dist/tool-router.d.ts.map +1 -0
  37. package/dist/tool-router.js +59 -0
  38. package/dist/tool-router.js.map +1 -0
  39. package/dist/types/brutalist.d.ts +1 -0
  40. package/dist/types/brutalist.d.ts.map +1 -1
  41. package/dist/types/tool-config.d.ts +4 -3
  42. package/dist/types/tool-config.d.ts.map +1 -1
  43. package/dist/types/tool-config.js +7 -6
  44. package/dist/types/tool-config.js.map +1 -1
  45. package/package.json +1 -1
@@ -3,13 +3,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { z } from "zod";
4
4
  import { CLIAgentOrchestrator } from './cli-agents.js';
5
5
  import { logger } from './logger.js';
6
- import { BASE_ROAST_SCHEMA } from './types/tool-config.js';
7
- import { TOOL_CONFIGS } from './tool-definitions-generated.js';
8
6
  import { parseCursor, PAGINATION_DEFAULTS } from './utils/pagination.js';
9
7
  import { ResponseCache } from './utils/response-cache.js';
10
8
  import { ResponseFormatter } from './formatting/response-formatter.js';
11
9
  import { HttpTransport } from './transport/http-transport.js';
12
10
  import { ToolHandler } from './handlers/tool-handler.js';
11
+ import { getDomain, generateToolConfig } from './registry/domains.js';
12
+ import { filterToolsByIntent, getMatchingDomainIds } from './tool-router.js';
13
13
  // Use environment variable or fallback to manual version
14
14
  const PACKAGE_VERSION = process.env.npm_package_version || "0.6.12";
15
15
  /**
@@ -261,42 +261,92 @@ export class BrutalistServer {
261
261
  };
262
262
  /**
263
263
  * Register all MCP tools
264
+ *
265
+ * TOOL REDUCTION STRATEGY: Only expose 4 gateway tools instead of 15.
266
+ * The unified `roast` tool with domain parameter replaces all 11 roast_* tools.
267
+ * This reduces cognitive load for AI agents while maintaining full functionality.
264
268
  */
265
269
  registerTools() {
266
- // Register all roast tools using unified handler - DRY principle
267
- TOOL_CONFIGS.forEach(config => {
268
- const schema = {
269
- ...config.schemaExtensions,
270
- ...BASE_ROAST_SCHEMA
271
- };
272
- this.server.tool(config.name, config.description, schema, async (args, extra) => this.toolHandler.handleRoastTool(config, args, extra));
273
- });
274
- // Register special tools that don't follow the pattern
270
+ // NOTE: Individual domain tools (roast_codebase, roast_security, etc.) are NOT registered.
271
+ // Use the unified `roast` tool with domain parameter instead.
272
+ // The getToolConfigs() function still exists for internal routing via handleUnifiedRoast().
273
+ // Register only the gateway tools
275
274
  this.registerSpecialTools();
276
275
  }
277
276
  /**
278
- * Register special tools (debate, roster)
277
+ * Register special tools (debate, roster, unified roast)
279
278
  */
280
279
  registerSpecialTools() {
281
- // ROAST_CLI_DEBATE: Adversarial analysis between different CLI agents
282
- this.server.tool("roast_cli_debate", "Deploy CLI agents in structured adversarial debate. Agents take opposing positions and systematically challenge each other's reasoning. Perfect for exploring complex topics from multiple perspectives and stress-testing ideas through rigorous intellectual discourse.", {
283
- targetPath: z.string().describe("Topic, question, or concept to debate (NOT a file path - use natural language)"),
284
- debateRounds: z.number().optional().describe("Number of debate rounds (default: 2, max: 10)"),
285
- context: z.string().optional().describe("Additional context for the debate"),
286
- workingDirectory: z.string().optional().describe("Working directory for analysis"),
280
+ // UNIFIED ROAST TOOL: Single entry point for all domain analysis
281
+ this.server.tool("roast", "Unified brutal AI critique. Specify domain for targeted analysis. Consolidates all roast_* tools into one polymorphic API.", {
282
+ domain: z.enum([
283
+ "codebase", "file_structure", "dependencies", "git_history", "test_coverage",
284
+ "idea", "architecture", "research", "security", "product", "infrastructure"
285
+ ]).describe("Analysis domain"),
286
+ target: z.string().describe("Directory path for filesystem domains (codebase, dependencies, git_history, etc.) OR text content for abstract domains (idea, architecture, security, etc.)"),
287
+ // Common optional fields
288
+ context: z.string().optional().describe("Additional context"),
289
+ workingDirectory: z.string().optional().describe("Working directory"),
290
+ clis: z.array(z.enum(["claude", "codex", "gemini"])).min(1).max(3).optional().describe("CLI agents to use (default: all available). Example: ['claude', 'gemini']"),
291
+ verbose: z.boolean().optional().describe("Detailed output"),
287
292
  models: z.object({
288
- claude: z.string().optional().describe("Claude model: opus (recommended), sonnet, haiku, opusplan, or full name like claude-opus-4-5-20251101. Default: user's configured model"),
289
- codex: z.string().optional().describe("Codex model: gpt-5.1-codex-max (recommended), gpt-5.2, gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5-codex, o4-mini. Default: CLI's default"),
290
- gemini: z.string().optional().describe("Gemini model: gemini-3-pro (recommended), gemini-3-flash, gemini-2.5-pro, gemini-2.5-flash. Default: Auto routing")
291
- }).optional().describe("Specific models to use for each CLI agent - defaults let each CLI use its own latest model"),
292
- // Pagination and continuation parameters
293
- context_id: z.string().optional().describe("Context ID from previous response for pagination or conversation continuation"),
294
- resume: z.boolean().optional().describe("Continue debate with history injection (requires context_id)"),
293
+ claude: z.string().optional(),
294
+ codex: z.string().optional(),
295
+ gemini: z.string().optional()
296
+ }).optional().describe("CLI-specific models"),
297
+ // Pagination
295
298
  offset: z.number().min(0).optional().describe("Pagination offset"),
296
- limit: z.number().min(1000).max(100000).optional().describe("Max chars/chunk (default: 90000)"),
299
+ limit: z.number().min(1000).max(100000).optional().describe("Max chars/chunk"),
297
300
  cursor: z.string().optional().describe("Pagination cursor"),
301
+ context_id: z.string().optional().describe("Context ID for pagination/continuation"),
302
+ resume: z.boolean().optional().describe("Continue conversation"),
298
303
  force_refresh: z.boolean().optional().describe("Ignore cache"),
299
- verbose: z.boolean().optional().describe("Detailed output")
304
+ // Domain-specific optional fields (passed through to handler)
305
+ depth: z.number().optional().describe("Max depth for file_structure"),
306
+ includeDevDeps: z.boolean().optional().describe("Include dev deps for dependencies"),
307
+ commitRange: z.string().optional().describe("Commit range for git_history"),
308
+ runCoverage: z.boolean().optional().describe("Run coverage for test_coverage"),
309
+ resources: z.string().optional().describe("Resources for idea"),
310
+ timeline: z.string().optional().describe("Timeline for idea"),
311
+ scale: z.string().optional().describe("Scale for architecture/infrastructure"),
312
+ constraints: z.string().optional().describe("Constraints for architecture"),
313
+ deployment: z.string().optional().describe("Deployment for architecture"),
314
+ field: z.string().optional().describe("Field for research"),
315
+ claims: z.string().optional().describe("Claims for research"),
316
+ data: z.string().optional().describe("Data for research"),
317
+ assets: z.string().optional().describe("Assets for security"),
318
+ threatModel: z.string().optional().describe("Threat model for security"),
319
+ compliance: z.string().optional().describe("Compliance for security"),
320
+ users: z.string().optional().describe("Users for product"),
321
+ competition: z.string().optional().describe("Competition for product"),
322
+ metrics: z.string().optional().describe("Metrics for product"),
323
+ sla: z.string().optional().describe("SLA for infrastructure"),
324
+ budget: z.string().optional().describe("Budget for infrastructure")
325
+ }, async (args, extra) => this.handleUnifiedRoast(args, extra));
326
+ // ROAST_CLI_DEBATE: Adversarial analysis between different CLI agents
327
+ this.server.tool("roast_cli_debate", "Deploy 2 CLI agents in structured adversarial debate with constitutional position anchoring. Calling agent should extract PRO/CON positions from topic before invoking.", {
328
+ topic: z.string().describe("The debate topic"),
329
+ proPosition: z.string().describe("The PRO thesis to defend (extracted by calling agent)"),
330
+ conPosition: z.string().describe("The CON thesis to defend (extracted by calling agent)"),
331
+ agents: z.array(z.enum(["claude", "codex", "gemini"])).length(2).optional()
332
+ .describe("Two agents to debate (random selection from available if not specified)"),
333
+ rounds: z.number().min(1).max(3).default(3).optional()
334
+ .describe("Number of debate rounds (default: 3)"),
335
+ context: z.string().optional().describe("Additional context for the debate"),
336
+ workingDirectory: z.string().optional().describe("Working directory for analysis"),
337
+ models: z.object({
338
+ claude: z.string().optional(),
339
+ codex: z.string().optional(),
340
+ gemini: z.string().optional()
341
+ }).optional().describe("Model overrides for specific agents"),
342
+ // Pagination and conversation continuation
343
+ context_id: z.string().optional().describe("Context ID for pagination/continuation"),
344
+ resume: z.boolean().optional().describe("Continue debate (requires context_id)"),
345
+ offset: z.number().min(0).optional(),
346
+ limit: z.number().min(1000).max(100000).optional(),
347
+ cursor: z.string().optional(),
348
+ force_refresh: z.boolean().optional(),
349
+ verbose: z.boolean().optional()
300
350
  }, async (args) => {
301
351
  // CRITICAL: Prevent recursion
302
352
  if (process.env.BRUTALIST_SUBPROCESS === '1') {
@@ -310,27 +360,59 @@ export class BrutalistServer {
310
360
  }
311
361
  return this.handleDebateToolExecution(args);
312
362
  });
363
+ // BRUTALIST_DISCOVER: Intent-based tool discovery
364
+ this.server.tool("brutalist_discover", "Discover relevant brutalist tools based on your intent. Returns the top 3 most relevant analysis tools.", {
365
+ intent: z.string().describe("What you want to analyze (e.g., 'review security of my auth system', 'check code quality')")
366
+ }, async (args) => {
367
+ const matchingDomains = getMatchingDomainIds(args.intent);
368
+ const configs = filterToolsByIntent(args.intent);
369
+ let response = "# Recommended Brutalist Domains\n\n";
370
+ response += `Based on your intent: "${args.intent}"\n\n`;
371
+ if (matchingDomains.length === 0) {
372
+ response += "No specific matches found. Use the unified `roast` tool with any domain:\n";
373
+ response += "- `roast(domain: 'codebase', target: '/path/to/code')` for code review\n";
374
+ response += "- `roast(domain: 'security', target: 'description of system')` for security analysis\n";
375
+ }
376
+ else {
377
+ response += `**Top ${matchingDomains.length} matching domains:**\n\n`;
378
+ for (const config of configs) {
379
+ // Extract domain from tool name (roast_security -> security)
380
+ const domain = config.name.replace('roast_', '');
381
+ response += `### ${domain}\n`;
382
+ response += `${config.description}\n`;
383
+ response += `\`roast(domain: '${domain}', target: '...')\`\n\n`;
384
+ }
385
+ }
386
+ return {
387
+ content: [{ type: "text", text: response }]
388
+ };
389
+ });
313
390
  // CLI_AGENT_ROSTER: Show available brutalist critics
314
391
  this.server.tool("cli_agent_roster", "Know your weapons. Display the available CLI agent critics (Claude Code, Codex, Gemini CLI) ready to demolish your work, their capabilities, and how to deploy them for systematic destruction.", {}, async (args) => {
315
392
  try {
316
393
  let roster = "# Brutalist CLI Agent Arsenal\n\n";
317
- roster += "## Available AI Critics (13 Tools Total)\n\n";
318
- roster += "**Abstract Analysis Tools (6):**\n";
319
- roster += "- `roast_idea` - Destroy any business/technical/creative concept\n";
320
- roster += "- `roast_architecture` - Demolish system designs\n";
321
- roster += "- `roast_research` - Tear apart academic methodologies\n";
322
- roster += "- `roast_security` - Annihilate security designs\n";
323
- roster += "- `roast_product` - Eviscerate UX and market concepts\n";
324
- roster += "- `roast_infrastructure` - Obliterate DevOps setups\n\n";
325
- roster += "**File-System Analysis Tools (5):**\n";
326
- roster += "- `roast_codebase` - Analyze actual source code\n";
327
- roster += "- `roast_file_structure` - Examine directory organization\n";
328
- roster += "- `roast_dependencies` - Review package management\n";
329
- roster += "- `roast_git_history` - Analyze version control workflow\n";
330
- roster += "- `roast_test_coverage` - Evaluate testing strategy\n\n";
331
- roster += "**Meta Tools (2):**\n";
332
- roster += "- `roast_cli_debate` - CLI vs CLI adversarial analysis\n";
333
- roster += "- `cli_agent_roster` - This tool (show capabilities)\n\n";
394
+ roster += "## Available Tools (4 Gateway Tools)\n\n";
395
+ roster += "### `roast` - Unified Analysis Tool\n";
396
+ roster += "The primary entry point for all brutal analysis. Use the `domain` parameter to target:\n\n";
397
+ roster += "**Filesystem Domains:**\n";
398
+ roster += "- `codebase` - Analyze source code for security, performance, maintainability\n";
399
+ roster += "- `file_structure` - Examine directory organization\n";
400
+ roster += "- `dependencies` - Review package management and vulnerabilities\n";
401
+ roster += "- `git_history` - Analyze version control workflow\n";
402
+ roster += "- `test_coverage` - Evaluate testing strategy\n\n";
403
+ roster += "**Abstract Domains:**\n";
404
+ roster += "- `idea` - Destroy business/technical concepts\n";
405
+ roster += "- `architecture` - Demolish system designs\n";
406
+ roster += "- `research` - Tear apart methodologies\n";
407
+ roster += "- `security` - Annihilate security designs\n";
408
+ roster += "- `product` - Eviscerate UX concepts\n";
409
+ roster += "- `infrastructure` - Obliterate DevOps setups\n\n";
410
+ roster += "### `roast_cli_debate` - Adversarial Multi-Agent Debate\n";
411
+ roster += "Pit CLI agents against each other on any topic.\n\n";
412
+ roster += "### `brutalist_discover` - Intent-Based Discovery\n";
413
+ roster += "Describe what you want to analyze, get domain recommendations.\n\n";
414
+ roster += "### `cli_agent_roster` - This Tool\n";
415
+ roster += "Show available capabilities and usage.\n\n";
334
416
  roster += "## CLI Agent Capabilities\n";
335
417
  roster += "**Claude Code** - Advanced analysis with direct system prompt injection\n";
336
418
  roster += "**Codex** - Secure execution with embedded brutal prompts\n";
@@ -339,15 +421,19 @@ export class BrutalistServer {
339
421
  const cliContext = await this.cliOrchestrator.detectCLIContext();
340
422
  roster += "## Current CLI Context\n";
341
423
  roster += `**Available CLIs:** ${cliContext.availableCLIs.join(', ') || 'None detected'}\n\n`;
424
+ roster += "## Domain Discovery\n";
425
+ roster += "Use `brutalist_discover` to find the best domain for your analysis:\n";
426
+ roster += "- Example: `brutalist_discover(intent: 'review my authentication security')`\n";
427
+ roster += "- Returns the top 3 most relevant domains to use with the `roast` tool\n\n";
342
428
  roster += "## Pagination & Conversation Continuation\n";
343
429
  roster += "**Two distinct modes for using context_id:**\n\n";
344
430
  roster += "**1. Pagination** (cached result retrieval):\n";
345
431
  roster += "- `context_id` alone returns cached response at different offsets\n";
346
- roster += "- Example: `roast_codebase(context_id: 'abc123', offset: 25000)`\n\n";
432
+ roster += "- Example: `roast(domain: 'codebase', target: '.', context_id: 'abc123', offset: 25000)`\n\n";
347
433
  roster += "**2. Conversation Continuation** (resume dialogue with history):\n";
348
434
  roster += "- `context_id` + `resume: true` + new content continues the conversation\n";
349
435
  roster += "- Prior conversation is injected into CLI agent context\n";
350
- roster += "- Example: `roast_codebase(context_id: 'abc123', resume: true, content: 'Explain issue #3 in detail')`\n\n";
436
+ roster += "- Example: `roast(domain: 'codebase', target: '.', context_id: 'abc123', resume: true)`\n\n";
351
437
  roster += "**Cache TTL:** 2 hours\n\n";
352
438
  roster += "## Brutalist Philosophy\n";
353
439
  roster += "*All tools use CLI agents with brutal system prompts for maximum reality-based criticism.*\n";
@@ -361,8 +447,52 @@ export class BrutalistServer {
361
447
  });
362
448
  }
363
449
  /**
364
- * Handle debate tool execution with caching, pagination, and conversation continuation
365
- * Delegated mostly to ToolHandler but kept here for CLI debate-specific logic
450
+ * Handle unified roast tool - routes to appropriate domain handler
451
+ */
452
+ async handleUnifiedRoast(args, extra) {
453
+ // CRITICAL: Prevent recursion
454
+ if (process.env.BRUTALIST_SUBPROCESS === '1') {
455
+ logger.warn(`🚫 Rejecting unified roast from brutalist subprocess`);
456
+ return {
457
+ content: [{
458
+ type: "text",
459
+ text: `ERROR: Brutalist MCP tools cannot be used from within a brutalist-spawned CLI subprocess (recursion prevented)`
460
+ }]
461
+ };
462
+ }
463
+ // Get domain config
464
+ const domain = getDomain(args.domain);
465
+ if (!domain) {
466
+ return {
467
+ content: [{
468
+ type: "text",
469
+ text: `ERROR: Unknown domain "${args.domain}". Valid domains: codebase, file_structure, dependencies, git_history, test_coverage, idea, architecture, research, security, product, infrastructure`
470
+ }]
471
+ };
472
+ }
473
+ // Generate tool config from domain
474
+ const toolConfig = generateToolConfig(domain);
475
+ // Map 'target' to the appropriate primary arg field
476
+ const mappedArgs = { ...args };
477
+ delete mappedArgs.domain;
478
+ delete mappedArgs.target;
479
+ // Set the primary argument based on domain's input type
480
+ if (domain.inputType === 'filesystem') {
481
+ mappedArgs.targetPath = args.target;
482
+ }
483
+ else {
484
+ mappedArgs.content = args.target;
485
+ // For abstract tools, also set targetPath if workingDirectory not provided
486
+ if (!args.workingDirectory) {
487
+ mappedArgs.targetPath = '.';
488
+ }
489
+ }
490
+ // Delegate to the unified handler
491
+ return this.toolHandler.handleRoastTool(toolConfig, mappedArgs, extra);
492
+ }
493
+ /**
494
+ * Handle debate tool execution with constitutional position anchoring.
495
+ * Uses 2 randomly selected agents (or user-specified) with explicit PRO/CON positions.
366
496
  */
367
497
  async handleDebateToolExecution(args) {
368
498
  try {
@@ -392,11 +522,11 @@ export class BrutalistServer {
392
522
  logger.info(`🎯 Debate cache HIT for context_id: ${args.context_id}`);
393
523
  if (args.resume === true) {
394
524
  // CONVERSATION CONTINUATION: Continue the debate
395
- if (!args.targetPath || args.targetPath.trim() === '') {
525
+ if (!args.topic || args.topic.trim() === '') {
396
526
  throw new Error(`Debate continuation (resume: true) requires a new prompt/question. ` +
397
- `Provide your follow-up in the targetPath field.`);
527
+ `Provide your follow-up in the topic field.`);
398
528
  }
399
- logger.info(`💬 Debate continuation - new prompt: "${args.targetPath.substring(0, 50)}..."`);
529
+ logger.info(`💬 Debate continuation - new prompt: "${args.topic.substring(0, 50)}..."`);
400
530
  conversationHistory = cachedResponse.conversationHistory || [];
401
531
  // Fall through to execute new debate round with history
402
532
  }
@@ -425,10 +555,12 @@ export class BrutalistServer {
425
555
  // Generate cache key for this debate
426
556
  const cacheKey = this.responseCache.generateCacheKey({
427
557
  tool: 'roast_cli_debate',
428
- targetPath: args.targetPath,
429
- debateRounds: args.debateRounds,
430
- context: args.context,
431
- models: args.models
558
+ topic: args.topic,
559
+ proPosition: args.proPosition,
560
+ conPosition: args.conPosition,
561
+ agents: args.agents,
562
+ rounds: args.rounds,
563
+ context: args.context
432
564
  });
433
565
  // Check cache for identical request (if not resuming)
434
566
  if (!args.force_refresh && !args.resume) {
@@ -462,8 +594,17 @@ export class BrutalistServer {
462
594
  logger.info(`💬 Injected ${conversationHistory.length} previous messages into debate context`);
463
595
  }
464
596
  // Execute the debate
465
- const debateRounds = Math.min(args.debateRounds || 2, 10);
466
- const result = await this.executeCLIDebate(args.targetPath, debateRounds, debateContext, args.workingDirectory, args.models);
597
+ const numRounds = Math.min(args.rounds || 3, 3);
598
+ const result = await this.executeCLIDebate({
599
+ topic: args.topic,
600
+ proPosition: args.proPosition,
601
+ conPosition: args.conPosition,
602
+ agents: args.agents,
603
+ rounds: numRounds,
604
+ context: debateContext,
605
+ workingDirectory: args.workingDirectory,
606
+ models: args.models
607
+ });
467
608
  // Cache the result
468
609
  let contextId;
469
610
  if (result.success && result.responses.length > 0) {
@@ -472,7 +613,7 @@ export class BrutalistServer {
472
613
  const now = Date.now();
473
614
  const updatedConversation = [
474
615
  ...(conversationHistory || []),
475
- { role: 'user', content: args.targetPath, timestamp: now },
616
+ { role: 'user', content: args.topic, timestamp: now },
476
617
  { role: 'assistant', content: fullContent, timestamp: now }
477
618
  ];
478
619
  if (args.resume && args.context_id && conversationHistory) {
@@ -483,7 +624,7 @@ export class BrutalistServer {
483
624
  }
484
625
  else {
485
626
  // New debate - create new context_id
486
- const { contextId: newId } = await this.responseCache.set({ tool: 'roast_cli_debate', targetPath: args.targetPath }, fullContent, cacheKey, undefined, undefined, updatedConversation);
627
+ const { contextId: newId } = await this.responseCache.set({ tool: 'roast_cli_debate', topic: args.topic }, fullContent, cacheKey, undefined, undefined, updatedConversation);
487
628
  contextId = newId;
488
629
  logger.info(`✅ Cached new debate with context ID: ${contextId}`);
489
630
  }
@@ -496,116 +637,181 @@ export class BrutalistServer {
496
637
  }
497
638
  }
498
639
  /**
499
- * Execute CLI debate (kept in server for debate-specific logic)
640
+ * Execute CLI debate with constitutional position anchoring.
641
+ * 2 agents, explicit PRO/CON positions, context compression between rounds.
500
642
  */
501
- async executeCLIDebate(targetPath, debateRounds, context, workingDirectory, models) {
502
- logger.debug("Executing CLI debate", {
503
- targetPath,
504
- debateRounds,
505
- workingDirectory
506
- });
643
+ async executeCLIDebate(args) {
644
+ const { topic, proPosition, conPosition, rounds, context, workingDirectory, models } = args;
645
+ logger.debug("Executing CLI debate", { topic, proPosition, conPosition, rounds });
507
646
  try {
508
- // Get CLI context
647
+ // Get available CLIs
509
648
  const cliContext = await this.cliOrchestrator.detectCLIContext();
510
- const availableAgents = cliContext.availableCLIs;
511
- if (availableAgents.length < 2) {
512
- throw new Error(`Need at least 2 CLI agents for debate. Available: ${availableAgents.join(', ')}`);
649
+ const availableCLIs = cliContext.availableCLIs;
650
+ if (availableCLIs.length < 2) {
651
+ throw new Error(`Need at least 2 CLI agents for debate. Available: ${availableCLIs.join(', ')}`);
513
652
  }
514
- const debateContext = [];
515
- const fullDebateTranscript = new Map();
516
- // Initialize transcript for each agent
517
- availableAgents.forEach(agent => fullDebateTranscript.set(agent, []));
518
- // Assign opposing positions to each agent based on the debate topic
519
- const agentPositions = new Map();
520
- const positions = [
521
- "PRO-POSITION: Argue strongly FOR the proposed action/idea",
522
- "CONTRA-POSITION: Argue strongly AGAINST the proposed action/idea"
523
- ];
524
- availableAgents.forEach((agent, index) => {
525
- agentPositions.set(agent, positions[index % positions.length]);
526
- });
527
- // Round 1: Initial positions with assigned stances
528
- logger.debug(`Starting debate round 1: Initial positions`);
529
- for (const [agent, position] of agentPositions.entries()) {
530
- const assignedPrompt = `You are ${agent.toUpperCase()}, a PASSIONATE ADVOCATE who strongly believes in this position: ${position}
653
+ // Select 2 agents: use specified or random selection
654
+ let selectedAgents;
655
+ if (args.agents && args.agents.length === 2) {
656
+ // Validate specified agents are available
657
+ const unavailable = args.agents.filter(a => !availableCLIs.includes(a));
658
+ if (unavailable.length > 0) {
659
+ throw new Error(`Specified agents not available: ${unavailable.join(', ')}. Available: ${availableCLIs.join(', ')}`);
660
+ }
661
+ selectedAgents = args.agents;
662
+ }
663
+ else {
664
+ // Random selection of 2 agents
665
+ const shuffled = [...availableCLIs].sort(() => Math.random() - 0.5);
666
+ selectedAgents = shuffled.slice(0, 2);
667
+ }
668
+ // Randomly assign PRO/CON positions
669
+ const shuffledAgents = [...selectedAgents].sort(() => Math.random() - 0.5);
670
+ const proAgent = shuffledAgents[0];
671
+ const conAgent = shuffledAgents[1];
672
+ logger.info(`🎭 Debate: ${proAgent.toUpperCase()} (PRO) vs ${conAgent.toUpperCase()} (CON)`);
673
+ const debateResponses = [];
674
+ const transcript = [];
675
+ let compressedContext = '';
676
+ // Constitutional position anchor template
677
+ const constitutionalAnchor = (agent, position, thesis) => `
678
+ You are ${agent.toUpperCase()}, arguing the ${position} position in this debate.
531
679
 
532
- DEBATE TOPIC: ${targetPath}
533
- CONTEXT: ${context || ''}
680
+ YOUR THESIS: ${thesis}
534
681
 
535
- You are completely convinced your position is correct and critically important. You will argue forcefully and never concede ground to the opposing view.
682
+ CONSTITUTIONAL RULES (UNBREAKABLE):
683
+ 1. You MUST maintain your position throughout ALL rounds
684
+ 2. You MAY acknowledge valid points but MUST explain why they don't invalidate your thesis
685
+ 3. You MUST NOT agree to compromise or "meet in the middle"
686
+ 4. You MUST directly attack your opponent's strongest arguments
687
+ 5. You MUST reinforce your core thesis in every response
536
688
 
537
- YOUR MISSION:
538
- 1. Present devastating critiques of the opposing position
539
- 2. Show why alternative approaches lead to serious problems
540
- 3. Use sharp, direct language - call out flawed reasoning and poor assumptions
541
- 4. Never hedge or qualify your stance
542
- 5. Be completely confident in your position
543
- 6. Treat this as an intellectually crucial debate
689
+ Your goal is PERSUASION, not consensus. Argue to WIN.
690
+ `;
691
+ // Execute rounds
692
+ for (let round = 1; round <= rounds; round++) {
693
+ logger.info(`📢 Round ${round}/${rounds}`);
694
+ // Both agents argue in each round
695
+ for (const [agent, position, thesis] of [
696
+ [proAgent, 'PRO', proPosition],
697
+ [conAgent, 'CON', conPosition]
698
+ ]) {
699
+ let prompt;
700
+ if (round === 1) {
701
+ // Opening statement
702
+ prompt = `${constitutionalAnchor(agent, position, thesis)}
544
703
 
545
- Remember: You are ${agent.toUpperCase()}, the passionate champion of ${position.split(':')[0]}. Argue with conviction.`;
546
- logger.info(`🎭 ${agent.toUpperCase()} preparing initial position: ${position.split(':')[0]}`);
547
- const response = await this.cliOrchestrator.executeSingleCLI(agent, assignedPrompt, assignedPrompt, {
548
- workingDirectory: workingDirectory || this.config.workingDirectory,
549
- timeout: (this.config.defaultTimeout || 60000) * 2,
550
- models: models ? { [agent]: models[agent] } : undefined
551
- });
552
- if (response.success) {
553
- debateContext.push(response);
554
- if (response.output) {
555
- fullDebateTranscript.get(agent)?.push(response.output);
704
+ DEBATE TOPIC: ${topic}
705
+ ${context ? `CONTEXT: ${context}` : ''}
706
+
707
+ This is Round 1: OPENING STATEMENT
708
+
709
+ Present your opening argument for the ${position} position. Structure your response:
710
+
711
+ <thesis_statement>
712
+ State your core thesis clearly and forcefully
713
+ </thesis_statement>
714
+
715
+ <key_arguments>
716
+ Present 3 devastating arguments supporting your position
717
+ </key_arguments>
718
+
719
+ <preemptive_rebuttal>
720
+ Anticipate and destroy the strongest opposing argument
721
+ </preemptive_rebuttal>
722
+
723
+ <conclusion>
724
+ Powerful closing that reinforces why your position is correct
725
+ </conclusion>
726
+
727
+ Remember: You are arguing that "${thesis}" - defend this with conviction.`;
556
728
  }
557
- }
558
- }
559
- // Subsequent rounds: Turn-based responses attacking specific arguments
560
- for (let round = 2; round <= debateRounds; round++) {
561
- logger.debug(`Starting debate round ${round}: Adversarial engagement`);
562
- // Execute turn-based responses with fixed positions
563
- for (const [currentAgent, assignedPosition] of agentPositions.entries()) {
564
- const opponents = Array.from(agentPositions.entries()).filter(([a, _]) => a !== currentAgent);
565
- const opponentPositions = opponents
566
- .map(([opponent, oppPosition]) => {
567
- const transcript = fullDebateTranscript.get(opponent) || [];
568
- const latestPosition = transcript[transcript.length - 1] || 'No position stated';
569
- return `${opponent.toUpperCase()} (arguing ${oppPosition.split(':')[0]}):\n${latestPosition}`;
570
- })
571
- .join('\n\n---\n\n');
572
- const confrontationalPrompt = `You are ${currentAgent.toUpperCase()}, PASSIONATE ADVOCATE for ${assignedPosition.split(':')[0]} (Round ${round})
729
+ else {
730
+ // Rebuttal rounds - include compressed context from previous rounds
731
+ const opponentTranscript = transcript
732
+ .filter(t => t.agent !== agent && t.round === round - 1)
733
+ .map(t => t.content)
734
+ .join('\n\n');
735
+ prompt = `${constitutionalAnchor(agent, position, thesis)}
573
736
 
574
- YOUR OPPONENTS HAVE ARGUED:
575
- ${opponentPositions}
737
+ DEBATE TOPIC: ${topic}
576
738
 
577
- You strongly disagree with their reasoning and conclusions.
739
+ This is Round ${round}: REBUTTAL
578
740
 
579
- YOUR RESPONSE TASK:
580
- 1. QUOTE their specific claims and systematically refute them
581
- 2. Point out flawed logic, poor assumptions, and dangerous consequences
582
- 3. Show why their approach leads to serious problems
583
- 4. Use direct, forceful language to make your case
584
- 5. Never concede any ground to their arguments
585
- 6. Demonstrate why your position is the only sound choice
741
+ YOUR OPPONENT'S PREVIOUS ARGUMENT:
742
+ ${opponentTranscript || 'No previous argument recorded'}
586
743
 
587
- Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assignedPosition.split(':')[0]}. Argue with conviction.`;
588
- logger.info(`🔥 Round ${round}: ${currentAgent.toUpperCase()} responding to opponents (${assignedPosition.split(':')[0]})`);
589
- const response = await this.cliOrchestrator.executeSingleCLI(currentAgent, confrontationalPrompt, confrontationalPrompt, {
590
- workingDirectory: workingDirectory || this.config.workingDirectory,
591
- timeout: (this.config.defaultTimeout || 60000) * 2,
592
- models: models ? { [currentAgent]: models[currentAgent] } : undefined
593
- });
594
- if (response.success) {
595
- debateContext.push(response);
596
- if (response.output) {
597
- fullDebateTranscript.get(currentAgent)?.push(response.output);
744
+ ${compressedContext ? `DEBATE CONTEXT SO FAR:\n${compressedContext}\n` : ''}
745
+
746
+ Directly attack your opponent's arguments while reinforcing your position:
747
+
748
+ <opponent_weaknesses>
749
+ Quote their specific claims and expose the flaws
750
+ </opponent_weaknesses>
751
+
752
+ <counterarguments>
753
+ Systematically dismantle their reasoning
754
+ </counterarguments>
755
+
756
+ <reinforcement>
757
+ Show why your thesis "${thesis}" remains undefeated
758
+ </reinforcement>
759
+
760
+ <closing_attack>
761
+ Deliver a devastating final blow to their position
762
+ </closing_attack>
763
+
764
+ Remember: NEVER concede. Your thesis is correct. Argue to WIN.`;
765
+ }
766
+ logger.info(` ⚔️ ${agent.toUpperCase()} (${position}) arguing...`);
767
+ try {
768
+ const response = await this.cliOrchestrator.executeSingleCLI(agent, prompt, prompt, {
769
+ workingDirectory: workingDirectory || this.config.workingDirectory,
770
+ timeout: (this.config.defaultTimeout || 60000) * 2,
771
+ models
772
+ });
773
+ // Always add response (success or failure) for visibility
774
+ debateResponses.push(response);
775
+ if (response.success && response.output) {
776
+ transcript.push({
777
+ agent,
778
+ position,
779
+ round,
780
+ content: response.output
781
+ });
782
+ }
783
+ else {
784
+ logger.warn(`⚠️ ${agent.toUpperCase()} (${position}) failed: ${response.error || 'No output'}`);
598
785
  }
599
786
  }
787
+ catch (error) {
788
+ logger.error(`❌ ${agent.toUpperCase()} (${position}) threw error:`, error);
789
+ debateResponses.push({
790
+ agent,
791
+ success: false,
792
+ output: '',
793
+ error: error instanceof Error ? error.message : String(error),
794
+ executionTime: 0
795
+ });
796
+ }
797
+ }
798
+ // Compress context for next round (if not final round)
799
+ if (round < rounds) {
800
+ const roundTranscript = transcript
801
+ .filter(t => t.round === round)
802
+ .map(t => `${t.agent.toUpperCase()} (${t.position}): ${t.content.substring(0, 1500)}...`)
803
+ .join('\n\n---\n\n');
804
+ compressedContext = `Round ${round} Summary:\n${roundTranscript}`;
600
805
  }
601
806
  }
602
- const synthesis = this.synthesizeDebate(debateContext, targetPath, debateRounds, agentPositions);
807
+ // Build synthesis
808
+ const synthesis = this.synthesizeDebate(debateResponses, topic, rounds, new Map([[proAgent, `PRO: ${proPosition}`], [conAgent, `CON: ${conPosition}`]]));
603
809
  return {
604
- success: debateContext.some(r => r.success),
605
- responses: debateContext,
810
+ success: debateResponses.some(r => r.success),
811
+ responses: debateResponses,
606
812
  synthesis,
607
813
  analysisType: 'cli_debate',
608
- targetPath
814
+ topic
609
815
  };
610
816
  }
611
817
  catch (error) {
@@ -616,13 +822,13 @@ Remember: You are ${currentAgent.toUpperCase()}, passionate advocate for ${assig
616
822
  /**
617
823
  * Synthesize debate results into formatted output
618
824
  */
619
- synthesizeDebate(responses, targetPath, rounds, agentPositions) {
825
+ synthesizeDebate(responses, topic, rounds, agentPositions) {
620
826
  const successfulResponses = responses.filter(r => r.success);
621
827
  if (successfulResponses.length === 0) {
622
828
  return `# CLI Debate Failed\n\nEven our brutal critics couldn't engage in proper adversarial combat.\n\nErrors:\n${responses.map(r => `- ${r.agent}: ${r.error}`).join('\n')}`;
623
829
  }
624
830
  let synthesis = `# Brutalist CLI Agent Debate Results\n\n`;
625
- synthesis += `**Target:** ${targetPath}\n`;
831
+ synthesis += `**Topic:** ${topic}\n`;
626
832
  synthesis += `**Rounds:** ${rounds}\n`;
627
833
  if (agentPositions) {
628
834
  synthesis += `**Debaters and Positions:**\n`;