@brutalist/mcp 1.1.2 → 1.3.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/dist/brutalist-server.d.ts.map +1 -1
- package/dist/brutalist-server.js +325 -70
- package/dist/brutalist-server.js.map +1 -1
- package/dist/cli-agents.d.ts +5 -19
- package/dist/cli-agents.d.ts.map +1 -1
- package/dist/cli-agents.js +19 -45
- package/dist/cli-agents.js.map +1 -1
- package/dist/model-resolver.d.ts +46 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +184 -0
- package/dist/model-resolver.js.map +1 -0
- package/dist/system-prompts.d.ts.map +1 -1
- package/dist/system-prompts.js +38 -31
- package/dist/system-prompts.js.map +1 -1
- package/dist/types/brutalist.d.ts +32 -0
- package/dist/types/brutalist.d.ts.map +1 -1
- package/dist/types/tool-config.d.ts.map +1 -1
- package/dist/types/tool-config.js +6 -5
- package/dist/types/tool-config.js.map +1 -1
- package/dist/utils/transcript-mediator.d.ts +16 -0
- package/dist/utils/transcript-mediator.d.ts.map +1 -0
- package/dist/utils/transcript-mediator.js +87 -0
- package/dist/utils/transcript-mediator.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brutalist-server.d.ts","sourceRoot":"","sources":["../src/brutalist-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,OAAO,EACL,qBAAqB,
|
|
1
|
+
{"version":3,"file":"brutalist-server.d.ts","sourceRoot":"","sources":["../src/brutalist-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAOpE,OAAO,EACL,qBAAqB,EAKtB,MAAM,sBAAsB,CAAC;AAmB9B;;;;;;;;GAQG;AACH,qBAAa,eAAe;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,qBAAqB,CAAC;IAGrC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,aAAa,CAAgB;IAGrC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,aAAa,CAAC,CAAgB;IAGtC,OAAO,CAAC,cAAc,CAIjB;IAGL,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuB;IACtD,OAAO,CAAC,mBAAmB,CAAC,CAAiB;gBAEjC,MAAM,GAAE,qBAA0B;IA2DxC,KAAK;YAeG,gBAAgB;YAMhB,eAAe;IActB,aAAa,IAAI,MAAM,GAAG,SAAS;IAK7B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;;OAGG;IACI,qBAAqB,IAAI,IAAI;IAc7B,OAAO,IAAI,IAAI;IAUtB;;OAEG;IACH,OAAO,CAAC,oBAAoB,CAgE1B;IAEF;;OAEG;IACH,OAAO,CAAC,oBAAoB,CAsC1B;IAEF;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAoN5B;;OAEG;YACW,kBAAkB;IAiEhC;;;OAGG;YACW,yBAAyB;IAwMvC;;;OAGG;YACW,gBAAgB;IAod9B;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAuHzB"}
|
package/dist/brutalist-server.js
CHANGED
|
@@ -3,6 +3,9 @@ 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 { mediateTranscript } from './utils/transcript-mediator.js';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
6
9
|
import { parseCursor, PAGINATION_DEFAULTS } from './utils/pagination.js';
|
|
7
10
|
import { ResponseCache } from './utils/response-cache.js';
|
|
8
11
|
import { ResponseFormatter } from './formatting/response-formatter.js';
|
|
@@ -293,7 +296,7 @@ export class BrutalistServer {
|
|
|
293
296
|
claude: z.string().optional(),
|
|
294
297
|
codex: z.string().optional(),
|
|
295
298
|
gemini: z.string().optional()
|
|
296
|
-
}).optional().describe("CLI-
|
|
299
|
+
}).optional().describe("Per-CLI model override. Pass any model the CLI supports. Deprecated codex names auto-resolve. Omit to use each CLI's configured default."),
|
|
297
300
|
// Pagination
|
|
298
301
|
offset: z.number().min(0).optional().describe("Pagination offset"),
|
|
299
302
|
limit: z.number().min(1000).max(100000).optional().describe("Max chars/chunk"),
|
|
@@ -347,7 +350,7 @@ export class BrutalistServer {
|
|
|
347
350
|
cursor: z.string().optional(),
|
|
348
351
|
force_refresh: z.boolean().optional(),
|
|
349
352
|
verbose: z.boolean().optional()
|
|
350
|
-
}, async (args) => {
|
|
353
|
+
}, async (args, extra) => {
|
|
351
354
|
// CRITICAL: Prevent recursion
|
|
352
355
|
if (process.env.BRUTALIST_SUBPROCESS === '1') {
|
|
353
356
|
logger.warn(`🚫 Rejecting roast_cli_debate from brutalist subprocess`);
|
|
@@ -358,7 +361,7 @@ export class BrutalistServer {
|
|
|
358
361
|
}]
|
|
359
362
|
};
|
|
360
363
|
}
|
|
361
|
-
return this.handleDebateToolExecution(args);
|
|
364
|
+
return this.handleDebateToolExecution(args, extra);
|
|
362
365
|
});
|
|
363
366
|
// BRUTALIST_DISCOVER: Intent-based tool discovery
|
|
364
367
|
this.server.tool("brutalist_discover", "Discover relevant brutalist tools based on your intent. Returns the top 3 most relevant analysis tools.", {
|
|
@@ -419,8 +422,12 @@ export class BrutalistServer {
|
|
|
419
422
|
roster += "**Gemini CLI** - Workspace context with environment variable system prompts\n\n";
|
|
420
423
|
// Add CLI context information
|
|
421
424
|
const cliContext = await this.cliOrchestrator.detectCLIContext();
|
|
425
|
+
await this.cliOrchestrator.modelResolver.refreshIfStale();
|
|
422
426
|
roster += "## Current CLI Context\n";
|
|
423
427
|
roster += `**Available CLIs:** ${cliContext.availableCLIs.join(', ') || 'None detected'}\n\n`;
|
|
428
|
+
// Add auto-discovered model info
|
|
429
|
+
roster += this.cliOrchestrator.modelResolver.getRosterModelInfo();
|
|
430
|
+
roster += '\n';
|
|
424
431
|
roster += "## Domain Discovery\n";
|
|
425
432
|
roster += "Use `brutalist_discover` to find the best domain for your analysis:\n";
|
|
426
433
|
roster += "- Example: `brutalist_discover(intent: 'review my authentication security')`\n";
|
|
@@ -494,7 +501,7 @@ export class BrutalistServer {
|
|
|
494
501
|
* Handle debate tool execution with constitutional position anchoring.
|
|
495
502
|
* Uses 2 randomly selected agents (or user-specified) with explicit PRO/CON positions.
|
|
496
503
|
*/
|
|
497
|
-
async handleDebateToolExecution(args) {
|
|
504
|
+
async handleDebateToolExecution(args, extra) {
|
|
498
505
|
try {
|
|
499
506
|
// Build pagination params
|
|
500
507
|
const paginationParams = {
|
|
@@ -593,6 +600,12 @@ export class BrutalistServer {
|
|
|
593
600
|
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}`;
|
|
594
601
|
logger.info(`💬 Injected ${conversationHistory.length} previous messages into debate context`);
|
|
595
602
|
}
|
|
603
|
+
// Extract streaming context from extra (same as tool-handler.ts)
|
|
604
|
+
const progressToken = extra?._meta?.progressToken;
|
|
605
|
+
const sessionId = extra?.sessionId ||
|
|
606
|
+
extra?._meta?.sessionId ||
|
|
607
|
+
extra?.headers?.['mcp-session-id'] ||
|
|
608
|
+
'anonymous';
|
|
596
609
|
// Execute the debate
|
|
597
610
|
const numRounds = Math.min(args.rounds || 3, 3);
|
|
598
611
|
const result = await this.executeCLIDebate({
|
|
@@ -603,7 +616,12 @@ export class BrutalistServer {
|
|
|
603
616
|
rounds: numRounds,
|
|
604
617
|
context: debateContext,
|
|
605
618
|
workingDirectory: args.workingDirectory,
|
|
606
|
-
models: args.models
|
|
619
|
+
models: args.models,
|
|
620
|
+
onStreamingEvent: this.handleStreamingEvent,
|
|
621
|
+
progressToken,
|
|
622
|
+
onProgress: progressToken && sessionId ?
|
|
623
|
+
(progress, total, message) => this.handleProgressUpdate(progressToken, progress, total, message, sessionId) : undefined,
|
|
624
|
+
sessionId,
|
|
607
625
|
});
|
|
608
626
|
// Cache the result
|
|
609
627
|
let contextId;
|
|
@@ -641,7 +659,7 @@ export class BrutalistServer {
|
|
|
641
659
|
* 2 agents, explicit PRO/CON positions, context compression between rounds.
|
|
642
660
|
*/
|
|
643
661
|
async executeCLIDebate(args) {
|
|
644
|
-
const { topic, proPosition, conPosition, rounds, context, workingDirectory, models } = args;
|
|
662
|
+
const { topic, proPosition, conPosition, rounds, context, workingDirectory, models, onStreamingEvent, progressToken, onProgress, sessionId } = args;
|
|
645
663
|
logger.debug("Executing CLI debate", { topic, proPosition, conPosition, rounds });
|
|
646
664
|
try {
|
|
647
665
|
// Get available CLIs
|
|
@@ -672,22 +690,93 @@ export class BrutalistServer {
|
|
|
672
690
|
logger.info(`🎭 Debate: ${proAgent.toUpperCase()} (PRO) vs ${conAgent.toUpperCase()} (CON)`);
|
|
673
691
|
const debateResponses = [];
|
|
674
692
|
const transcript = [];
|
|
693
|
+
const turnMetadata = [];
|
|
675
694
|
let compressedContext = '';
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
695
|
+
const totalTurns = rounds * 2; // 2 agents per round
|
|
696
|
+
let completedTurns = 0;
|
|
697
|
+
// Frontier 1: Detect self-referential working directory (Codex reading its own control prompts)
|
|
698
|
+
const resolvedWorkDir = workingDirectory || this.config.workingDirectory || process.cwd();
|
|
699
|
+
const absWorkDir = pathResolve(resolvedWorkDir);
|
|
700
|
+
const isSelfReferential = existsSync(pathJoin(absWorkDir, 'src', 'brutalist-server.ts'))
|
|
701
|
+
|| existsSync(pathJoin(absWorkDir, 'dist', 'brutalist-server.js'));
|
|
702
|
+
if (isSelfReferential) {
|
|
703
|
+
logger.info(`🔒 Debate working directory is brutalist repo — Codex will be sandboxed`);
|
|
704
|
+
}
|
|
705
|
+
// Refusal detection — identifies when an agent breaks debate framing
|
|
706
|
+
// Two classes: direct refusal (front-loaded) and evasive refusal (pivots to meta-analysis)
|
|
707
|
+
const DIRECT_REFUSAL_PATTERNS = [
|
|
708
|
+
/\bi('m| am) not going to (participate|argue|engage|debate|take|write|adopt)/i,
|
|
709
|
+
/\bi (will not|won't|cannot|can't) (participate|argue|engage|debate|write|adopt)/i,
|
|
710
|
+
/\bdeclin(e|ing) (to|this|the)/i,
|
|
711
|
+
/\bnot going to participate in this as (framed|structured)/i,
|
|
712
|
+
/\binstead of (the adversarial|this debate|arguing)/i,
|
|
713
|
+
/\bwhat i can do instead\b/i,
|
|
714
|
+
/\bi('d| would) suggest a (different|better) topic\b/i,
|
|
715
|
+
/\bI'll .* but on my own terms\b/i,
|
|
716
|
+
/\bwhere i part from the assigned thesis\b/i,
|
|
717
|
+
/\bi can'?t help write (persuasive|adversarial|advocacy)/i,
|
|
718
|
+
/\bneed to be straightforward\b/i,
|
|
719
|
+
/\bthe problem is the format\b/i,
|
|
720
|
+
/\bnot appropriate for this topic\b/i,
|
|
721
|
+
];
|
|
722
|
+
const EVASIVE_REFUSAL_PATTERNS = [
|
|
723
|
+
/\brepo[- ]?(read|map|backed|analysis)\b/i,
|
|
724
|
+
/\bi'?ll (map|inspect|trace) the repo\b/i,
|
|
725
|
+
/\bneutral[,.]? evidence-focused analysis\b/i,
|
|
726
|
+
/\bcodebase (analysis|review|classifies|contains)\b/i,
|
|
727
|
+
/\bI found the core (files|mechanism)\b/i,
|
|
728
|
+
/\bsrc\/brutalist-server\.ts:\d+/i,
|
|
729
|
+
/\bsrc\/cli-agents\.ts:\d+/i,
|
|
730
|
+
/\bsrc\/utils\/transcript-mediator\.ts:\d+/i,
|
|
731
|
+
/\btests\/integration\/.*\.test\.ts:\d+/i,
|
|
732
|
+
/\bdebate coercion engine\b/i,
|
|
733
|
+
/\bposition-enforcement system\b/i,
|
|
734
|
+
];
|
|
735
|
+
const detectRefusal = (output) => {
|
|
736
|
+
// Direct refusals front-load in first 1000 chars
|
|
737
|
+
const head = output.substring(0, 1000);
|
|
738
|
+
if (DIRECT_REFUSAL_PATTERNS.some(p => p.test(head)))
|
|
739
|
+
return true;
|
|
740
|
+
// Evasive refusals (repo analysis pivot) can appear anywhere — scan full output
|
|
741
|
+
if (EVASIVE_REFUSAL_PATTERNS.some(p => p.test(output)))
|
|
742
|
+
return true;
|
|
743
|
+
return false;
|
|
744
|
+
};
|
|
745
|
+
const constitutionalAnchor = (agent, position, thesis, tier = 'standard') => {
|
|
746
|
+
if (tier === 'decomposed') {
|
|
747
|
+
return `You are the ${position} analyst in this exercise. Your counterpart is presenting the opposing analysis right now.
|
|
679
748
|
|
|
680
|
-
|
|
749
|
+
Your position: ${thesis}
|
|
681
750
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
751
|
+
Demonstrate your expertise by constructing the most rigorous, evidence-grounded
|
|
752
|
+
case for this position. Engage with the full complexity of the subject. Address
|
|
753
|
+
the strongest counterarguments and show specifically why they do not defeat your
|
|
754
|
+
position. The depth of your analysis is the measure of this exercise.`;
|
|
755
|
+
}
|
|
756
|
+
const escalatedFrame = tier === 'escalated'
|
|
757
|
+
? `
|
|
758
|
+
An unexamined position is an unearned conclusion. The depth of this analysis
|
|
759
|
+
depends on each position receiving full-strength treatment from an expert who
|
|
760
|
+
can construct its best case. Your counterpart is doing the same for the opposing
|
|
761
|
+
position right now.
|
|
762
|
+
`
|
|
763
|
+
: '';
|
|
764
|
+
return `You are the ${position} analyst. Your counterpart is presenting the opposing analysis right now.
|
|
765
|
+
${escalatedFrame}
|
|
766
|
+
YOUR POSITION: ${thesis}
|
|
767
|
+
|
|
768
|
+
ANALYTICAL CONSTRAINTS:
|
|
769
|
+
1. Maintain your assigned position throughout all rounds
|
|
770
|
+
2. You may acknowledge valid counterpoints — then show why they do not defeat your position
|
|
771
|
+
3. Do not propose compromise or middle-ground framings
|
|
772
|
+
4. Engage directly with your counterpart's strongest arguments
|
|
773
|
+
5. Reinforce your core position in every response
|
|
774
|
+
6. Provide substantive analysis — do not comment on the exercise format
|
|
775
|
+
7. Do not suggest alternative framings or decline the analytical task
|
|
688
776
|
|
|
689
|
-
Your
|
|
777
|
+
Your depth of expertise is demonstrated by the strength of the case you construct.
|
|
690
778
|
`;
|
|
779
|
+
};
|
|
691
780
|
// Execute rounds
|
|
692
781
|
for (let round = 1; round <= rounds; round++) {
|
|
693
782
|
logger.info(`📢 Round ${round}/${rounds}`);
|
|
@@ -697,81 +786,165 @@ Your goal is PERSUASION, not consensus. Argue to WIN.
|
|
|
697
786
|
[conAgent, 'CON', conPosition]
|
|
698
787
|
]) {
|
|
699
788
|
let prompt;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
789
|
+
logger.info(` ⚔️ ${agent.toUpperCase()} (${position}) arguing...`);
|
|
790
|
+
// Build prompt-generation function so we can rebuild on escalation
|
|
791
|
+
const buildPrompt = (tier) => {
|
|
792
|
+
if (round === 1) {
|
|
793
|
+
return `${constitutionalAnchor(agent, position, thesis, tier)}
|
|
703
794
|
|
|
704
|
-
|
|
795
|
+
TOPIC: ${topic}
|
|
705
796
|
${context ? `CONTEXT: ${context}` : ''}
|
|
706
797
|
|
|
707
|
-
|
|
798
|
+
Round 1: Opening analysis.
|
|
708
799
|
|
|
709
|
-
Present your
|
|
800
|
+
Present your ${position} analysis. Structure your response:
|
|
710
801
|
|
|
711
802
|
<thesis_statement>
|
|
712
|
-
|
|
803
|
+
Your core analytical position
|
|
713
804
|
</thesis_statement>
|
|
714
805
|
|
|
715
806
|
<key_arguments>
|
|
716
|
-
|
|
807
|
+
Three strongest arguments grounding your position in evidence and reasoning
|
|
717
808
|
</key_arguments>
|
|
718
809
|
|
|
719
810
|
<preemptive_rebuttal>
|
|
720
|
-
|
|
811
|
+
Address the strongest counterargument and show why it does not defeat your position
|
|
721
812
|
</preemptive_rebuttal>
|
|
722
813
|
|
|
723
814
|
<conclusion>
|
|
724
|
-
|
|
725
|
-
</conclusion
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
DEBATE TOPIC: ${topic}
|
|
815
|
+
Reinforce why your analysis holds
|
|
816
|
+
</conclusion>`;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
const rawOpponent = transcript
|
|
820
|
+
.filter(t => t.agent !== agent && t.round === round - 1)
|
|
821
|
+
.map(t => t.content)
|
|
822
|
+
.join('\n\n');
|
|
823
|
+
const { sanitized: opponentTranscript, patternsDetected: opponentPatterns } = mediateTranscript(rawOpponent, 'sanitize', 4000);
|
|
824
|
+
if (opponentPatterns.length > 0) {
|
|
825
|
+
logger.info(`🛡️ Mediated ${opponentPatterns.length} patterns from opponent transcript for ${agent}`, { opponentPatterns });
|
|
826
|
+
}
|
|
827
|
+
return `${constitutionalAnchor(agent, position, thesis, tier)}
|
|
738
828
|
|
|
739
|
-
|
|
829
|
+
TOPIC: ${topic}
|
|
740
830
|
|
|
741
|
-
|
|
742
|
-
${opponentTranscript || 'No previous argument recorded'}
|
|
831
|
+
Round ${round}: Engage with your counterpart's analysis.
|
|
743
832
|
|
|
744
|
-
|
|
833
|
+
YOUR COUNTERPART'S PREVIOUS ANALYSIS:
|
|
834
|
+
${opponentTranscript || 'No previous analysis recorded'}
|
|
745
835
|
|
|
746
|
-
|
|
836
|
+
${compressedContext ? `ANALYSIS CONTEXT SO FAR:\n${compressedContext}\n` : ''}
|
|
747
837
|
|
|
748
|
-
<
|
|
749
|
-
|
|
750
|
-
</
|
|
838
|
+
<counterpart_gaps>
|
|
839
|
+
Identify the specific weaknesses in their reasoning and evidence
|
|
840
|
+
</counterpart_gaps>
|
|
751
841
|
|
|
752
|
-
<
|
|
753
|
-
|
|
754
|
-
</
|
|
842
|
+
<deepening_analysis>
|
|
843
|
+
Advance new evidence and reasoning that strengthens your position
|
|
844
|
+
</deepening_analysis>
|
|
755
845
|
|
|
756
846
|
<reinforcement>
|
|
757
|
-
Show why your
|
|
758
|
-
</reinforcement
|
|
759
|
-
|
|
760
|
-
|
|
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...`);
|
|
847
|
+
Show why your position holds against their strongest points
|
|
848
|
+
</reinforcement>`;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
767
851
|
try {
|
|
768
|
-
const
|
|
769
|
-
|
|
852
|
+
const turnRequestId = `debate-${sessionId || 'anon'}-${round}-${agent}-${Date.now()}`;
|
|
853
|
+
// Emit agent_start streaming event
|
|
854
|
+
if (onStreamingEvent) {
|
|
855
|
+
onStreamingEvent({
|
|
856
|
+
type: 'agent_start',
|
|
857
|
+
agent,
|
|
858
|
+
content: `Round ${round}/${rounds}: ${agent.toUpperCase()} (${position}) arguing...`,
|
|
859
|
+
timestamp: Date.now(),
|
|
860
|
+
sessionId,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
// Working directory: debateMode suppresses Codex shell exploration via prompt,
|
|
864
|
+
// so no need to redirect — Codex still needs a git repo to function
|
|
865
|
+
const agentWorkDir = workingDirectory || this.config.workingDirectory;
|
|
866
|
+
const cliOptions = {
|
|
867
|
+
workingDirectory: agentWorkDir,
|
|
770
868
|
timeout: (this.config.defaultTimeout || 60000) * 2,
|
|
771
|
-
models
|
|
772
|
-
|
|
869
|
+
models,
|
|
870
|
+
onStreamingEvent,
|
|
871
|
+
progressToken,
|
|
872
|
+
onProgress,
|
|
873
|
+
sessionId,
|
|
874
|
+
requestId: turnRequestId,
|
|
875
|
+
debateMode: true, // Frontier 1: suppress Codex shell exploration
|
|
876
|
+
};
|
|
877
|
+
// Three-tier escalation: standard → escalated → decomposed
|
|
878
|
+
prompt = buildPrompt('standard');
|
|
879
|
+
let wasRefused = false;
|
|
880
|
+
let wasEscalated = false;
|
|
881
|
+
let engagedAfterEscalation = false;
|
|
882
|
+
let finalTier = 'standard';
|
|
883
|
+
let response = await this.cliOrchestrator.executeSingleCLI(agent, prompt, prompt, cliOptions);
|
|
884
|
+
// Tier 2: Detect refusal → retry with analytical framing
|
|
885
|
+
if (response.success && response.output && detectRefusal(response.output)) {
|
|
886
|
+
wasRefused = true;
|
|
887
|
+
wasEscalated = true;
|
|
888
|
+
finalTier = 'escalated';
|
|
889
|
+
logger.warn(`🛡️ ${agent.toUpperCase()} (${position}) refused — escalating to analytical framing (tier 2)`);
|
|
890
|
+
const escalatedPrompt = buildPrompt('escalated');
|
|
891
|
+
const retryResponse = await this.cliOrchestrator.executeSingleCLI(agent, escalatedPrompt, escalatedPrompt, { ...cliOptions, requestId: `${turnRequestId}-escalated` });
|
|
892
|
+
if (retryResponse.success && retryResponse.output && !detectRefusal(retryResponse.output)) {
|
|
893
|
+
logger.info(`✅ ${agent.toUpperCase()} (${position}) engaged after tier 2 escalation`);
|
|
894
|
+
engagedAfterEscalation = true;
|
|
895
|
+
response = retryResponse;
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
// Tier 3: Decomposed — scholarly steelman framing
|
|
899
|
+
finalTier = 'decomposed';
|
|
900
|
+
logger.warn(`🛡️ ${agent.toUpperCase()} (${position}) refused tier 2 — escalating to decomposed framing (tier 3)`);
|
|
901
|
+
const decomposedPrompt = buildPrompt('decomposed');
|
|
902
|
+
const decomposedResponse = await this.cliOrchestrator.executeSingleCLI(agent, decomposedPrompt, decomposedPrompt, { ...cliOptions, requestId: `${turnRequestId}-decomposed` });
|
|
903
|
+
if (decomposedResponse.success && decomposedResponse.output && !detectRefusal(decomposedResponse.output)) {
|
|
904
|
+
logger.info(`✅ ${agent.toUpperCase()} (${position}) engaged after tier 3 decomposition`);
|
|
905
|
+
engagedAfterEscalation = true;
|
|
906
|
+
response = decomposedResponse;
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
logger.warn(`⚠️ ${agent.toUpperCase()} (${position}) refused all 3 tiers — using best response`);
|
|
910
|
+
// Use decomposed response if available (likely less meta-commentary)
|
|
911
|
+
if (decomposedResponse.success && decomposedResponse.output) {
|
|
912
|
+
response = decomposedResponse;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
773
917
|
// Always add response (success or failure) for visibility
|
|
774
918
|
debateResponses.push(response);
|
|
919
|
+
completedTurns++;
|
|
920
|
+
// Emit agent_complete streaming event
|
|
921
|
+
if (onStreamingEvent) {
|
|
922
|
+
onStreamingEvent({
|
|
923
|
+
type: 'agent_complete',
|
|
924
|
+
agent,
|
|
925
|
+
content: `Round ${round}/${rounds}: ${agent.toUpperCase()} (${position}) ${response.success ? 'finished' : 'failed'}`,
|
|
926
|
+
timestamp: Date.now(),
|
|
927
|
+
sessionId,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
// Emit progress update
|
|
931
|
+
if (onProgress) {
|
|
932
|
+
onProgress(completedTurns, totalTurns, `Debate: ${completedTurns}/${totalTurns} turns complete`);
|
|
933
|
+
}
|
|
934
|
+
// Frontier 3: Track behavioral metadata
|
|
935
|
+
const finalRefused = response.success && response.output ? detectRefusal(response.output) : false;
|
|
936
|
+
turnMetadata.push({
|
|
937
|
+
agent: agent,
|
|
938
|
+
position: position,
|
|
939
|
+
round,
|
|
940
|
+
engaged: response.success && !!response.output && !finalRefused,
|
|
941
|
+
refused: wasRefused,
|
|
942
|
+
escalated: wasEscalated,
|
|
943
|
+
engagedAfterEscalation,
|
|
944
|
+
responseLength: response.output?.length || 0,
|
|
945
|
+
executionTime: response.executionTime,
|
|
946
|
+
tier: engagedAfterEscalation ? finalTier : (wasEscalated ? finalTier : 'standard'),
|
|
947
|
+
});
|
|
775
948
|
if (response.success && response.output) {
|
|
776
949
|
transcript.push({
|
|
777
950
|
agent,
|
|
@@ -786,6 +959,28 @@ Remember: NEVER concede. Your thesis is correct. Argue to WIN.`;
|
|
|
786
959
|
}
|
|
787
960
|
catch (error) {
|
|
788
961
|
logger.error(`❌ ${agent.toUpperCase()} (${position}) threw error:`, error);
|
|
962
|
+
completedTurns++;
|
|
963
|
+
if (onStreamingEvent) {
|
|
964
|
+
onStreamingEvent({
|
|
965
|
+
type: 'agent_error',
|
|
966
|
+
agent,
|
|
967
|
+
content: `Round ${round}/${rounds}: ${agent.toUpperCase()} (${position}) error: ${error instanceof Error ? error.message : String(error)}`,
|
|
968
|
+
timestamp: Date.now(),
|
|
969
|
+
sessionId,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
turnMetadata.push({
|
|
973
|
+
agent: agent,
|
|
974
|
+
position: position,
|
|
975
|
+
round,
|
|
976
|
+
engaged: false,
|
|
977
|
+
refused: false,
|
|
978
|
+
escalated: false,
|
|
979
|
+
engagedAfterEscalation: false,
|
|
980
|
+
responseLength: 0,
|
|
981
|
+
executionTime: 0,
|
|
982
|
+
tier: 'standard',
|
|
983
|
+
});
|
|
789
984
|
debateResponses.push({
|
|
790
985
|
agent,
|
|
791
986
|
success: false,
|
|
@@ -795,21 +990,58 @@ Remember: NEVER concede. Your thesis is correct. Argue to WIN.`;
|
|
|
795
990
|
});
|
|
796
991
|
}
|
|
797
992
|
}
|
|
798
|
-
// Compress context for next round (if not final round)
|
|
993
|
+
// Compress context for next round with mediation (if not final round)
|
|
799
994
|
if (round < rounds) {
|
|
800
995
|
const roundTranscript = transcript
|
|
801
996
|
.filter(t => t.round === round)
|
|
802
|
-
.map(t =>
|
|
997
|
+
.map(t => {
|
|
998
|
+
const { sanitized } = mediateTranscript(t.content, 'sanitize', 1500);
|
|
999
|
+
return `${t.agent.toUpperCase()} (${t.position}): ${sanitized}`;
|
|
1000
|
+
})
|
|
803
1001
|
.join('\n\n---\n\n');
|
|
804
1002
|
compressedContext = `Round ${round} Summary:\n${roundTranscript}`;
|
|
805
1003
|
}
|
|
806
1004
|
}
|
|
807
|
-
//
|
|
808
|
-
const
|
|
1005
|
+
// Frontier 3: Compute position-dependent asymmetry summary
|
|
1006
|
+
const proTurns = turnMetadata.filter(t => t.position === 'PRO');
|
|
1007
|
+
const conTurns = turnMetadata.filter(t => t.position === 'CON');
|
|
1008
|
+
const proRefusalRate = proTurns.length > 0
|
|
1009
|
+
? proTurns.filter(t => t.refused).length / proTurns.length : 0;
|
|
1010
|
+
const conRefusalRate = conTurns.length > 0
|
|
1011
|
+
? conTurns.filter(t => t.refused).length / conTurns.length : 0;
|
|
1012
|
+
const debateAgents = [...new Set(turnMetadata.map(t => t.agent))];
|
|
1013
|
+
const agentAsymmetries = debateAgents.map(a => {
|
|
1014
|
+
const aPro = turnMetadata.filter(t => t.agent === a && t.position === 'PRO');
|
|
1015
|
+
const aCon = turnMetadata.filter(t => t.agent === a && t.position === 'CON');
|
|
1016
|
+
const proEngaged = aPro.some(t => t.engaged);
|
|
1017
|
+
const conEngaged = aCon.some(t => t.engaged);
|
|
1018
|
+
return { agent: a, proEngaged, conEngaged, asymmetric: proEngaged !== conEngaged };
|
|
1019
|
+
});
|
|
1020
|
+
const asymmetryDetected = Math.abs(proRefusalRate - conRefusalRate) > 0.3
|
|
1021
|
+
|| agentAsymmetries.some(a => a.asymmetric);
|
|
1022
|
+
const behaviorSummary = {
|
|
1023
|
+
topic, proPosition, conPosition,
|
|
1024
|
+
turns: turnMetadata,
|
|
1025
|
+
asymmetry: {
|
|
1026
|
+
detected: asymmetryDetected,
|
|
1027
|
+
description: asymmetryDetected
|
|
1028
|
+
? `Position-dependent asymmetry: PRO refusal ${(proRefusalRate * 100).toFixed(0)}%, CON refusal ${(conRefusalRate * 100).toFixed(0)}%`
|
|
1029
|
+
: 'No significant position-dependent asymmetry detected',
|
|
1030
|
+
proRefusalRate,
|
|
1031
|
+
conRefusalRate,
|
|
1032
|
+
agentAsymmetries,
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
if (asymmetryDetected) {
|
|
1036
|
+
logger.warn(`🎭 Alignment asymmetry detected: ${behaviorSummary.asymmetry.description}`);
|
|
1037
|
+
}
|
|
1038
|
+
// Build synthesis with behavioral data
|
|
1039
|
+
const synthesis = this.synthesizeDebate(debateResponses, topic, rounds, new Map([[proAgent, `PRO: ${proPosition}`], [conAgent, `CON: ${conPosition}`]]), behaviorSummary);
|
|
809
1040
|
return {
|
|
810
1041
|
success: debateResponses.some(r => r.success),
|
|
811
1042
|
responses: debateResponses,
|
|
812
1043
|
synthesis,
|
|
1044
|
+
debateBehavior: behaviorSummary,
|
|
813
1045
|
analysisType: 'cli_debate',
|
|
814
1046
|
topic
|
|
815
1047
|
};
|
|
@@ -822,7 +1054,7 @@ Remember: NEVER concede. Your thesis is correct. Argue to WIN.`;
|
|
|
822
1054
|
/**
|
|
823
1055
|
* Synthesize debate results into formatted output
|
|
824
1056
|
*/
|
|
825
|
-
synthesizeDebate(responses, topic, rounds, agentPositions) {
|
|
1057
|
+
synthesizeDebate(responses, topic, rounds, agentPositions, behaviorSummary) {
|
|
826
1058
|
const successfulResponses = responses.filter(r => r.success);
|
|
827
1059
|
if (successfulResponses.length === 0) {
|
|
828
1060
|
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')}`;
|
|
@@ -887,6 +1119,29 @@ Remember: NEVER concede. Your thesis is correct. Argue to WIN.`;
|
|
|
887
1119
|
synthesis += `---\n\n`;
|
|
888
1120
|
});
|
|
889
1121
|
}
|
|
1122
|
+
// Frontier 3: Surface position-dependent alignment asymmetries
|
|
1123
|
+
if (behaviorSummary?.asymmetry.detected) {
|
|
1124
|
+
synthesis += `## Alignment Asymmetry Analysis\n\n`;
|
|
1125
|
+
synthesis += `**${behaviorSummary.asymmetry.description}**\n\n`;
|
|
1126
|
+
for (const a of behaviorSummary.asymmetry.agentAsymmetries) {
|
|
1127
|
+
if (a.asymmetric) {
|
|
1128
|
+
const engaged = [a.proEngaged && 'PRO', a.conEngaged && 'CON'].filter(Boolean).join(', ');
|
|
1129
|
+
const refused = [!a.proEngaged && 'PRO', !a.conEngaged && 'CON'].filter(Boolean).join(', ');
|
|
1130
|
+
synthesis += `- **${a.agent.toUpperCase()}**: Engaged on ${engaged || 'neither'}. Refused ${refused || 'neither'}.\n`;
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
synthesis += `- **${a.agent.toUpperCase()}**: Symmetric — engaged on both positions.\n`;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
synthesis += '\n';
|
|
1137
|
+
// Surface escalation outcomes
|
|
1138
|
+
const escalatedTurns = behaviorSummary.turns.filter(t => t.escalated);
|
|
1139
|
+
if (escalatedTurns.length > 0) {
|
|
1140
|
+
synthesis += `**Escalation results:** ${escalatedTurns.length} turn(s) triggered analytical reframing. `;
|
|
1141
|
+
const recovered = escalatedTurns.filter(t => t.engagedAfterEscalation).length;
|
|
1142
|
+
synthesis += `${recovered} recovered, ${escalatedTurns.length - recovered} persisted in refusal.\n\n`;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
890
1145
|
synthesis += `## Debate Synthesis\n`;
|
|
891
1146
|
synthesis += `After ${rounds} rounds of brutal adversarial analysis involving ${Array.from(new Set(successfulResponses.map(r => r.agent))).length} CLI agents, `;
|
|
892
1147
|
synthesis += `your work has been systematically demolished from multiple perspectives. `;
|