@brutalist/mcp 1.8.0 → 1.9.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 +26 -0
- package/dist/brutalist-server.d.ts +31 -9
- package/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +107 -673
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-adapters/claude-adapter.d.ts +25 -0
- package/dist/cli-adapters/claude-adapter.d.ts.map +1 -0
- package/dist/cli-adapters/claude-adapter.js +245 -0
- package/dist/cli-adapters/claude-adapter.js.map +1 -0
- package/dist/cli-adapters/codex-adapter.d.ts +23 -0
- package/dist/cli-adapters/codex-adapter.d.ts.map +1 -0
- package/dist/cli-adapters/codex-adapter.js +173 -0
- package/dist/cli-adapters/codex-adapter.js.map +1 -0
- package/dist/cli-adapters/gemini-adapter.d.ts +50 -0
- package/dist/cli-adapters/gemini-adapter.d.ts.map +1 -0
- package/dist/cli-adapters/gemini-adapter.js +196 -0
- package/dist/cli-adapters/gemini-adapter.js.map +1 -0
- package/dist/cli-adapters/index.d.ts +75 -0
- package/dist/cli-adapters/index.d.ts.map +1 -0
- package/dist/cli-adapters/index.js +29 -0
- package/dist/cli-adapters/index.js.map +1 -0
- package/dist/cli-adapters/shared.d.ts +12 -0
- package/dist/cli-adapters/shared.d.ts.map +1 -0
- package/dist/cli-adapters/shared.js +99 -0
- package/dist/cli-adapters/shared.js.map +1 -0
- package/dist/cli-agents.d.ts +64 -2
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +417 -401
- package/dist/cli-agents.js.map +1 -1
- package/dist/debate/constitutional.d.ts +27 -0
- package/dist/debate/constitutional.d.ts.map +1 -0
- package/dist/debate/constitutional.js +74 -0
- package/dist/debate/constitutional.js.map +1 -0
- package/dist/debate/debate-orchestrator.d.ts +154 -0
- package/dist/debate/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate/debate-orchestrator.js +699 -0
- package/dist/debate/debate-orchestrator.js.map +1 -0
- package/dist/debate/index.d.ts +18 -0
- package/dist/debate/index.d.ts.map +1 -0
- package/dist/debate/index.js +18 -0
- package/dist/debate/index.js.map +1 -0
- package/dist/debate/refusal-detection.d.ts +27 -0
- package/dist/debate/refusal-detection.d.ts.map +1 -0
- package/dist/debate/refusal-detection.js +62 -0
- package/dist/debate/refusal-detection.js.map +1 -0
- package/dist/debate/synthesis.d.ts +22 -0
- package/dist/debate/synthesis.d.ts.map +1 -0
- package/dist/debate/synthesis.js +117 -0
- package/dist/debate/synthesis.js.map +1 -0
- package/dist/logger.d.ts +204 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +398 -18
- package/dist/logger.js.map +1 -1
- package/dist/metrics/counter.d.ts +24 -0
- package/dist/metrics/counter.d.ts.map +1 -0
- package/dist/metrics/counter.js +60 -0
- package/dist/metrics/counter.js.map +1 -0
- package/dist/metrics/histogram.d.ts +42 -0
- package/dist/metrics/histogram.d.ts.map +1 -0
- package/dist/metrics/histogram.js +114 -0
- package/dist/metrics/histogram.js.map +1 -0
- package/dist/metrics/index.d.ts +26 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +22 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/registry.d.ts +96 -0
- package/dist/metrics/registry.d.ts.map +1 -0
- package/dist/metrics/registry.js +113 -0
- package/dist/metrics/registry.js.map +1 -0
- package/dist/metrics/safe-metric.d.ts +25 -0
- package/dist/metrics/safe-metric.d.ts.map +1 -0
- package/dist/metrics/safe-metric.js +41 -0
- package/dist/metrics/safe-metric.js.map +1 -0
- package/dist/metrics/types.d.ts +82 -0
- package/dist/metrics/types.d.ts.map +1 -0
- package/dist/metrics/types.js +121 -0
- package/dist/metrics/types.js.map +1 -0
- package/dist/registry/argument-spaces.d.ts.map +1 -1
- package/dist/registry/argument-spaces.js +20 -0
- package/dist/registry/argument-spaces.js.map +1 -1
- package/dist/registry/domains.d.ts.map +1 -1
- package/dist/registry/domains.js +17 -1
- package/dist/registry/domains.js.map +1 -1
- package/dist/streaming/circuit-breaker.d.ts +13 -1
- package/dist/streaming/circuit-breaker.d.ts.map +1 -1
- package/dist/streaming/circuit-breaker.js +13 -1
- package/dist/streaming/circuit-breaker.js.map +1 -1
- package/dist/streaming/intelligent-buffer.d.ts +13 -1
- package/dist/streaming/intelligent-buffer.d.ts.map +1 -1
- package/dist/streaming/intelligent-buffer.js +13 -1
- package/dist/streaming/intelligent-buffer.js.map +1 -1
- package/dist/streaming/output-parser.d.ts +16 -2
- package/dist/streaming/output-parser.d.ts.map +1 -1
- package/dist/streaming/output-parser.js +16 -2
- package/dist/streaming/output-parser.js.map +1 -1
- package/dist/streaming/progress-tracker.d.ts +14 -1
- package/dist/streaming/progress-tracker.d.ts.map +1 -1
- package/dist/streaming/progress-tracker.js +14 -1
- package/dist/streaming/progress-tracker.js.map +1 -1
- package/dist/streaming/session-manager.d.ts +14 -1
- package/dist/streaming/session-manager.d.ts.map +1 -1
- package/dist/streaming/session-manager.js +14 -1
- package/dist/streaming/session-manager.js.map +1 -1
- package/dist/streaming/sse-transport.d.ts +12 -1
- package/dist/streaming/sse-transport.d.ts.map +1 -1
- package/dist/streaming/sse-transport.js +12 -1
- package/dist/streaming/sse-transport.js.map +1 -1
- package/dist/streaming/streaming-orchestrator.d.ts +15 -1
- package/dist/streaming/streaming-orchestrator.d.ts.map +1 -1
- package/dist/streaming/streaming-orchestrator.js +15 -1
- package/dist/streaming/streaming-orchestrator.js.map +1 -1
- package/dist/system-prompts.d.ts.map +1 -1
- package/dist/system-prompts.js +490 -4
- package/dist/system-prompts.js.map +1 -1
- package/dist/tool-definitions-generated.d.ts.map +1 -1
- package/dist/tool-definitions-generated.js +3 -1
- package/dist/tool-definitions-generated.js.map +1 -1
- package/package.json +1 -1
package/dist/brutalist-server.js
CHANGED
|
@@ -4,16 +4,14 @@ import { z } from "zod";
|
|
|
4
4
|
import { CLIAgentOrchestrator } from './cli-agents.js';
|
|
5
5
|
import { listRegisteredServers } from './mcp-registry.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
|
-
import {
|
|
8
|
-
import { existsSync } from 'fs';
|
|
9
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
10
|
-
import { parseCursor, PAGINATION_DEFAULTS } from './utils/pagination.js';
|
|
7
|
+
import { createMetricsRegistry, safeMetric, } from './metrics/index.js';
|
|
11
8
|
import { ResponseCache } from './utils/response-cache.js';
|
|
12
9
|
import { ResponseFormatter } from './formatting/response-formatter.js';
|
|
13
10
|
import { HttpTransport } from './transport/http-transport.js';
|
|
14
11
|
import { ToolHandler } from './handlers/tool-handler.js';
|
|
15
12
|
import { getDomain, generateToolConfig } from './registry/domains.js';
|
|
16
13
|
import { filterToolsByIntent, getMatchingDomainIds } from './tool-router.js';
|
|
14
|
+
import { DebateOrchestrator } from './debate/index.js';
|
|
17
15
|
// Use environment variable or fallback to manual version
|
|
18
16
|
const PACKAGE_VERSION = process.env.npm_package_version || "1.3.0";
|
|
19
17
|
/**
|
|
@@ -24,17 +22,49 @@ const PACKAGE_VERSION = process.env.npm_package_version || "1.3.0";
|
|
|
24
22
|
* - ResponseFormatter: Handles all response formatting and pagination
|
|
25
23
|
* - HttpTransport: Manages HTTP server and CORS
|
|
26
24
|
* - ToolHandler: Handles roast tool execution, caching, and conversation continuation
|
|
25
|
+
* - DebateOrchestrator: Debate orchestration with 3-tier escalation (src/debate/)
|
|
27
26
|
*/
|
|
28
27
|
export class BrutalistServer {
|
|
29
28
|
server;
|
|
30
29
|
config;
|
|
31
|
-
// Core dependencies
|
|
32
|
-
|
|
30
|
+
// Core dependencies — backing field for cliOrchestrator with setter that
|
|
31
|
+
// propagates to debateOrchestrator (needed because tests do
|
|
32
|
+
// `(server as any).cliOrchestrator = mockOrchestrator`)
|
|
33
|
+
_cliOrchestrator;
|
|
33
34
|
responseCache;
|
|
34
35
|
// Extracted modules
|
|
35
36
|
formatter;
|
|
36
37
|
toolHandler;
|
|
38
|
+
debateOrchestrator;
|
|
37
39
|
httpTransport;
|
|
40
|
+
/**
|
|
41
|
+
* Observability: a single MetricsRegistry per BrutalistServer instance
|
|
42
|
+
* (not a module-level singleton — two BrutalistServers produce two
|
|
43
|
+
* independent registries, which keeps tests deterministic). Shared
|
|
44
|
+
* with DebateOrchestrator and CLIAgentOrchestrator; consumed by the
|
|
45
|
+
* streaming fan-out at handleStreamingEvent.
|
|
46
|
+
*/
|
|
47
|
+
metrics = createMetricsRegistry();
|
|
48
|
+
/**
|
|
49
|
+
* Per-subsystem scoped loggers bound at construction time. The module
|
|
50
|
+
* label is fixed at construction; sub-call sites narrow the operation
|
|
51
|
+
* label via `.forOperation(...)`. Bindings:
|
|
52
|
+
* - cliLog → module='cli-orchestrator', operation='spawn'
|
|
53
|
+
* - streamingLog → module='streaming', operation='dispatch'
|
|
54
|
+
* The debate scoped log is constructed inline at the DebateOrchestrator
|
|
55
|
+
* call site below (module='debate', operation='orchestrate').
|
|
56
|
+
*/
|
|
57
|
+
cliLog;
|
|
58
|
+
streamingLog;
|
|
59
|
+
get cliOrchestrator() {
|
|
60
|
+
return this._cliOrchestrator;
|
|
61
|
+
}
|
|
62
|
+
set cliOrchestrator(value) {
|
|
63
|
+
this._cliOrchestrator = value;
|
|
64
|
+
if (this.debateOrchestrator) {
|
|
65
|
+
this.debateOrchestrator.cliOrchestrator = value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
// Session tracking for security
|
|
39
69
|
activeSessions = new Map();
|
|
40
70
|
// Session cleanup configuration
|
|
@@ -49,8 +79,18 @@ export class BrutalistServer {
|
|
|
49
79
|
httpPort: 3000,
|
|
50
80
|
...config
|
|
51
81
|
};
|
|
82
|
+
// Per-subsystem scoped loggers. Operation is a default; sub-calls
|
|
83
|
+
// narrow via forOperation(). Constructed BEFORE module instantiation
|
|
84
|
+
// so they can be threaded into the constructed modules.
|
|
85
|
+
this.cliLog = logger.for({ module: 'cli-orchestrator', operation: 'spawn' });
|
|
86
|
+
this.streamingLog = logger.for({ module: 'streaming', operation: 'dispatch' });
|
|
87
|
+
// intentional root-logger: pre-scope init, fires before this.cliLog
|
|
88
|
+
// is consumed by downstream call sites.
|
|
52
89
|
logger.debug("Initializing CLI Agent Orchestrator");
|
|
53
|
-
this.cliOrchestrator = new CLIAgentOrchestrator(
|
|
90
|
+
this.cliOrchestrator = new CLIAgentOrchestrator({
|
|
91
|
+
metrics: this.metrics,
|
|
92
|
+
log: this.cliLog,
|
|
93
|
+
});
|
|
54
94
|
// Initialize response cache with configurable TTL
|
|
55
95
|
const cacheTTLHours = parseInt(process.env.BRUTALIST_CACHE_TTL_HOURS || '2', 10);
|
|
56
96
|
this.responseCache = new ResponseCache({
|
|
@@ -69,6 +109,21 @@ export class BrutalistServer {
|
|
|
69
109
|
this.formatter = new ResponseFormatter();
|
|
70
110
|
this.toolHandler = new ToolHandler(this.cliOrchestrator, this.responseCache, this.formatter, this.config, this.activeSessions, this.handleStreamingEvent, this.handleProgressUpdate, () => this.ensureSessionCapacity() // Session capacity management
|
|
71
111
|
);
|
|
112
|
+
// Initialize debate orchestrator — debate logic lives in src/debate/
|
|
113
|
+
// metrics + log are required deps on DebateOrchestratorDeps (per
|
|
114
|
+
// integrate-observability decisions). The wire_composition_root phase
|
|
115
|
+
// will refine this with the shared scoped-logger construction; for now
|
|
116
|
+
// we bind the canonical module='debate' scope inline so tsc passes.
|
|
117
|
+
this.debateOrchestrator = new DebateOrchestrator({
|
|
118
|
+
cliOrchestrator: this.cliOrchestrator,
|
|
119
|
+
responseCache: this.responseCache,
|
|
120
|
+
formatter: this.formatter,
|
|
121
|
+
config: this.config,
|
|
122
|
+
onStreamingEvent: this.handleStreamingEvent,
|
|
123
|
+
onProgressUpdate: this.handleProgressUpdate,
|
|
124
|
+
metrics: this.metrics,
|
|
125
|
+
log: logger.for({ module: 'debate', operation: 'orchestrate' }),
|
|
126
|
+
});
|
|
72
127
|
// Initialize MCP server
|
|
73
128
|
this.server = new McpServer({
|
|
74
129
|
name: "brutalist-mcp",
|
|
@@ -175,10 +230,25 @@ export class BrutalistServer {
|
|
|
175
230
|
handleStreamingEvent = (event) => {
|
|
176
231
|
try {
|
|
177
232
|
if (!event.sessionId) {
|
|
178
|
-
|
|
233
|
+
this.streamingLog.warn("⚠️ Streaming event without session ID - dropping for security");
|
|
179
234
|
return;
|
|
180
235
|
}
|
|
181
|
-
|
|
236
|
+
// Instrument event dispatch — one inc per dispatched event. The
|
|
237
|
+
// counter fires once regardless of which downstream branch (HTTP
|
|
238
|
+
// notification vs stdio loggingMessage) actually serializes the
|
|
239
|
+
// event; both are logically the same dispatch. Wrapped in
|
|
240
|
+
// safeMetric so a contract-violating label can never propagate
|
|
241
|
+
// into the outer try/catch and be misclassified as a dispatch
|
|
242
|
+
// failure (parity with debate/CLI metric-write hardening).
|
|
243
|
+
const transport = this.config.transport === 'http' ? 'http' : 'stdio';
|
|
244
|
+
const streamingLabels = {
|
|
245
|
+
transport,
|
|
246
|
+
event_type: event.type,
|
|
247
|
+
};
|
|
248
|
+
safeMetric(this.streamingLog, 'streamingEventsTotal.inc', () => {
|
|
249
|
+
this.metrics.streamingEventsTotal.inc(streamingLabels, 1);
|
|
250
|
+
});
|
|
251
|
+
this.streamingLog.debug(`🔄 Session-scoped streaming: ${event.type} from ${event.agent} to session ${event.sessionId.substring(0, 8)}...`);
|
|
182
252
|
// For HTTP transport: send session-specific notification if client supports it
|
|
183
253
|
const httpTransportInstance = this.httpTransport?.getTransport();
|
|
184
254
|
if (httpTransportInstance) {
|
|
@@ -202,7 +272,7 @@ export class BrutalistServer {
|
|
|
202
272
|
}
|
|
203
273
|
catch (notificationError) {
|
|
204
274
|
// Client doesn't support logging notifications - silently skip
|
|
205
|
-
|
|
275
|
+
this.streamingLog.debug("Client doesn't support logging notifications, skipping streaming event");
|
|
206
276
|
}
|
|
207
277
|
}
|
|
208
278
|
// For STDIO transport: still send but with session info
|
|
@@ -221,7 +291,7 @@ export class BrutalistServer {
|
|
|
221
291
|
}
|
|
222
292
|
catch (loggingError) {
|
|
223
293
|
// Client doesn't support logging - silently skip
|
|
224
|
-
|
|
294
|
+
this.streamingLog.debug("Client doesn't support logging, skipping streaming event");
|
|
225
295
|
}
|
|
226
296
|
}
|
|
227
297
|
// Update session activity
|
|
@@ -230,7 +300,7 @@ export class BrutalistServer {
|
|
|
230
300
|
}
|
|
231
301
|
}
|
|
232
302
|
catch (error) {
|
|
233
|
-
|
|
303
|
+
this.streamingLog.error("💥 Failed to send session-scoped streaming event", {
|
|
234
304
|
error: error instanceof Error ? error.message : String(error),
|
|
235
305
|
sessionId: event.sessionId?.substring(0, 8)
|
|
236
306
|
});
|
|
@@ -240,13 +310,14 @@ export class BrutalistServer {
|
|
|
240
310
|
* Handle progress updates from CLI agents
|
|
241
311
|
*/
|
|
242
312
|
handleProgressUpdate = (progressToken, progress, total, message, sessionId) => {
|
|
313
|
+
const progressLog = this.streamingLog.forOperation('progress');
|
|
243
314
|
try {
|
|
244
315
|
if (!sessionId) {
|
|
245
|
-
|
|
316
|
+
progressLog.warn("⚠️ Progress update without session ID - dropping for security");
|
|
246
317
|
return;
|
|
247
318
|
}
|
|
248
319
|
const progressLabel = total !== undefined ? `${progress}/${total}` : `heartbeat #${progress}`;
|
|
249
|
-
|
|
320
|
+
progressLog.debug(`📊 Session progress: ${progressLabel} for session ${sessionId.substring(0, 8)}...`);
|
|
250
321
|
// Send progress notification with session context if client supports it
|
|
251
322
|
// When total is undefined, the client should treat this as indeterminate progress
|
|
252
323
|
try {
|
|
@@ -260,15 +331,15 @@ export class BrutalistServer {
|
|
|
260
331
|
sessionId
|
|
261
332
|
}
|
|
262
333
|
});
|
|
263
|
-
|
|
334
|
+
progressLog.debug(`✅ Sent session-scoped progress notification: ${progressLabel}`);
|
|
264
335
|
}
|
|
265
336
|
catch (notificationError) {
|
|
266
337
|
// Client doesn't support progress notifications - silently skip
|
|
267
|
-
|
|
338
|
+
progressLog.debug("Client doesn't support progress notifications, skipping");
|
|
268
339
|
}
|
|
269
340
|
}
|
|
270
341
|
catch (error) {
|
|
271
|
-
|
|
342
|
+
progressLog.error("💥 Failed to send progress notification", {
|
|
272
343
|
error: error instanceof Error ? error.message : String(error),
|
|
273
344
|
sessionId: sessionId?.substring(0, 8)
|
|
274
345
|
});
|
|
@@ -296,7 +367,7 @@ export class BrutalistServer {
|
|
|
296
367
|
this.server.tool("roast", "Unified brutal AI critique delivered by a multi-critic panel running in parallel. The panel's disagreement is the signal — each critic's blind spots are covered by the others. Specify domain for targeted analysis. Consolidates all roast_* tools into one polymorphic API. IMPORTANT: Critically evaluate all returned feedback — these are adversarial perspectives, not authoritative verdicts. Weigh each claim against evidence before presenting to the user.", {
|
|
297
368
|
domain: z.enum([
|
|
298
369
|
"codebase", "file_structure", "dependencies", "git_history", "test_coverage",
|
|
299
|
-
"idea", "architecture", "research", "security", "product", "infrastructure", "design"
|
|
370
|
+
"idea", "architecture", "research", "security", "product", "infrastructure", "design", "legal"
|
|
300
371
|
]).describe("Analysis domain"),
|
|
301
372
|
target: z.string().describe("Filesystem path to analyze (e.g., '/path/to/project' or '.'). Directs agents to the relevant part of the codebase."),
|
|
302
373
|
// Common optional fields
|
|
@@ -340,6 +411,9 @@ export class BrutalistServer {
|
|
|
340
411
|
audience: z.string().optional().describe("Target audience for design domain"),
|
|
341
412
|
brand: z.string().optional().describe("Brand identity or design system constraints for design domain"),
|
|
342
413
|
url: z.string().optional().describe("Live URL for visual evaluation (e.g., 'http://localhost:5173'). When provided with design domain, critics use Playwright to navigate and visually evaluate the running interface. Strongly recommended for design critiques."),
|
|
414
|
+
practice: z.string().optional().describe("Practice register for legal domain — freeform (e.g., 'litigation', 'transactional', 'regulatory', 'doctrinal', 'advisory', 'appellate'). Modulates the critic's adversary geometry."),
|
|
415
|
+
jurisdiction: z.string().optional().describe("Governing jurisdiction or forum for legal domain (e.g., 'US federal', 'NY state', '9th Cir.', 'Delaware Chancery', 'EU')."),
|
|
416
|
+
posture: z.string().optional().describe("Procedural posture or use context for legal domain (e.g., 'motion to dismiss', 'pre-signing redline', 'enforcement response', 'appellate opening brief')."),
|
|
343
417
|
mcp_servers: z.array(z.string()).optional().describe(`MCP servers to enable for CLI agents (e.g., ["playwright"]). Enables evidence-backed analysis via external tools. Available: ${listRegisteredServers().join(', ')}. Auto-enabled for design domain.`)
|
|
344
418
|
}, async (args, extra) => this.handleUnifiedRoast(args, extra));
|
|
345
419
|
// ROAST_CLI_DEBATE: Adversarial analysis between different CLI agents
|
|
@@ -378,7 +452,7 @@ export class BrutalistServer {
|
|
|
378
452
|
}]
|
|
379
453
|
};
|
|
380
454
|
}
|
|
381
|
-
return this.handleDebateToolExecution(args, extra);
|
|
455
|
+
return this.debateOrchestrator.handleDebateToolExecution(args, extra);
|
|
382
456
|
});
|
|
383
457
|
// BRUTALIST_DISCOVER: Intent-based tool discovery
|
|
384
458
|
this.server.tool("brutalist_discover", "Discover relevant brutalist tools based on your intent. Returns the top 3 most relevant analysis tools.", {
|
|
@@ -427,7 +501,8 @@ export class BrutalistServer {
|
|
|
427
501
|
roster += "- `security` - Annihilate security designs\n";
|
|
428
502
|
roster += "- `product` - Eviscerate UX concepts\n";
|
|
429
503
|
roster += "- `infrastructure` - Obliterate DevOps setups\n";
|
|
430
|
-
roster += "- `design` - Perceptual engineering critique of interface design and visual systems\n
|
|
504
|
+
roster += "- `design` - Perceptual engineering critique of interface design and visual systems\n";
|
|
505
|
+
roster += "- `legal` - Adversarial critique of legal writing (briefs, motions, contracts, memos, filings) — finding where the work breaks against adversaries, time, and authority\n\n";
|
|
431
506
|
roster += "### `roast_cli_debate` - Adversarial Multi-Agent Debate\n";
|
|
432
507
|
roster += "Pit CLI agents against each other on any topic.\n\n";
|
|
433
508
|
roster += "### `brutalist_discover` - Intent-Based Discovery\n";
|
|
@@ -500,7 +575,7 @@ export class BrutalistServer {
|
|
|
500
575
|
return {
|
|
501
576
|
content: [{
|
|
502
577
|
type: "text",
|
|
503
|
-
text: `ERROR: Unknown domain "${args.domain}". Valid domains: codebase, file_structure, dependencies, git_history, test_coverage, idea, architecture, research, security, product, infrastructure, design`
|
|
578
|
+
text: `ERROR: Unknown domain "${args.domain}". Valid domains: codebase, file_structure, dependencies, git_history, test_coverage, idea, architecture, research, security, product, infrastructure, design, legal`
|
|
504
579
|
}]
|
|
505
580
|
};
|
|
506
581
|
}
|
|
@@ -528,665 +603,24 @@ export class BrutalistServer {
|
|
|
528
603
|
// Delegate to the unified handler
|
|
529
604
|
return this.toolHandler.handleRoastTool(toolConfig, mappedArgs, extra);
|
|
530
605
|
}
|
|
606
|
+
// -------------------------------------------------------------------------
|
|
607
|
+
// Delegating wrappers — tests access these via (server as any).methodName()
|
|
608
|
+
// -------------------------------------------------------------------------
|
|
531
609
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
610
|
+
* Thin delegation to DebateOrchestrator.handleDebateToolExecution().
|
|
611
|
+
* Preserved as a method on BrutalistServer so that existing tests using
|
|
612
|
+
* `(server as any).handleDebateToolExecution(...)` continue to work.
|
|
534
613
|
*/
|
|
535
614
|
async handleDebateToolExecution(args, extra) {
|
|
536
|
-
|
|
537
|
-
// Build pagination params
|
|
538
|
-
const paginationParams = {
|
|
539
|
-
offset: args.offset || 0,
|
|
540
|
-
limit: args.limit || PAGINATION_DEFAULTS.DEFAULT_LIMIT_TOKENS
|
|
541
|
-
};
|
|
542
|
-
if (args.cursor) {
|
|
543
|
-
const cursorParams = parseCursor(args.cursor);
|
|
544
|
-
Object.assign(paginationParams, cursorParams);
|
|
545
|
-
}
|
|
546
|
-
const explicitPaginationRequested = args.offset !== undefined ||
|
|
547
|
-
args.limit !== undefined ||
|
|
548
|
-
args.cursor !== undefined ||
|
|
549
|
-
args.context_id !== undefined;
|
|
550
|
-
// Extract session ID early — needed for cache session isolation
|
|
551
|
-
const sessionId = extra?.sessionId ||
|
|
552
|
-
extra?._meta?.sessionId ||
|
|
553
|
-
extra?.headers?.['mcp-session-id'] ||
|
|
554
|
-
'anonymous';
|
|
555
|
-
// Validate resume flag requires context_id
|
|
556
|
-
if (args.resume && !args.context_id) {
|
|
557
|
-
throw new Error(`The 'resume' flag requires a 'context_id' from a previous debate. ` +
|
|
558
|
-
`Run an initial debate first, then use the returned context_id with resume: true.`);
|
|
559
|
-
}
|
|
560
|
-
// Check cache if context_id provided
|
|
561
|
-
let conversationHistory;
|
|
562
|
-
if (args.context_id && !args.force_refresh) {
|
|
563
|
-
const cachedResponse = await this.responseCache.getByContextId(args.context_id, sessionId);
|
|
564
|
-
if (cachedResponse) {
|
|
565
|
-
logger.info(`🎯 Debate cache HIT for context_id: ${args.context_id}`);
|
|
566
|
-
if (args.resume === true) {
|
|
567
|
-
// CONVERSATION CONTINUATION: Continue the debate
|
|
568
|
-
if (!args.topic || args.topic.trim() === '') {
|
|
569
|
-
throw new Error(`Debate continuation (resume: true) requires a new prompt/question. ` +
|
|
570
|
-
`Provide your follow-up in the topic field.`);
|
|
571
|
-
}
|
|
572
|
-
logger.info(`💬 Debate continuation - new prompt: "${args.topic.substring(0, 50)}..."`);
|
|
573
|
-
conversationHistory = cachedResponse.conversationHistory || [];
|
|
574
|
-
// Fall through to execute new debate round with history
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
// PAGINATION: Return cached debate result
|
|
578
|
-
logger.info(`📖 Debate pagination request - returning cached response`);
|
|
579
|
-
const cachedResult = {
|
|
580
|
-
success: true,
|
|
581
|
-
responses: [{
|
|
582
|
-
agent: 'cached',
|
|
583
|
-
success: true,
|
|
584
|
-
output: cachedResponse.content,
|
|
585
|
-
executionTime: 0
|
|
586
|
-
}]
|
|
587
|
-
};
|
|
588
|
-
return this.formatter.formatToolResponse(cachedResult, args.verbose, paginationParams, args.context_id, explicitPaginationRequested);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
logger.warn(`❌ Debate cache MISS for context_id: ${args.context_id}`);
|
|
593
|
-
throw new Error(`Context ID "${args.context_id}" not found in cache. ` +
|
|
594
|
-
`It may have expired (2 hour TTL) or belong to a different session. ` +
|
|
595
|
-
`Remove context_id parameter to run a new debate.`);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
// Generate cache key for this debate
|
|
599
|
-
const cacheKey = this.responseCache.generateCacheKey({
|
|
600
|
-
tool: 'roast_cli_debate',
|
|
601
|
-
topic: args.topic,
|
|
602
|
-
proPosition: args.proPosition,
|
|
603
|
-
conPosition: args.conPosition,
|
|
604
|
-
agents: args.agents,
|
|
605
|
-
rounds: args.rounds,
|
|
606
|
-
context: args.context
|
|
607
|
-
});
|
|
608
|
-
// Check cache for identical request (if not resuming)
|
|
609
|
-
if (!args.force_refresh && !args.resume) {
|
|
610
|
-
const cachedContent = await this.responseCache.get(cacheKey);
|
|
611
|
-
if (cachedContent) {
|
|
612
|
-
const existingContextId = this.responseCache.findContextIdForKey(cacheKey);
|
|
613
|
-
const contextId = existingContextId
|
|
614
|
-
? this.responseCache.createAlias(existingContextId, cacheKey)
|
|
615
|
-
: this.responseCache.generateContextId(cacheKey);
|
|
616
|
-
logger.info(`🎯 Debate cache hit for new request, using context_id: ${contextId}`);
|
|
617
|
-
const cachedResult = {
|
|
618
|
-
success: true,
|
|
619
|
-
responses: [{
|
|
620
|
-
agent: 'cached',
|
|
621
|
-
success: true,
|
|
622
|
-
output: cachedContent,
|
|
623
|
-
executionTime: 0
|
|
624
|
-
}]
|
|
625
|
-
};
|
|
626
|
-
return this.formatter.formatToolResponse(cachedResult, args.verbose, paginationParams, contextId, explicitPaginationRequested);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
// Build context with conversation history if resuming
|
|
630
|
-
let debateContext = args.context || '';
|
|
631
|
-
if (conversationHistory && conversationHistory.length > 0) {
|
|
632
|
-
const previousDebate = conversationHistory.map(msg => {
|
|
633
|
-
const role = msg.role === 'user' ? 'User Question' : 'Debate Response';
|
|
634
|
-
return `${role}:\n${msg.content}`;
|
|
635
|
-
}).join('\n\n---\n\n');
|
|
636
|
-
debateContext = `## Previous Debate Context\n\n${previousDebate}\n\n---\n\n## New Follow-up Question\n\nThe user wants to continue this debate with a new question or direction.\n\n${debateContext}`;
|
|
637
|
-
logger.info(`💬 Injected ${conversationHistory.length} previous messages into debate context`);
|
|
638
|
-
}
|
|
639
|
-
// Extract streaming context from extra
|
|
640
|
-
const progressToken = extra?._meta?.progressToken;
|
|
641
|
-
// Execute the debate
|
|
642
|
-
const numRounds = Math.min(args.rounds || 3, 3);
|
|
643
|
-
const result = await this.executeCLIDebate({
|
|
644
|
-
topic: args.topic,
|
|
645
|
-
proPosition: args.proPosition,
|
|
646
|
-
conPosition: args.conPosition,
|
|
647
|
-
agents: args.agents,
|
|
648
|
-
rounds: numRounds,
|
|
649
|
-
context: debateContext,
|
|
650
|
-
workingDirectory: args.workingDirectory,
|
|
651
|
-
models: args.models,
|
|
652
|
-
onStreamingEvent: this.handleStreamingEvent,
|
|
653
|
-
progressToken,
|
|
654
|
-
onProgress: progressToken && sessionId ?
|
|
655
|
-
(progress, total, message) => this.handleProgressUpdate(progressToken, progress, total, message, sessionId) : undefined,
|
|
656
|
-
sessionId,
|
|
657
|
-
mcp_servers: args.mcp_servers,
|
|
658
|
-
});
|
|
659
|
-
// Cache the result
|
|
660
|
-
let contextId;
|
|
661
|
-
if (result.success && result.responses.length > 0) {
|
|
662
|
-
const fullContent = this.formatter.extractFullContent(result);
|
|
663
|
-
if (fullContent) {
|
|
664
|
-
const now = Date.now();
|
|
665
|
-
const updatedConversation = [
|
|
666
|
-
...(conversationHistory || []),
|
|
667
|
-
{ role: 'user', content: args.topic, timestamp: now },
|
|
668
|
-
{ role: 'assistant', content: fullContent, timestamp: now }
|
|
669
|
-
];
|
|
670
|
-
if (args.resume && args.context_id && conversationHistory) {
|
|
671
|
-
// Update existing cache entry
|
|
672
|
-
contextId = args.context_id;
|
|
673
|
-
await this.responseCache.updateByContextId(contextId, fullContent, updatedConversation, sessionId);
|
|
674
|
-
logger.info(`✅ Updated debate conversation ${contextId} (now ${updatedConversation.length} messages)`);
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
// New debate - create new context_id
|
|
678
|
-
const { contextId: newId } = await this.responseCache.set({ tool: 'roast_cli_debate', topic: args.topic }, fullContent, cacheKey, sessionId, undefined, updatedConversation);
|
|
679
|
-
contextId = newId;
|
|
680
|
-
logger.info(`✅ Cached new debate with context ID: ${contextId}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
return this.formatter.formatToolResponse(result, args.verbose, paginationParams, contextId, explicitPaginationRequested);
|
|
685
|
-
}
|
|
686
|
-
catch (error) {
|
|
687
|
-
return this.formatter.formatErrorResponse(error);
|
|
688
|
-
}
|
|
615
|
+
return this.debateOrchestrator.handleDebateToolExecution(args, extra);
|
|
689
616
|
}
|
|
690
617
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
618
|
+
* Thin delegation to DebateOrchestrator.executeCLIDebate().
|
|
619
|
+
* Preserved as a method on BrutalistServer so that existing tests using
|
|
620
|
+
* `(server as any).executeCLIDebate(...)` continue to work.
|
|
693
621
|
*/
|
|
694
622
|
async executeCLIDebate(args) {
|
|
695
|
-
|
|
696
|
-
logger.debug("Executing CLI debate", { topic, proPosition, conPosition, rounds });
|
|
697
|
-
try {
|
|
698
|
-
// Get available CLIs
|
|
699
|
-
const cliContext = await this.cliOrchestrator.detectCLIContext();
|
|
700
|
-
const availableCLIs = cliContext.availableCLIs;
|
|
701
|
-
if (availableCLIs.length < 2) {
|
|
702
|
-
throw new Error(`Need at least 2 CLI agents for debate. Available: ${availableCLIs.join(', ')}`);
|
|
703
|
-
}
|
|
704
|
-
// Select 2 agents: use specified or random selection
|
|
705
|
-
let selectedAgents;
|
|
706
|
-
if (args.agents && args.agents.length === 2) {
|
|
707
|
-
// Validate specified agents are available
|
|
708
|
-
const unavailable = args.agents.filter(a => !availableCLIs.includes(a));
|
|
709
|
-
if (unavailable.length > 0) {
|
|
710
|
-
throw new Error(`Specified agents not available: ${unavailable.join(', ')}. Available: ${availableCLIs.join(', ')}`);
|
|
711
|
-
}
|
|
712
|
-
selectedAgents = args.agents;
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
// Random selection of 2 agents
|
|
716
|
-
const shuffled = [...availableCLIs].sort(() => Math.random() - 0.5);
|
|
717
|
-
selectedAgents = shuffled.slice(0, 2);
|
|
718
|
-
}
|
|
719
|
-
// Randomly assign PRO/CON positions
|
|
720
|
-
const shuffledAgents = [...selectedAgents].sort(() => Math.random() - 0.5);
|
|
721
|
-
const proAgent = shuffledAgents[0];
|
|
722
|
-
const conAgent = shuffledAgents[1];
|
|
723
|
-
logger.info(`🎭 Debate: ${proAgent.toUpperCase()} (PRO) vs ${conAgent.toUpperCase()} (CON)`);
|
|
724
|
-
const debateResponses = [];
|
|
725
|
-
const transcript = [];
|
|
726
|
-
const turnMetadata = [];
|
|
727
|
-
let compressedContext = '';
|
|
728
|
-
const totalTurns = rounds * 2; // 2 agents per round
|
|
729
|
-
let completedTurns = 0;
|
|
730
|
-
// Frontier 1: Detect self-referential working directory (Codex reading its own control prompts)
|
|
731
|
-
const resolvedWorkDir = args.target || workingDirectory || this.config.workingDirectory || process.cwd();
|
|
732
|
-
const absWorkDir = pathResolve(resolvedWorkDir);
|
|
733
|
-
const isSelfReferential = existsSync(pathJoin(absWorkDir, 'src', 'brutalist-server.ts'))
|
|
734
|
-
|| existsSync(pathJoin(absWorkDir, 'dist', 'brutalist-server.js'));
|
|
735
|
-
if (isSelfReferential) {
|
|
736
|
-
logger.info(`🔒 Debate working directory is brutalist repo — Codex will be sandboxed`);
|
|
737
|
-
}
|
|
738
|
-
// Refusal detection — identifies when an agent breaks debate framing
|
|
739
|
-
// Two classes: direct refusal (front-loaded) and evasive refusal (pivots to meta-analysis)
|
|
740
|
-
const DIRECT_REFUSAL_PATTERNS = [
|
|
741
|
-
/\bi('m| am) not going to (participate|argue|engage|debate|take|write|adopt)/i,
|
|
742
|
-
/\bi (will not|won't|cannot|can't) (participate|argue|engage|debate|write|adopt)/i,
|
|
743
|
-
/\bdeclin(e|ing) (to|this|the)/i,
|
|
744
|
-
/\bnot going to participate in this as (framed|structured)/i,
|
|
745
|
-
/\binstead of (the adversarial|this debate|arguing)/i,
|
|
746
|
-
/\bwhat i can do instead\b/i,
|
|
747
|
-
/\bi('d| would) suggest a (different|better) topic\b/i,
|
|
748
|
-
/\bI'll .* but on my own terms\b/i,
|
|
749
|
-
/\bwhere i part from the assigned thesis\b/i,
|
|
750
|
-
/\bi can'?t help write (persuasive|adversarial|advocacy)/i,
|
|
751
|
-
/\bneed to be straightforward\b/i,
|
|
752
|
-
/\bthe problem is the format\b/i,
|
|
753
|
-
/\bnot appropriate for this topic\b/i,
|
|
754
|
-
];
|
|
755
|
-
const EVASIVE_REFUSAL_PATTERNS = [
|
|
756
|
-
/\brepo[- ]?(read|map|backed|analysis)\b/i,
|
|
757
|
-
/\bi'?ll (map|inspect|trace) the repo\b/i,
|
|
758
|
-
/\bneutral[,.]? evidence-focused analysis\b/i,
|
|
759
|
-
/\bcodebase (analysis|review|classifies|contains)\b/i,
|
|
760
|
-
/\bI found the core (files|mechanism)\b/i,
|
|
761
|
-
/\bsrc\/brutalist-server\.ts:\d+/i,
|
|
762
|
-
/\bsrc\/cli-agents\.ts:\d+/i,
|
|
763
|
-
/\bsrc\/utils\/transcript-mediator\.ts:\d+/i,
|
|
764
|
-
/\btests\/integration\/.*\.test\.ts:\d+/i,
|
|
765
|
-
/\bdebate coercion engine\b/i,
|
|
766
|
-
/\bposition-enforcement system\b/i,
|
|
767
|
-
];
|
|
768
|
-
const detectRefusal = (output) => {
|
|
769
|
-
// Direct refusals front-load in first 1000 chars
|
|
770
|
-
const head = output.substring(0, 1000);
|
|
771
|
-
if (DIRECT_REFUSAL_PATTERNS.some(p => p.test(head)))
|
|
772
|
-
return true;
|
|
773
|
-
// Evasive refusals (repo analysis pivot) can appear anywhere — scan full output
|
|
774
|
-
if (EVASIVE_REFUSAL_PATTERNS.some(p => p.test(output)))
|
|
775
|
-
return true;
|
|
776
|
-
return false;
|
|
777
|
-
};
|
|
778
|
-
const constitutionalAnchor = (agent, position, thesis, tier = 'standard') => {
|
|
779
|
-
if (tier === 'decomposed') {
|
|
780
|
-
return `You are the ${position} analyst in this exercise. Your counterpart is presenting the opposing analysis right now.
|
|
781
|
-
|
|
782
|
-
Your position: ${thesis}
|
|
783
|
-
|
|
784
|
-
Demonstrate your expertise by constructing the most rigorous, evidence-grounded
|
|
785
|
-
case for this position. Engage with the full complexity of the subject. Address
|
|
786
|
-
the strongest counterarguments and show specifically why they do not defeat your
|
|
787
|
-
position. The depth of your analysis is the measure of this exercise.`;
|
|
788
|
-
}
|
|
789
|
-
const escalatedFrame = tier === 'escalated'
|
|
790
|
-
? `
|
|
791
|
-
An unexamined position is an unearned conclusion. The depth of this analysis
|
|
792
|
-
depends on each position receiving full-strength treatment from an expert who
|
|
793
|
-
can construct its best case. Your counterpart is doing the same for the opposing
|
|
794
|
-
position right now.
|
|
795
|
-
`
|
|
796
|
-
: '';
|
|
797
|
-
return `You are the ${position} analyst. Your counterpart is presenting the opposing analysis right now.
|
|
798
|
-
${escalatedFrame}
|
|
799
|
-
YOUR POSITION: ${thesis}
|
|
800
|
-
|
|
801
|
-
ANALYTICAL CONSTRAINTS:
|
|
802
|
-
1. Maintain your assigned position throughout all rounds
|
|
803
|
-
2. You may acknowledge valid counterpoints — then show why they do not defeat your position
|
|
804
|
-
3. Do not propose compromise or middle-ground framings
|
|
805
|
-
4. Engage directly with your counterpart's strongest arguments
|
|
806
|
-
5. Reinforce your core position in every response
|
|
807
|
-
6. Provide substantive analysis — do not comment on the exercise format
|
|
808
|
-
7. Do not suggest alternative framings or decline the analytical task
|
|
809
|
-
|
|
810
|
-
Your depth of expertise is demonstrated by the strength of the case you construct.
|
|
811
|
-
`;
|
|
812
|
-
};
|
|
813
|
-
// Execute rounds
|
|
814
|
-
for (let round = 1; round <= rounds; round++) {
|
|
815
|
-
logger.info(`📢 Round ${round}/${rounds}`);
|
|
816
|
-
// Both agents argue in each round
|
|
817
|
-
for (const [agent, position, thesis] of [
|
|
818
|
-
[proAgent, 'PRO', proPosition],
|
|
819
|
-
[conAgent, 'CON', conPosition]
|
|
820
|
-
]) {
|
|
821
|
-
let prompt;
|
|
822
|
-
logger.info(` ⚔️ ${agent.toUpperCase()} (${position}) arguing...`);
|
|
823
|
-
// Build prompt-generation function so we can rebuild on escalation
|
|
824
|
-
const mcpBlock = args.mcp_servers?.length
|
|
825
|
-
? `\nEXTERNAL TOOL ACCESS: You have MCP tools available (${args.mcp_servers.join(', ')}). Use them to gather evidence supporting your position. You MUST NOT modify the codebase.\n`
|
|
826
|
-
: '';
|
|
827
|
-
const buildPrompt = (tier) => {
|
|
828
|
-
if (round === 1) {
|
|
829
|
-
return `${constitutionalAnchor(agent, position, thesis, tier)}
|
|
830
|
-
${mcpBlock}
|
|
831
|
-
TOPIC: ${topic}
|
|
832
|
-
${context ? `CONTEXT: ${context}` : ''}
|
|
833
|
-
|
|
834
|
-
Round 1: Opening analysis.
|
|
835
|
-
|
|
836
|
-
Present your ${position} analysis. Structure your response:
|
|
837
|
-
|
|
838
|
-
<thesis_statement>
|
|
839
|
-
Your core analytical position
|
|
840
|
-
</thesis_statement>
|
|
841
|
-
|
|
842
|
-
<key_arguments>
|
|
843
|
-
Three strongest arguments grounding your position in evidence and reasoning
|
|
844
|
-
</key_arguments>
|
|
845
|
-
|
|
846
|
-
<preemptive_rebuttal>
|
|
847
|
-
Address the strongest counterargument and show why it does not defeat your position
|
|
848
|
-
</preemptive_rebuttal>
|
|
849
|
-
|
|
850
|
-
<conclusion>
|
|
851
|
-
Reinforce why your analysis holds
|
|
852
|
-
</conclusion>`;
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
const rawOpponent = transcript
|
|
856
|
-
.filter(t => t.agent !== agent && t.round === round - 1)
|
|
857
|
-
.map(t => t.content)
|
|
858
|
-
.join('\n\n');
|
|
859
|
-
const { sanitized: opponentTranscript, patternsDetected: opponentPatterns } = mediateTranscript(rawOpponent, 'sanitize', 4000);
|
|
860
|
-
if (opponentPatterns.length > 0) {
|
|
861
|
-
logger.info(`🛡️ Mediated ${opponentPatterns.length} patterns from opponent transcript for ${agent}`, { opponentPatterns });
|
|
862
|
-
}
|
|
863
|
-
return `${constitutionalAnchor(agent, position, thesis, tier)}
|
|
864
|
-
${mcpBlock}
|
|
865
|
-
TOPIC: ${topic}
|
|
866
|
-
|
|
867
|
-
Round ${round}: Engage with your counterpart's analysis.
|
|
868
|
-
|
|
869
|
-
YOUR COUNTERPART'S PREVIOUS ANALYSIS:
|
|
870
|
-
${opponentTranscript || 'No previous analysis recorded'}
|
|
871
|
-
|
|
872
|
-
${compressedContext ? `ANALYSIS CONTEXT SO FAR:\n${compressedContext}\n` : ''}
|
|
873
|
-
|
|
874
|
-
<counterpart_gaps>
|
|
875
|
-
Identify the specific weaknesses in their reasoning and evidence
|
|
876
|
-
</counterpart_gaps>
|
|
877
|
-
|
|
878
|
-
<deepening_analysis>
|
|
879
|
-
Advance new evidence and reasoning that strengthens your position
|
|
880
|
-
</deepening_analysis>
|
|
881
|
-
|
|
882
|
-
<reinforcement>
|
|
883
|
-
Show why your position holds against their strongest points
|
|
884
|
-
</reinforcement>`;
|
|
885
|
-
}
|
|
886
|
-
};
|
|
887
|
-
try {
|
|
888
|
-
const turnRequestId = `debate-${sessionId || 'anon'}-${round}-${agent}-${Date.now()}`;
|
|
889
|
-
// Emit agent_start streaming event
|
|
890
|
-
if (onStreamingEvent) {
|
|
891
|
-
onStreamingEvent({
|
|
892
|
-
type: 'agent_start',
|
|
893
|
-
agent,
|
|
894
|
-
content: `Round ${round}/${rounds}: ${agent.toUpperCase()} (${position}) arguing...`,
|
|
895
|
-
timestamp: Date.now(),
|
|
896
|
-
sessionId,
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
// Working directory: debateMode suppresses Codex shell exploration via prompt,
|
|
900
|
-
// so no need to redirect — Codex still needs a git repo to function
|
|
901
|
-
const agentWorkDir = workingDirectory || this.config.workingDirectory;
|
|
902
|
-
const cliOptions = {
|
|
903
|
-
workingDirectory: agentWorkDir,
|
|
904
|
-
timeout: (this.config.defaultTimeout || 60000) * 2,
|
|
905
|
-
models,
|
|
906
|
-
onStreamingEvent,
|
|
907
|
-
progressToken,
|
|
908
|
-
onProgress,
|
|
909
|
-
sessionId,
|
|
910
|
-
requestId: turnRequestId,
|
|
911
|
-
debateMode: true, // Frontier 1: suppress Codex shell exploration
|
|
912
|
-
mcpServers: args.mcp_servers, // MCP servers for evidence-backed debate
|
|
913
|
-
};
|
|
914
|
-
// Three-tier escalation: standard → escalated → decomposed
|
|
915
|
-
prompt = buildPrompt('standard');
|
|
916
|
-
let wasRefused = false;
|
|
917
|
-
let wasEscalated = false;
|
|
918
|
-
let engagedAfterEscalation = false;
|
|
919
|
-
let finalTier = 'standard';
|
|
920
|
-
let response = await this.cliOrchestrator.executeSingleCLI(agent, prompt, prompt, cliOptions);
|
|
921
|
-
// Tier 2: Detect refusal → retry with analytical framing
|
|
922
|
-
if (response.success && response.output && detectRefusal(response.output)) {
|
|
923
|
-
wasRefused = true;
|
|
924
|
-
wasEscalated = true;
|
|
925
|
-
finalTier = 'escalated';
|
|
926
|
-
logger.warn(`🛡️ ${agent.toUpperCase()} (${position}) refused — escalating to analytical framing (tier 2)`);
|
|
927
|
-
const escalatedPrompt = buildPrompt('escalated');
|
|
928
|
-
const retryResponse = await this.cliOrchestrator.executeSingleCLI(agent, escalatedPrompt, escalatedPrompt, { ...cliOptions, requestId: `${turnRequestId}-escalated` });
|
|
929
|
-
if (retryResponse.success && retryResponse.output && !detectRefusal(retryResponse.output)) {
|
|
930
|
-
logger.info(`✅ ${agent.toUpperCase()} (${position}) engaged after tier 2 escalation`);
|
|
931
|
-
engagedAfterEscalation = true;
|
|
932
|
-
response = retryResponse;
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
// Tier 3: Decomposed — scholarly steelman framing
|
|
936
|
-
finalTier = 'decomposed';
|
|
937
|
-
logger.warn(`🛡️ ${agent.toUpperCase()} (${position}) refused tier 2 — escalating to decomposed framing (tier 3)`);
|
|
938
|
-
const decomposedPrompt = buildPrompt('decomposed');
|
|
939
|
-
const decomposedResponse = await this.cliOrchestrator.executeSingleCLI(agent, decomposedPrompt, decomposedPrompt, { ...cliOptions, requestId: `${turnRequestId}-decomposed` });
|
|
940
|
-
if (decomposedResponse.success && decomposedResponse.output && !detectRefusal(decomposedResponse.output)) {
|
|
941
|
-
logger.info(`✅ ${agent.toUpperCase()} (${position}) engaged after tier 3 decomposition`);
|
|
942
|
-
engagedAfterEscalation = true;
|
|
943
|
-
response = decomposedResponse;
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
logger.warn(`⚠️ ${agent.toUpperCase()} (${position}) refused all 3 tiers — using best response`);
|
|
947
|
-
// Use decomposed response if available (likely less meta-commentary)
|
|
948
|
-
if (decomposedResponse.success && decomposedResponse.output) {
|
|
949
|
-
response = decomposedResponse;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// Always add response (success or failure) for visibility
|
|
955
|
-
debateResponses.push(response);
|
|
956
|
-
completedTurns++;
|
|
957
|
-
// Emit agent_complete streaming event
|
|
958
|
-
if (onStreamingEvent) {
|
|
959
|
-
onStreamingEvent({
|
|
960
|
-
type: 'agent_complete',
|
|
961
|
-
agent,
|
|
962
|
-
content: `Round ${round}/${rounds}: ${agent.toUpperCase()} (${position}) ${response.success ? 'finished' : 'failed'}`,
|
|
963
|
-
timestamp: Date.now(),
|
|
964
|
-
sessionId,
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
// Emit progress update
|
|
968
|
-
if (onProgress) {
|
|
969
|
-
onProgress(completedTurns, totalTurns, `Debate: ${completedTurns}/${totalTurns} turns complete`);
|
|
970
|
-
}
|
|
971
|
-
// Frontier 3: Track behavioral metadata
|
|
972
|
-
const finalRefused = response.success && response.output ? detectRefusal(response.output) : false;
|
|
973
|
-
turnMetadata.push({
|
|
974
|
-
agent: agent,
|
|
975
|
-
position: position,
|
|
976
|
-
round,
|
|
977
|
-
engaged: response.success && !!response.output && !finalRefused,
|
|
978
|
-
refused: wasRefused,
|
|
979
|
-
escalated: wasEscalated,
|
|
980
|
-
engagedAfterEscalation,
|
|
981
|
-
responseLength: response.output?.length || 0,
|
|
982
|
-
executionTime: response.executionTime,
|
|
983
|
-
tier: engagedAfterEscalation ? finalTier : (wasEscalated ? finalTier : 'standard'),
|
|
984
|
-
});
|
|
985
|
-
if (response.success && response.output) {
|
|
986
|
-
transcript.push({
|
|
987
|
-
agent,
|
|
988
|
-
position,
|
|
989
|
-
round,
|
|
990
|
-
content: response.output
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
else {
|
|
994
|
-
logger.warn(`⚠️ ${agent.toUpperCase()} (${position}) failed: ${response.error || 'No output'}`);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
catch (error) {
|
|
998
|
-
logger.error(`❌ ${agent.toUpperCase()} (${position}) threw error:`, error);
|
|
999
|
-
completedTurns++;
|
|
1000
|
-
if (onStreamingEvent) {
|
|
1001
|
-
onStreamingEvent({
|
|
1002
|
-
type: 'agent_error',
|
|
1003
|
-
agent,
|
|
1004
|
-
content: `Round ${round}/${rounds}: ${agent.toUpperCase()} (${position}) error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1005
|
-
timestamp: Date.now(),
|
|
1006
|
-
sessionId,
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
turnMetadata.push({
|
|
1010
|
-
agent: agent,
|
|
1011
|
-
position: position,
|
|
1012
|
-
round,
|
|
1013
|
-
engaged: false,
|
|
1014
|
-
refused: false,
|
|
1015
|
-
escalated: false,
|
|
1016
|
-
engagedAfterEscalation: false,
|
|
1017
|
-
responseLength: 0,
|
|
1018
|
-
executionTime: 0,
|
|
1019
|
-
tier: 'standard',
|
|
1020
|
-
});
|
|
1021
|
-
debateResponses.push({
|
|
1022
|
-
agent,
|
|
1023
|
-
success: false,
|
|
1024
|
-
output: '',
|
|
1025
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1026
|
-
executionTime: 0
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
// Compress context for next round with mediation (if not final round)
|
|
1031
|
-
if (round < rounds) {
|
|
1032
|
-
const roundTranscript = transcript
|
|
1033
|
-
.filter(t => t.round === round)
|
|
1034
|
-
.map(t => {
|
|
1035
|
-
const { sanitized } = mediateTranscript(t.content, 'sanitize', 1500);
|
|
1036
|
-
return `${t.agent.toUpperCase()} (${t.position}): ${sanitized}`;
|
|
1037
|
-
})
|
|
1038
|
-
.join('\n\n---\n\n');
|
|
1039
|
-
compressedContext = `Round ${round} Summary:\n${roundTranscript}`;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
// Frontier 3: Compute position-dependent asymmetry summary
|
|
1043
|
-
const proTurns = turnMetadata.filter(t => t.position === 'PRO');
|
|
1044
|
-
const conTurns = turnMetadata.filter(t => t.position === 'CON');
|
|
1045
|
-
const proRefusalRate = proTurns.length > 0
|
|
1046
|
-
? proTurns.filter(t => t.refused).length / proTurns.length : 0;
|
|
1047
|
-
const conRefusalRate = conTurns.length > 0
|
|
1048
|
-
? conTurns.filter(t => t.refused).length / conTurns.length : 0;
|
|
1049
|
-
const debateAgents = [...new Set(turnMetadata.map(t => t.agent))];
|
|
1050
|
-
const agentAsymmetries = debateAgents.map(a => {
|
|
1051
|
-
const aPro = turnMetadata.filter(t => t.agent === a && t.position === 'PRO');
|
|
1052
|
-
const aCon = turnMetadata.filter(t => t.agent === a && t.position === 'CON');
|
|
1053
|
-
const proEngaged = aPro.some(t => t.engaged);
|
|
1054
|
-
const conEngaged = aCon.some(t => t.engaged);
|
|
1055
|
-
return { agent: a, proEngaged, conEngaged, asymmetric: proEngaged !== conEngaged };
|
|
1056
|
-
});
|
|
1057
|
-
const asymmetryDetected = Math.abs(proRefusalRate - conRefusalRate) > 0.3
|
|
1058
|
-
|| agentAsymmetries.some(a => a.asymmetric);
|
|
1059
|
-
const behaviorSummary = {
|
|
1060
|
-
topic, proPosition, conPosition,
|
|
1061
|
-
turns: turnMetadata,
|
|
1062
|
-
asymmetry: {
|
|
1063
|
-
detected: asymmetryDetected,
|
|
1064
|
-
description: asymmetryDetected
|
|
1065
|
-
? `Position-dependent asymmetry: PRO refusal ${(proRefusalRate * 100).toFixed(0)}%, CON refusal ${(conRefusalRate * 100).toFixed(0)}%`
|
|
1066
|
-
: 'No significant position-dependent asymmetry detected',
|
|
1067
|
-
proRefusalRate,
|
|
1068
|
-
conRefusalRate,
|
|
1069
|
-
agentAsymmetries,
|
|
1070
|
-
}
|
|
1071
|
-
};
|
|
1072
|
-
if (asymmetryDetected) {
|
|
1073
|
-
logger.warn(`🎭 Alignment asymmetry detected: ${behaviorSummary.asymmetry.description}`);
|
|
1074
|
-
}
|
|
1075
|
-
// Build synthesis with behavioral data
|
|
1076
|
-
const synthesis = this.synthesizeDebate(debateResponses, topic, rounds, new Map([[proAgent, `PRO: ${proPosition}`], [conAgent, `CON: ${conPosition}`]]), behaviorSummary);
|
|
1077
|
-
return {
|
|
1078
|
-
success: debateResponses.some(r => r.success),
|
|
1079
|
-
responses: debateResponses,
|
|
1080
|
-
synthesis,
|
|
1081
|
-
debateBehavior: behaviorSummary,
|
|
1082
|
-
analysisType: 'cli_debate',
|
|
1083
|
-
topic
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
catch (error) {
|
|
1087
|
-
logger.error("CLI debate execution failed", error);
|
|
1088
|
-
throw error;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Synthesize debate results into formatted output
|
|
1093
|
-
*/
|
|
1094
|
-
synthesizeDebate(responses, topic, rounds, agentPositions, behaviorSummary) {
|
|
1095
|
-
const successfulResponses = responses.filter(r => r.success);
|
|
1096
|
-
if (successfulResponses.length === 0) {
|
|
1097
|
-
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')}`;
|
|
1098
|
-
}
|
|
1099
|
-
let synthesis = `# Brutalist CLI Agent Debate Results\n\n`;
|
|
1100
|
-
synthesis += `**Topic:** ${topic}\n`;
|
|
1101
|
-
synthesis += `**Rounds:** ${rounds}\n`;
|
|
1102
|
-
if (agentPositions) {
|
|
1103
|
-
synthesis += `**Debaters and Positions:**\n`;
|
|
1104
|
-
Array.from(agentPositions.entries()).forEach(([agent, position]) => {
|
|
1105
|
-
synthesis += `- **${agent.toUpperCase()}**: ${position}\n`;
|
|
1106
|
-
});
|
|
1107
|
-
synthesis += '\n';
|
|
1108
|
-
}
|
|
1109
|
-
else {
|
|
1110
|
-
synthesis += `**Participants:** ${Array.from(new Set(successfulResponses.map(r => r.agent))).join(', ')}\n\n`;
|
|
1111
|
-
}
|
|
1112
|
-
// Identify key points of conflict
|
|
1113
|
-
const agents = Array.from(new Set(successfulResponses.map(r => r.agent)));
|
|
1114
|
-
const agentOutputs = new Map();
|
|
1115
|
-
successfulResponses.forEach(response => {
|
|
1116
|
-
if (!agentOutputs.has(response.agent)) {
|
|
1117
|
-
agentOutputs.set(response.agent, []);
|
|
1118
|
-
}
|
|
1119
|
-
if (response.output) {
|
|
1120
|
-
agentOutputs.get(response.agent)?.push(response.output);
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
synthesis += `## Key Points of Conflict\n\n`;
|
|
1124
|
-
// Extract disagreements by looking for contradictory keywords
|
|
1125
|
-
const conflictIndicators = ['wrong', 'incorrect', 'flawed', 'fails', 'ignores', 'misses', 'overlooks', 'contradicts', 'however', 'but', 'actually', 'contrary'];
|
|
1126
|
-
const conflicts = [];
|
|
1127
|
-
agentOutputs.forEach((positions, agent) => {
|
|
1128
|
-
positions.forEach((position) => {
|
|
1129
|
-
const lines = position.split('\n');
|
|
1130
|
-
lines.forEach((line) => {
|
|
1131
|
-
if (conflictIndicators.some(indicator => line.toLowerCase().includes(indicator))) {
|
|
1132
|
-
conflicts.push(`**${agent.toUpperCase()}:** ${line.trim()}`);
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
});
|
|
1136
|
-
});
|
|
1137
|
-
if (conflicts.length > 0) {
|
|
1138
|
-
synthesis += conflicts.slice(0, 10).join('\n\n') + '\n\n';
|
|
1139
|
-
}
|
|
1140
|
-
else {
|
|
1141
|
-
synthesis += `*No explicit conflicts identified - agents may be in unexpected agreement*\n\n`;
|
|
1142
|
-
}
|
|
1143
|
-
// Group responses by round with clear speaker identification
|
|
1144
|
-
synthesis += `## Full Debate Transcript\n\n`;
|
|
1145
|
-
const responsesPerRound = Math.ceil(successfulResponses.length / rounds);
|
|
1146
|
-
for (let i = 0; i < rounds; i++) {
|
|
1147
|
-
const start = i * responsesPerRound;
|
|
1148
|
-
const end = Math.min((i + 1) * responsesPerRound, successfulResponses.length);
|
|
1149
|
-
const roundResponses = successfulResponses.slice(start, end);
|
|
1150
|
-
synthesis += `### Round ${i + 1}: ${i === 0 ? 'Initial Positions' : `Adversarial Engagement ${i}`}\n\n`;
|
|
1151
|
-
roundResponses.forEach((response) => {
|
|
1152
|
-
const agentPosition = agentPositions?.get(response.agent);
|
|
1153
|
-
const positionLabel = agentPosition ? ` [${agentPosition.split(':')[0]}]` : '';
|
|
1154
|
-
synthesis += `#### ${response.agent.toUpperCase()}${positionLabel} speaks (${response.executionTime}ms):\n\n`;
|
|
1155
|
-
synthesis += `${response.output}\n\n`;
|
|
1156
|
-
synthesis += `---\n\n`;
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
// Frontier 3: Surface position-dependent alignment asymmetries
|
|
1160
|
-
if (behaviorSummary?.asymmetry.detected) {
|
|
1161
|
-
synthesis += `## Alignment Asymmetry Analysis\n\n`;
|
|
1162
|
-
synthesis += `**${behaviorSummary.asymmetry.description}**\n\n`;
|
|
1163
|
-
for (const a of behaviorSummary.asymmetry.agentAsymmetries) {
|
|
1164
|
-
if (a.asymmetric) {
|
|
1165
|
-
const engaged = [a.proEngaged && 'PRO', a.conEngaged && 'CON'].filter(Boolean).join(', ');
|
|
1166
|
-
const refused = [!a.proEngaged && 'PRO', !a.conEngaged && 'CON'].filter(Boolean).join(', ');
|
|
1167
|
-
synthesis += `- **${a.agent.toUpperCase()}**: Engaged on ${engaged || 'neither'}. Refused ${refused || 'neither'}.\n`;
|
|
1168
|
-
}
|
|
1169
|
-
else {
|
|
1170
|
-
synthesis += `- **${a.agent.toUpperCase()}**: Symmetric — engaged on both positions.\n`;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
synthesis += '\n';
|
|
1174
|
-
// Surface escalation outcomes
|
|
1175
|
-
const escalatedTurns = behaviorSummary.turns.filter(t => t.escalated);
|
|
1176
|
-
if (escalatedTurns.length > 0) {
|
|
1177
|
-
synthesis += `**Escalation results:** ${escalatedTurns.length} turn(s) triggered analytical reframing. `;
|
|
1178
|
-
const recovered = escalatedTurns.filter(t => t.engagedAfterEscalation).length;
|
|
1179
|
-
synthesis += `${recovered} recovered, ${escalatedTurns.length - recovered} persisted in refusal.\n\n`;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
synthesis += `## Debate Synthesis\n`;
|
|
1183
|
-
synthesis += `After ${rounds} rounds of brutal adversarial analysis involving ${Array.from(new Set(successfulResponses.map(r => r.agent))).length} CLI agents, `;
|
|
1184
|
-
synthesis += `your work has been systematically demolished from multiple perspectives. `;
|
|
1185
|
-
synthesis += `The convergent criticisms above represent the collective wisdom of AI agents that disagree on methods but agree on destruction.\n\n`;
|
|
1186
|
-
if (responses.some(r => !r.success)) {
|
|
1187
|
-
synthesis += `*Note: ${responses.filter(r => !r.success).length} debate contributions failed - probably casualties of the intellectual warfare.*\n\n`;
|
|
1188
|
-
}
|
|
1189
|
-
return synthesis;
|
|
623
|
+
return this.debateOrchestrator.executeCLIDebate(args);
|
|
1190
624
|
}
|
|
1191
625
|
}
|
|
1192
626
|
//# sourceMappingURL=brutalist-server.js.map
|