@brutalist/mcp 0.9.3 → 1.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.
- package/README.md +12 -6
- package/dist/brutalist-server.d.ts +13 -4
- package/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +357 -151
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-agents.d.ts +1 -1
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +55 -47
- package/dist/cli-agents.js.map +1 -1
- package/dist/domains/argument-space.d.ts +3 -3
- package/dist/domains/argument-space.js +9 -9
- package/dist/domains/argument-space.js.map +1 -1
- package/dist/domains/critique-domain.d.ts +12 -0
- package/dist/domains/critique-domain.d.ts.map +1 -1
- package/dist/domains/critique-domain.js +12 -1
- package/dist/domains/critique-domain.js.map +1 -1
- package/dist/generators/tool-generator.d.ts.map +1 -1
- package/dist/generators/tool-generator.js +5 -5
- package/dist/generators/tool-generator.js.map +1 -1
- package/dist/handlers/tool-handler.d.ts.map +1 -1
- package/dist/handlers/tool-handler.js +18 -10
- package/dist/handlers/tool-handler.js.map +1 -1
- package/dist/registry/domains.d.ts +10 -0
- package/dist/registry/domains.d.ts.map +1 -1
- package/dist/registry/domains.js +153 -11
- package/dist/registry/domains.js.map +1 -1
- package/dist/system-prompts.d.ts +8 -0
- package/dist/system-prompts.d.ts.map +1 -0
- package/dist/system-prompts.js +596 -0
- package/dist/system-prompts.js.map +1 -0
- package/dist/tool-definitions.d.ts +20 -1
- package/dist/tool-definitions.d.ts.map +1 -1
- package/dist/tool-definitions.js +42 -213
- package/dist/tool-definitions.js.map +1 -1
- package/dist/tool-router.d.ts +12 -0
- package/dist/tool-router.d.ts.map +1 -0
- package/dist/tool-router.js +59 -0
- package/dist/tool-router.js.map +1 -0
- package/dist/types/brutalist.d.ts +1 -0
- package/dist/types/brutalist.d.ts.map +1 -1
- package/dist/types/tool-config.d.ts +4 -3
- package/dist/types/tool-config.d.ts.map +1 -1
- package/dist/types/tool-config.js +7 -6
- package/dist/types/tool-config.js.map +1 -1
- package/package.json +1 -1
package/dist/brutalist-server.js
CHANGED
|
@@ -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
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
//
|
|
282
|
-
this.server.tool("
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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()
|
|
289
|
-
codex: z.string().optional()
|
|
290
|
-
gemini: z.string().optional()
|
|
291
|
-
}).optional().describe("
|
|
292
|
-
// Pagination
|
|
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
|
|
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
|
-
|
|
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
|
|
318
|
-
roster += "
|
|
319
|
-
roster += "
|
|
320
|
-
roster += "
|
|
321
|
-
roster += "- `
|
|
322
|
-
roster += "- `
|
|
323
|
-
roster += "- `
|
|
324
|
-
roster += "- `
|
|
325
|
-
roster += "
|
|
326
|
-
roster += "
|
|
327
|
-
roster += "- `
|
|
328
|
-
roster += "- `
|
|
329
|
-
roster += "- `
|
|
330
|
-
roster += "- `
|
|
331
|
-
roster += "
|
|
332
|
-
roster += "- `
|
|
333
|
-
roster += "
|
|
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: `
|
|
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: `
|
|
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
|
|
365
|
-
|
|
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.
|
|
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
|
|
527
|
+
`Provide your follow-up in the topic field.`);
|
|
398
528
|
}
|
|
399
|
-
logger.info(`💬 Debate continuation - new prompt: "${args.
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
466
|
-
const result = await this.executeCLIDebate(
|
|
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.
|
|
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',
|
|
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
|
|
640
|
+
* Execute CLI debate with constitutional position anchoring.
|
|
641
|
+
* 2 agents, explicit PRO/CON positions, context compression between rounds.
|
|
500
642
|
*/
|
|
501
|
-
async executeCLIDebate(
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
647
|
+
// Get available CLIs
|
|
509
648
|
const cliContext = await this.cliOrchestrator.detectCLIContext();
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
512
|
-
throw new Error(`Need at least 2 CLI agents for debate. Available: ${
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
533
|
-
CONTEXT: ${context || ''}
|
|
680
|
+
YOUR THESIS: ${thesis}
|
|
534
681
|
|
|
535
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
575
|
-
${opponentPositions}
|
|
737
|
+
DEBATE TOPIC: ${topic}
|
|
576
738
|
|
|
577
|
-
|
|
739
|
+
This is Round ${round}: REBUTTAL
|
|
578
740
|
|
|
579
|
-
YOUR
|
|
580
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
807
|
+
// Build synthesis
|
|
808
|
+
const synthesis = this.synthesizeDebate(debateResponses, topic, rounds, new Map([[proAgent, `PRO: ${proPosition}`], [conAgent, `CON: ${conPosition}`]]));
|
|
603
809
|
return {
|
|
604
|
-
success:
|
|
605
|
-
responses:
|
|
810
|
+
success: debateResponses.some(r => r.success),
|
|
811
|
+
responses: debateResponses,
|
|
606
812
|
synthesis,
|
|
607
813
|
analysisType: 'cli_debate',
|
|
608
|
-
|
|
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,
|
|
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 += `**
|
|
831
|
+
synthesis += `**Topic:** ${topic}\n`;
|
|
626
832
|
synthesis += `**Rounds:** ${rounds}\n`;
|
|
627
833
|
if (agentPositions) {
|
|
628
834
|
synthesis += `**Debaters and Positions:**\n`;
|