dialectic 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.cursor/commands/setup-test.mdc +175 -0
  2. package/.cursor/rules/basic-code-cleanup.mdc +1110 -0
  3. package/.cursor/rules/riper5.mdc +96 -0
  4. package/.env.example +6 -0
  5. package/AGENTS.md +1052 -0
  6. package/LICENSE +21 -0
  7. package/README.md +93 -0
  8. package/WARP.md +113 -0
  9. package/dialectic-1.0.0.tgz +0 -0
  10. package/dialectic.js +10 -0
  11. package/docs/commands.md +375 -0
  12. package/docs/configuration.md +882 -0
  13. package/docs/context_summarization.md +1023 -0
  14. package/docs/debate_flow.md +1127 -0
  15. package/docs/eval_flow.md +795 -0
  16. package/docs/evaluator.md +141 -0
  17. package/examples/debate-config-openrouter.json +48 -0
  18. package/examples/debate_config1.json +48 -0
  19. package/examples/eval/eval1/eval_config1.json +13 -0
  20. package/examples/eval/eval1/result1.json +62 -0
  21. package/examples/eval/eval1/result2.json +97 -0
  22. package/examples/eval_summary_format.md +11 -0
  23. package/examples/example3/debate-config.json +64 -0
  24. package/examples/example3/eval_config2.json +25 -0
  25. package/examples/example3/problem.md +17 -0
  26. package/examples/example3/rounds_test/eval_run.sh +16 -0
  27. package/examples/example3/rounds_test/run_test.sh +16 -0
  28. package/examples/kata1/architect-only-solution_2-rounds.json +121 -0
  29. package/examples/kata1/architect-perf-solution_2-rounds.json +234 -0
  30. package/examples/kata1/debate-config-kata1.json +54 -0
  31. package/examples/kata1/eval_architect-only_2-rounds.json +97 -0
  32. package/examples/kata1/eval_architect-perf_2-rounds.json +97 -0
  33. package/examples/kata1/kata1-report.md +12224 -0
  34. package/examples/kata1/kata1-report_temps-01_01_01_07.md +2451 -0
  35. package/examples/kata1/kata1.md +5 -0
  36. package/examples/kata1/meta.txt +1 -0
  37. package/examples/kata2/debate-config.json +54 -0
  38. package/examples/kata2/eval_config1.json +21 -0
  39. package/examples/kata2/eval_config2.json +25 -0
  40. package/examples/kata2/kata2.md +5 -0
  41. package/examples/kata2/only_architect/debate-config.json +45 -0
  42. package/examples/kata2/only_architect/eval_run.sh +11 -0
  43. package/examples/kata2/only_architect/run_test.sh +5 -0
  44. package/examples/kata2/rounds_test/eval_run.sh +11 -0
  45. package/examples/kata2/rounds_test/run_test.sh +5 -0
  46. package/examples/kata2/summary_length_test/eval_run.sh +11 -0
  47. package/examples/kata2/summary_length_test/eval_run_w_clarify.sh +7 -0
  48. package/examples/kata2/summary_length_test/run_test.sh +5 -0
  49. package/examples/task-queue/debate-config.json +76 -0
  50. package/examples/task-queue/debate_report.md +566 -0
  51. package/examples/task-queue/task-queue-system.md +25 -0
  52. package/jest.config.ts +13 -0
  53. package/multi_agent_debate_spec.md +2980 -0
  54. package/package.json +38 -0
  55. package/sanity-check-problem.txt +9 -0
  56. package/src/agents/prompts/architect-prompts.ts +203 -0
  57. package/src/agents/prompts/generalist-prompts.ts +157 -0
  58. package/src/agents/prompts/index.ts +41 -0
  59. package/src/agents/prompts/judge-prompts.ts +19 -0
  60. package/src/agents/prompts/kiss-prompts.ts +230 -0
  61. package/src/agents/prompts/performance-prompts.ts +142 -0
  62. package/src/agents/prompts/prompt-types.ts +68 -0
  63. package/src/agents/prompts/security-prompts.ts +149 -0
  64. package/src/agents/prompts/shared.ts +144 -0
  65. package/src/agents/prompts/testing-prompts.ts +149 -0
  66. package/src/agents/role-based-agent.ts +386 -0
  67. package/src/cli/commands/debate.ts +761 -0
  68. package/src/cli/commands/eval.ts +475 -0
  69. package/src/cli/commands/report.ts +265 -0
  70. package/src/cli/index.ts +79 -0
  71. package/src/core/agent.ts +198 -0
  72. package/src/core/clarifications.ts +34 -0
  73. package/src/core/judge.ts +257 -0
  74. package/src/core/orchestrator.ts +432 -0
  75. package/src/core/state-manager.ts +322 -0
  76. package/src/eval/evaluator-agent.ts +130 -0
  77. package/src/eval/prompts/system.md +41 -0
  78. package/src/eval/prompts/user.md +64 -0
  79. package/src/providers/llm-provider.ts +25 -0
  80. package/src/providers/openai-provider.ts +84 -0
  81. package/src/providers/openrouter-provider.ts +122 -0
  82. package/src/providers/provider-factory.ts +64 -0
  83. package/src/types/agent.types.ts +141 -0
  84. package/src/types/config.types.ts +47 -0
  85. package/src/types/debate.types.ts +237 -0
  86. package/src/types/eval.types.ts +85 -0
  87. package/src/utils/common.ts +104 -0
  88. package/src/utils/context-formatter.ts +102 -0
  89. package/src/utils/context-summarizer.ts +143 -0
  90. package/src/utils/env-loader.ts +46 -0
  91. package/src/utils/exit-codes.ts +5 -0
  92. package/src/utils/id.ts +11 -0
  93. package/src/utils/logger.ts +48 -0
  94. package/src/utils/paths.ts +10 -0
  95. package/src/utils/progress-ui.ts +313 -0
  96. package/src/utils/prompt-loader.ts +79 -0
  97. package/src/utils/report-generator.ts +301 -0
  98. package/tests/clarifications.spec.ts +128 -0
  99. package/tests/cli.debate.spec.ts +144 -0
  100. package/tests/config-loading.spec.ts +206 -0
  101. package/tests/context-summarizer.spec.ts +131 -0
  102. package/tests/debate-config-custom.json +38 -0
  103. package/tests/env-loader.spec.ts +149 -0
  104. package/tests/eval.command.spec.ts +1191 -0
  105. package/tests/logger.spec.ts +19 -0
  106. package/tests/openai-provider.spec.ts +26 -0
  107. package/tests/openrouter-provider.spec.ts +279 -0
  108. package/tests/orchestrator-summary.spec.ts +386 -0
  109. package/tests/orchestrator.spec.ts +207 -0
  110. package/tests/prompt-loader.spec.ts +52 -0
  111. package/tests/prompts/architect.md +16 -0
  112. package/tests/provider-factory.spec.ts +150 -0
  113. package/tests/report.command.spec.ts +546 -0
  114. package/tests/role-based-agent-summary.spec.ts +476 -0
  115. package/tests/security-agent.spec.ts +221 -0
  116. package/tests/shared-prompts.spec.ts +318 -0
  117. package/tests/state-manager.spec.ts +251 -0
  118. package/tests/summary-prompts.spec.ts +153 -0
  119. package/tsconfig.json +49 -0
@@ -0,0 +1,761 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import { Command } from 'commander';
5
+ import { EXIT_INVALID_ARGS, EXIT_GENERAL_ERROR } from '../../utils/exit-codes';
6
+ import { warnUser, infoUser, writeStderr } from '../index';
7
+ import { SystemConfig } from '../../types/config.types';
8
+ import { AgentConfig, AGENT_ROLES, LLM_PROVIDERS, PROMPT_SOURCES, AgentPromptMetadata, JudgePromptMetadata, PromptSource } from '../../types/agent.types';
9
+ import { DebateConfig, DebateResult, DebateRound, Contribution, ContributionType, TERMINATION_TYPES, SYNTHESIS_METHODS, CONTRIBUTION_TYPES, SummarizationConfig, AgentClarifications } from '../../types/debate.types';
10
+ import { DEFAULT_SUMMARIZATION_ENABLED, DEFAULT_SUMMARIZATION_THRESHOLD, DEFAULT_SUMMARIZATION_MAX_LENGTH, DEFAULT_SUMMARIZATION_METHOD } from '../../types/config.types';
11
+ import { LLMProvider } from '../../providers/llm-provider';
12
+ import { createProvider } from '../../providers/provider-factory';
13
+ import { RoleBasedAgent } from '../../agents/role-based-agent';
14
+ import { JudgeAgent } from '../../core/judge';
15
+ import { StateManager } from '../../core/state-manager';
16
+ import { DebateOrchestrator } from '../../core/orchestrator';
17
+ import { resolvePrompt } from '../../utils/prompt-loader';
18
+ import { loadEnvironmentFile } from '../../utils/env-loader';
19
+ import { Agent } from '../../core/agent';
20
+ import { DebateProgressUI } from '../../utils/progress-ui';
21
+ import { generateDebateReport } from '../../utils/report-generator';
22
+ import { collectClarifications } from '../../core/clarifications';
23
+ import { createValidationError, writeFileWithDirectories } from '../../utils/common';
24
+
25
+ const DEFAULT_CONFIG_PATH = path.resolve(process.cwd(), 'debate-config.json');
26
+ const DEFAULT_ROUNDS = 3;
27
+ const DEFAULT_CLARIFICATIONS_MAX_PER_AGENT = 5;
28
+
29
+ // File handling constants
30
+ const FILE_ENCODING_UTF8 = 'utf-8';
31
+ const JSON_FILE_EXTENSION = '.json';
32
+ const MARKDOWN_FILE_EXTENSION = '.md';
33
+ const JSON_INDENT_SPACES = 2;
34
+
35
+ // Problem resolution error messages
36
+ const ERROR_BOTH_PROBLEM_SOURCES = 'Invalid arguments: provide exactly one of <problem> or --problemDescription';
37
+ const ERROR_NO_PROBLEM_SOURCE = 'Invalid arguments: problem is required (provide <problem> or --problemDescription)';
38
+ const ERROR_FILE_NOT_FOUND = 'Invalid arguments: problem description file not found';
39
+ const ERROR_PATH_IS_DIRECTORY = 'Invalid arguments: problem description path is a directory';
40
+ const ERROR_FILE_EMPTY = 'Invalid arguments: problem description file is empty';
41
+ const ERROR_FILE_READ_FAILED = 'Failed to read problem description file';
42
+
43
+ // Default agent identifiers and names
44
+ const DEFAULT_ARCHITECT_ID = 'agent-architect';
45
+ const DEFAULT_ARCHITECT_NAME = 'System Architect';
46
+ const DEFAULT_PERFORMANCE_ID = 'agent-performance';
47
+ const DEFAULT_PERFORMANCE_NAME = 'Performance Engineer';
48
+ const DEFAULT_KISS_ID = 'agent-kiss';
49
+ const DEFAULT_KISS_NAME = 'Simplicity Advocate';
50
+ const DEFAULT_JUDGE_ID = 'judge-main';
51
+ const DEFAULT_JUDGE_NAME = 'Technical Judge';
52
+
53
+ // Default LLM configuration
54
+ const DEFAULT_LLM_MODEL = 'gpt-4';
55
+ const DEFAULT_AGENT_TEMPERATURE = 0.5;
56
+ const DEFAULT_JUDGE_TEMPERATURE = 0.3;
57
+
58
+ // Default summary prompt fallback
59
+ const DEFAULT_SUMMARY_PROMPT_FALLBACK = 'Summarize the following debate history from your perspective, preserving key points and decisions.';
60
+
61
+ /**
62
+ * Collects clarifying questions from agents and prompts the user for answers.
63
+ * Returns the collected clarifications with user-provided answers.
64
+ *
65
+ * @param resolvedProblem - The problem statement to clarify
66
+ * @param agents - Array of agents to collect questions from
67
+ * @param maxPerAgent - Maximum questions per agent
68
+ * @returns Promise resolving to collected clarifications with answers
69
+ */
70
+ async function collectAndAnswerClarifications( resolvedProblem: string, agents: Agent[], maxPerAgent: number ): Promise<AgentClarifications[]>
71
+ {
72
+ // Collect questions from agents (grouped, truncated with warnings)
73
+ const collected: AgentClarifications[] = await collectClarifications( resolvedProblem, agents, maxPerAgent, (msg) => warnUser(msg) );
74
+
75
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
76
+
77
+
78
+ async function askUser(promptText: string): Promise<string> { // Inline helper to convert readline callback to Promise, scoped to this readline instance
79
+ return await new Promise((resolve) => rl.question(promptText, resolve));
80
+ }
81
+
82
+ try { // Prompt user for answers (empty line => NA)
83
+ for (const group of collected) {
84
+ if (group.items.length === 0) continue;
85
+ writeStderr(`\n[${group.agentName}] Clarifying Questions\n`);
86
+ for (const item of group.items) {
87
+ const ans = (await askUser(`Q (${item.id}): ${item.question}\n> `)).trim();
88
+ item.answer = ans.length === 0 ? 'NA' : ans;
89
+ }
90
+ }
91
+ } finally {
92
+ rl.close();
93
+ }
94
+
95
+ return collected;
96
+ }
97
+
98
+ /**
99
+ * Creates orchestrator hooks that drive the progress UI during debate execution.
100
+ *
101
+ * @param progressUI - The progress UI instance to drive
102
+ * @param options - CLI options containing verbose flag
103
+ * @returns Object containing all orchestrator hook functions
104
+ */
105
+ function createOrchestratorHooks(progressUI: DebateProgressUI, options: any) {
106
+ const SUMMARY_ACTIVITY_LABEL = 'summarizing context';
107
+ return {
108
+ onRoundStart: (roundNumber: number, _totalRounds: number) => {
109
+ progressUI.startRound(roundNumber);
110
+ },
111
+ onPhaseStart: (_roundNumber: number, phase: ContributionType, expectedTaskCount: number) => {
112
+ progressUI.startPhase(phase, expectedTaskCount);
113
+ },
114
+ onAgentStart: (agentName: string, activity: string) => {
115
+ progressUI.startAgentActivity(agentName, activity);
116
+ },
117
+ onAgentComplete: (agentName: string, activity: string) => {
118
+ progressUI.completeAgentActivity(agentName, activity);
119
+ },
120
+ onPhaseComplete: (_roundNumber: number, phase: ContributionType) => {
121
+ progressUI.completePhase(phase);
122
+ },
123
+ onSummarizationStart: (agentName: string) => {
124
+ progressUI.startAgentActivity(agentName, SUMMARY_ACTIVITY_LABEL);
125
+ },
126
+ onSummarizationComplete: (agentName: string, beforeChars: number, afterChars: number) => {
127
+ progressUI.completeAgentActivity(agentName, SUMMARY_ACTIVITY_LABEL);
128
+ if (options.verbose) {
129
+ progressUI.log(` [${agentName}] Summarized: ${beforeChars} → ${afterChars} chars`);
130
+ }
131
+ },
132
+ // Ensure activity is cleared even when no summary is produced
133
+ onSummarizationEnd: (agentName: string) => {
134
+ progressUI.completeAgentActivity(agentName, SUMMARY_ACTIVITY_LABEL);
135
+ },
136
+ onSynthesisStart: () => {
137
+ progressUI.startSynthesis();
138
+ },
139
+ onSynthesisComplete: () => {
140
+ progressUI.completeSynthesis();
141
+ },
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Creates a judge agent with resolved prompts and metadata collection.
147
+ *
148
+ * @param sysConfig - System configuration containing judge settings
149
+ * @param systemSummaryConfig - System-wide summarization configuration
150
+ * @param promptSources - Collection object to record prompt source metadata
151
+ * @returns Configured JudgeAgent instance
152
+ */
153
+ function createJudgeWithPromptResolution(
154
+ sysConfig: SystemConfig,
155
+ systemSummaryConfig: SummarizationConfig,
156
+ promptSources: { judge: JudgePromptMetadata }
157
+ ): JudgeAgent {
158
+ // Judge prompt resolution
159
+ const judgeDefault = JudgeAgent.defaultSystemPrompt();
160
+ const jres = resolvePrompt({
161
+ label: sysConfig.judge!.name,
162
+ configDir: sysConfig.configDir || process.cwd(),
163
+ ...((sysConfig.judge!.systemPromptPath !== undefined) && { promptPath: sysConfig.judge!.systemPromptPath }),
164
+ defaultText: judgeDefault
165
+ });
166
+
167
+ // Judge summary prompt resolution
168
+ const judgeSummaryDefault = JudgeAgent.defaultSummaryPrompt('', systemSummaryConfig.maxLength);
169
+ const jsres = resolvePrompt({
170
+ label: `${sysConfig.judge!.name} (summary)`,
171
+ configDir: sysConfig.configDir || process.cwd(),
172
+ ...((sysConfig.judge!.summaryPromptPath !== undefined) && { promptPath: sysConfig.judge!.summaryPromptPath }),
173
+ defaultText: judgeSummaryDefault
174
+ });
175
+
176
+ const judgeProvider = createProvider(sysConfig.judge!.provider);
177
+ const judge = new JudgeAgent(
178
+ sysConfig.judge!,
179
+ judgeProvider,
180
+ jres.text,
181
+ jres.source === PROMPT_SOURCES.FILE ? ({ source: PROMPT_SOURCES.FILE, ...(jres.absPath !== undefined && { absPath: jres.absPath }) }) : ({ source: PROMPT_SOURCES.BUILT_IN }),
182
+ systemSummaryConfig,
183
+ jsres.source === PROMPT_SOURCES.FILE ? ({ source: PROMPT_SOURCES.FILE, ...(jsres.absPath !== undefined && { absPath: jsres.absPath }) }) : ({ source: PROMPT_SOURCES.BUILT_IN })
184
+ );
185
+
186
+ // Record prompt source metadata
187
+ promptSources.judge = {
188
+ id: sysConfig.judge!.id,
189
+ source: jres.source,
190
+ ...(jres.absPath !== undefined && { path: jres.absPath }),
191
+ summarySource: jsres.source,
192
+ ...(jsres.absPath !== undefined && { summaryPath: jsres.absPath })
193
+ };
194
+
195
+ return judge;
196
+ }
197
+
198
+ /**
199
+ * Returns the built-in default system configuration for the debate system.
200
+ *
201
+ * This includes:
202
+ * - Two default agents: a System Architect and a Performance Engineer, both using the GPT-4 model via OpenAI.
203
+ * - A default judge agent with the role of "generalist" and a lower temperature for more deterministic judging.
204
+ * - Default debate configuration: 3 rounds, fixed termination, judge-based synthesis, full history included, and a 5-minute timeout per round.
205
+ *
206
+ * @returns {SystemConfig} The default system configuration object.
207
+ */
208
+ function builtInDefaults(): SystemConfig {
209
+ const defaultAgents: AgentConfig[] = [
210
+ { id: DEFAULT_ARCHITECT_ID, name: DEFAULT_ARCHITECT_NAME, role: AGENT_ROLES.ARCHITECT,
211
+ model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_AGENT_TEMPERATURE, enabled: true },
212
+ { id: DEFAULT_PERFORMANCE_ID, name: DEFAULT_PERFORMANCE_NAME, role: AGENT_ROLES.PERFORMANCE,
213
+ model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_AGENT_TEMPERATURE, enabled: true },
214
+ { id: DEFAULT_KISS_ID, name: DEFAULT_KISS_NAME, role: AGENT_ROLES.KISS,
215
+ model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_AGENT_TEMPERATURE, enabled: true },
216
+ ];
217
+ const judge: AgentConfig = { id: DEFAULT_JUDGE_ID, name: DEFAULT_JUDGE_NAME, role: AGENT_ROLES.GENERALIST,
218
+ model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_JUDGE_TEMPERATURE };
219
+
220
+ // Default summarization configuration
221
+ const summarization = {
222
+ enabled: DEFAULT_SUMMARIZATION_ENABLED,
223
+ threshold: DEFAULT_SUMMARIZATION_THRESHOLD,
224
+ maxLength: DEFAULT_SUMMARIZATION_MAX_LENGTH,
225
+ method: DEFAULT_SUMMARIZATION_METHOD,
226
+ };
227
+
228
+ const debate: DebateConfig = {
229
+ rounds: DEFAULT_ROUNDS,
230
+ terminationCondition: { type: TERMINATION_TYPES.FIXED },
231
+ synthesisMethod: SYNTHESIS_METHODS.JUDGE,
232
+ includeFullHistory: true,
233
+ timeoutPerRound: 300000,
234
+ summarization,
235
+ };
236
+ return { agents: defaultAgents, judge, debate } as SystemConfig;
237
+ }
238
+
239
+ /**
240
+ * Loads the system configuration for the debate system from a specified file path,
241
+ * or falls back to a default location and built-in defaults if the file is missing or incomplete.
242
+ *
243
+ * This function attempts to read and parse a JSON configuration file containing agent, judge,
244
+ * and debate settings. If the file does not exist, or if required fields are missing,
245
+ * it prints a warning to stderr and uses built-in defaults for the missing parts.
246
+ *
247
+ * The function ensures that:
248
+ * - If the config file is missing, the entire built-in default configuration is used.
249
+ * - If the config file is present but missing the 'agents' array or it is empty,
250
+ * the entire built-in default configuration is used.
251
+ * - If the config file is missing the 'judge' or 'debate' fields, those fields are filled in
252
+ * from the built-in defaults, and a warning is printed.
253
+ *
254
+ * @param {string} [configPath] - Optional path to the configuration file. If not provided,
255
+ * uses the default path ('debate-config.json' in the current working directory).
256
+ * @returns {Promise<SystemConfig>} The loaded and validated system configuration object.
257
+ */
258
+ export async function loadConfig(configPath?: string): Promise<SystemConfig> {
259
+ const finalPath = configPath ? path.resolve(process.cwd(), configPath) : DEFAULT_CONFIG_PATH;
260
+ const defaults = builtInDefaults();
261
+
262
+ if (!fs.existsSync(finalPath)) {
263
+ warnUser(`Config not found at ${finalPath}. Using built-in defaults.`);
264
+ defaults.configDir = process.cwd();
265
+ return defaults;
266
+ }
267
+
268
+ const raw = await fs.promises.readFile(finalPath, FILE_ENCODING_UTF8);
269
+ const parsed = JSON.parse(raw);
270
+
271
+ // Ensure shape minimal
272
+ if (!Array.isArray(parsed.agents) || parsed.agents.length === 0) {
273
+ warnUser('Config missing agents. Using built-in defaults.');
274
+ defaults.configDir = path.dirname(finalPath);
275
+ return defaults;
276
+ }
277
+
278
+ if (!parsed.judge) {
279
+ warnUser('Config missing judge. Using default judge.');
280
+ parsed.judge = defaults.judge;
281
+ }
282
+
283
+ if (!parsed.debate) {
284
+ parsed.debate = defaults.debate;
285
+ }
286
+
287
+ parsed.configDir = path.dirname(finalPath);
288
+ return parsed as SystemConfig;
289
+ }
290
+
291
+ /**
292
+ * Helper to create an agent instance with prompt resolution and metadata collection.
293
+ * Resolves system and summary prompts from files or defaults, merges summarization config,
294
+ * creates the agent, and records provenance.
295
+ *
296
+ * @param cfg - Agent configuration containing role, model, and other settings.
297
+ * @param provider - LLM provider instance for LLM interactions.
298
+ * @param configDir - Directory path where the configuration file is located.
299
+ * @param systemSummaryConfig - System-wide summarization configuration.
300
+ * @param collect - Collection object to record prompt source metadata.
301
+ * @returns A configured RoleBasedAgent instance.
302
+ */
303
+ function createAgentWithPromptResolution(
304
+ cfg: AgentConfig,
305
+ provider: LLMProvider,
306
+ configDir: string,
307
+ systemSummaryConfig: SummarizationConfig,
308
+ collect: { agents: AgentPromptMetadata[] }
309
+ ): Agent {
310
+ // Resolve system prompt
311
+ const defaultText = RoleBasedAgent.defaultSystemPrompt(cfg.role);
312
+ const res = resolvePrompt({
313
+ label: cfg.name,
314
+ configDir,
315
+ ...(cfg.systemPromptPath !== undefined && { promptPath: cfg.systemPromptPath }),
316
+ defaultText
317
+ });
318
+
319
+ const promptSource: PromptSource = res.source === PROMPT_SOURCES.FILE
320
+ ? { source: PROMPT_SOURCES.FILE, ...(res.absPath !== undefined && { absPath: res.absPath }) }
321
+ : { source: PROMPT_SOURCES.BUILT_IN };
322
+
323
+ // Merge summarization config (agent-level overrides system-level)
324
+ const mergedSummaryConfig: SummarizationConfig = {
325
+ ...systemSummaryConfig,
326
+ ...cfg.summarization,
327
+ };
328
+
329
+ // Resolve summary prompt
330
+ // For the default, we use a generic fallback prompt
331
+ const summaryRes = resolvePrompt({
332
+ label: `${cfg.name} (summary)`,
333
+ configDir,
334
+ ...(cfg.summaryPromptPath !== undefined && { promptPath: cfg.summaryPromptPath }),
335
+ defaultText: DEFAULT_SUMMARY_PROMPT_FALLBACK
336
+ });
337
+
338
+ const summaryPromptSource: PromptSource = summaryRes.source === PROMPT_SOURCES.FILE
339
+ ? { source: PROMPT_SOURCES.FILE, ...(summaryRes.absPath !== undefined && { absPath: summaryRes.absPath }) }
340
+ : { source: PROMPT_SOURCES.BUILT_IN };
341
+
342
+ // Resolve clarification prompt (optional)
343
+ const clarificationRes = resolvePrompt({
344
+ label: `${cfg.name} (clarifications)`,
345
+ configDir,
346
+ ...(cfg.clarificationPromptPath !== undefined && { promptPath: cfg.clarificationPromptPath }),
347
+ defaultText: ''
348
+ });
349
+
350
+ // Create agent with all resolved parameters
351
+ const agent = RoleBasedAgent.create( cfg, provider, res.text, promptSource,
352
+ mergedSummaryConfig, summaryPromptSource,
353
+ clarificationRes.text );
354
+
355
+ collect.agents.push({
356
+ agentId: cfg.id,
357
+ role: cfg.role,
358
+ source: res.source,
359
+ ...(res.absPath !== undefined && { path: res.absPath })
360
+ });
361
+
362
+ return agent;
363
+ }
364
+
365
+ /**
366
+ * Builds an array of Agent instances based on the provided configuration and LLM provider.
367
+ *
368
+ * Creates RoleBasedAgent instances for each agent configuration, resolving system and summary
369
+ * prompts from files or defaults, merging summarization configs, and collecting metadata about
370
+ * prompt sources. The RoleBasedAgent class handles all roles through a prompt registry,
371
+ * eliminating the need for role-specific agent classes.
372
+ *
373
+ * @param agentConfigs - Array of agent configurations.
374
+ * @param configDir - Directory where the config file is located, used for resolving relative prompt paths.
375
+ * @param systemSummaryConfig - System-wide summarization configuration.
376
+ * @param collect - Object to collect prompt metadata.
377
+ * @returns Array of Agent instances.
378
+ */
379
+ function buildAgents(
380
+ agentConfigs: AgentConfig[],
381
+ configDir: string,
382
+ systemSummaryConfig: SummarizationConfig,
383
+ collect: { agents: AgentPromptMetadata[] }
384
+ ): Agent[] {
385
+ return agentConfigs.map((cfg) => {
386
+ const provider = createProvider(cfg.provider);
387
+ return createAgentWithPromptResolution(cfg, provider, configDir, systemSummaryConfig, collect);
388
+ });
389
+ }
390
+
391
+ /**
392
+ * Creates a DebateConfig from the system configuration and command-line options.
393
+ * Validates that the number of rounds is at least 1.
394
+ *
395
+ * @param {SystemConfig} sysConfig - The system configuration.
396
+ * @param {any} options - Command-line options containing optional rounds override.
397
+ * @returns {DebateConfig} The debate configuration.
398
+ * @throws {Error} If rounds is less than 1.
399
+ */
400
+ function debateConfigFromSysConfig(sysConfig: SystemConfig, options: any): DebateConfig {
401
+ const debateCfg: DebateConfig = {
402
+ ...sysConfig.debate!,
403
+ rounds: options.rounds ? parseInt(options.rounds, 10) : (sysConfig.debate?.rounds ?? DEFAULT_ROUNDS),
404
+ } as DebateConfig;
405
+
406
+ if (!debateCfg.rounds || debateCfg.rounds < 1) {
407
+ throw createValidationError('Invalid arguments: --rounds must be >= 1', EXIT_INVALID_ARGS);
408
+ }
409
+
410
+ return debateCfg;
411
+ }
412
+
413
+ /**
414
+ * Filters and returns agent configurations from the system configuration based on command-line options.
415
+ * If specific agent roles are provided via options, only agents with matching roles are included.
416
+ * If no agents are selected after filtering, defaults to built-in agents.
417
+ *
418
+ * @param {SystemConfig} sysConfig - The system configuration.
419
+ * @param {any} options - Command-line options containing optional agent roles filter.
420
+ * @returns {AgentConfig[]} Array of filtered agent configurations.
421
+ */
422
+ function agentConfigsFromSysConfig(sysConfig: SystemConfig, options: any): AgentConfig[] {
423
+ let agentConfigs = sysConfig.agents.filter((a) => a.enabled !== false);
424
+
425
+ if (options.agents) {
426
+ const roles = String(options.agents).split(',').map((r: string) => r.trim());
427
+ agentConfigs = agentConfigs.filter((a) => roles.includes(a.role));
428
+ }
429
+
430
+ if (agentConfigs.length === 0) {
431
+ warnUser('No agents selected; defaulting to architect,performance.');
432
+ const defaults = builtInDefaults();
433
+ agentConfigs = defaults.agents;
434
+ }
435
+
436
+ return agentConfigs;
437
+ }
438
+
439
+
440
+ /**
441
+ * Validates the exactly-one constraint for problem sources.
442
+ * @param hasProblem - Whether problem string is provided.
443
+ * @param hasFile - Whether problemDescription file is provided.
444
+ * @throws {Error} If both or neither are provided.
445
+ */
446
+ function validateExactlyOneProblemSource(hasProblem: boolean, hasFile: boolean): void {
447
+ if (hasProblem && hasFile) {
448
+ throw createValidationError(ERROR_BOTH_PROBLEM_SOURCES, EXIT_INVALID_ARGS);
449
+ }
450
+ if (!hasProblem && !hasFile) {
451
+ throw createValidationError(ERROR_NO_PROBLEM_SOURCE, EXIT_INVALID_ARGS);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Validates that the file path exists and is a file (not directory).
457
+ * @param filePath - Absolute path to the file.
458
+ * @throws {Error} If file doesn't exist or is a directory.
459
+ */
460
+ function validateFilePathExists(filePath: string): void {
461
+ if (!fs.existsSync(filePath)) {
462
+ throw createValidationError(`${ERROR_FILE_NOT_FOUND}: ${filePath}`, EXIT_INVALID_ARGS);
463
+ }
464
+
465
+ const stats = fs.statSync(filePath);
466
+ if (stats.isDirectory()) {
467
+ throw createValidationError(`${ERROR_PATH_IS_DIRECTORY}: ${filePath}`, EXIT_INVALID_ARGS);
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Reads and validates file content.
473
+ * @param filePath - Absolute path to the file.
474
+ * @returns File content as string.
475
+ * @throws {Error} If file is empty or read fails.
476
+ */
477
+ async function readAndValidateFileContent(filePath: string): Promise<string> {
478
+ try {
479
+ const content = await fs.promises.readFile(filePath, FILE_ENCODING_UTF8);
480
+
481
+ // Check if content is non-empty after trimming (whitespace-only = empty)
482
+ if (content.trim().length === 0) {
483
+ throw createValidationError(`${ERROR_FILE_EMPTY}: ${filePath}`, EXIT_INVALID_ARGS);
484
+ }
485
+
486
+ // Return raw content (preserve formatting)
487
+ return content;
488
+ } catch (error: any) {
489
+ if (error.code === EXIT_INVALID_ARGS) {
490
+ throw error;
491
+ }
492
+ // Handle read errors
493
+ throw createValidationError(`${ERROR_FILE_READ_FAILED}: ${error.message}`, EXIT_GENERAL_ERROR);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Resolves the problem description from either command line string or file.
499
+ * Enforces exactly-one constraint and validates file content.
500
+ *
501
+ * @param {string | undefined} problem - Optional problem string from command line.
502
+ * @param {any} options - Command-line options containing optional problemDescription.
503
+ * @returns {Promise<string>} The resolved problem description.
504
+ * @throws {Error} If validation fails or file operations fail.
505
+ */
506
+ async function resolveProblemDescription(problem: string | undefined, options: any): Promise<string> {
507
+ const hasProblem = !!(problem && problem.trim().length > 0);
508
+ const hasFile = !!options.problemDescription;
509
+
510
+ // Validate exactly-one constraint
511
+ validateExactlyOneProblemSource(hasProblem, hasFile);
512
+
513
+ // Return problem string if provided
514
+ if (hasProblem) {
515
+ return problem!.trim();
516
+ }
517
+
518
+ // Handle file-based problem description
519
+ const filePath = path.resolve(process.cwd(), options.problemDescription);
520
+ validateFilePathExists(filePath);
521
+ return await readAndValidateFileContent(filePath);
522
+ }
523
+
524
+ /**
525
+ * Outputs a summary of a single debate round to stderr for verbose mode.
526
+ * Lists all contributions (proposals, critiques, refinements) and summaries with metadata.
527
+ *
528
+ * @param {DebateRound} round - The debate round to summarize.
529
+ */
530
+ function outputRoundSummary(round: DebateRound): void {
531
+ writeStderr(`Round ${round.roundNumber}\n`);
532
+
533
+ // Output summaries if present
534
+ if (round.summaries && Object.keys(round.summaries).length > 0) {
535
+ writeStderr(` summaries:\n`);
536
+ Object.values(round.summaries).forEach((s) => {
537
+ const tokens = s.metadata.tokensUsed != null ? s.metadata.tokensUsed : 'N/A';
538
+ const lat = s.metadata.latencyMs != null ? `${s.metadata.latencyMs}ms` : 'N/A';
539
+ writeStderr(` [${s.agentRole}] ${s.metadata.beforeChars} → ${s.metadata.afterChars} chars\n`);
540
+ writeStderr(` (latency=${lat}, tokens=${tokens}, method=${s.metadata.method})\n`);
541
+ });
542
+ }
543
+
544
+ const types = [CONTRIBUTION_TYPES.PROPOSAL, CONTRIBUTION_TYPES.CRITIQUE, CONTRIBUTION_TYPES.REFINEMENT] as const;
545
+ types.forEach((t) => {
546
+ const items = round.contributions.filter((c: Contribution) => c.type === t);
547
+ if (items.length > 0) {
548
+ writeStderr(` ${t}:\n`);
549
+ items.forEach((c: Contribution) => {
550
+ const firstLine = c.content.split('\n')[0];
551
+ const tokens = (c.metadata && c.metadata.tokensUsed != null) ? c.metadata.tokensUsed : 'N/A';
552
+ const lat = (c.metadata && c.metadata.latencyMs != null) ? `${c.metadata.latencyMs}ms` : 'N/A';
553
+ writeStderr(` [${c.agentRole}] ${firstLine}\n`);
554
+ writeStderr(` (latency=${lat}, tokens=${tokens})\n`);
555
+ });
556
+ }
557
+ });
558
+ }
559
+
560
+ /**
561
+ * Outputs the debate results to a file or stdout, with optional verbose summary.
562
+ * If an output path is provided and ends with .json, writes the full debate state.
563
+ * Otherwise, writes the final solution text. If no output path is provided, writes to stdout.
564
+ * When verbose mode is enabled and no output file is specified, also writes a detailed summary.
565
+ *
566
+ * @param {DebateResult} result - The debate result containing the solution and metadata.
567
+ * @param {StateManager} stateManager - The state manager to retrieve the full debate state.
568
+ * @param {any} options - Command-line options containing output path and verbose flag.
569
+ * @returns {Promise<void>} A promise that resolves when output is complete.
570
+ */
571
+ async function outputResults(result: DebateResult, stateManager: StateManager, options: any): Promise<void> {
572
+ const outputPath = options.output ? path.resolve(process.cwd(), options.output) : undefined;
573
+ const finalText = result.solution.description + '\n';
574
+
575
+ if (outputPath) {
576
+ if (outputPath.toLowerCase().endsWith(JSON_FILE_EXTENSION)) {
577
+ const fullState = await stateManager.getDebate(result.debateId);
578
+ await fs.promises.writeFile(outputPath, JSON.stringify(fullState, null, JSON_INDENT_SPACES), FILE_ENCODING_UTF8);
579
+ } else {
580
+ await fs.promises.writeFile(outputPath, finalText, FILE_ENCODING_UTF8);
581
+ }
582
+ } else {
583
+ // stdout minimal
584
+ process.stdout.write(finalText);
585
+ }
586
+
587
+ // Verbose summary after solution to stderr (only when not writing to a file)
588
+ if (!outputPath && options.verbose) {
589
+ const debate = await stateManager.getDebate(result.debateId);
590
+ if (debate) {
591
+ writeStderr('\nSummary (verbose)\n');
592
+ debate.rounds.forEach(outputRoundSummary);
593
+ const totalTokens = debate.rounds.reduce((sum, r) => sum + r.contributions.reduce((s, c) => s + (c.metadata.tokensUsed ?? 0), 0), 0);
594
+ writeStderr(`\nTotals: rounds=${result.metadata.totalRounds}, duration=${result.metadata.durationMs}ms, tokens=${totalTokens ?? 'N/A'}\n`);
595
+ }
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Generates and writes a markdown report file for the debate.
601
+ *
602
+ * This function retrieves the full debate state, generates a markdown report using
603
+ * generateDebateReport, enforces the .md file extension, and writes the report to a file.
604
+ * Report generation failures are caught and logged but do not fail the debate.
605
+ *
606
+ * @param result - The debate result containing the debate ID and metadata.
607
+ * @param stateManager - The state manager to retrieve the full debate state.
608
+ * @param agentConfigs - Array of agent configurations for the report.
609
+ * @param judgeConfig - Judge configuration for the report.
610
+ * @param problemDescription - The full problem description text.
611
+ * @param options - CLI options including report path and verbose flag.
612
+ */
613
+ async function generateReport(
614
+ result: DebateResult,
615
+ stateManager: StateManager,
616
+ agentConfigs: AgentConfig[],
617
+ judgeConfig: AgentConfig,
618
+ problemDescription: string,
619
+ options: any
620
+ ): Promise<void> {
621
+ try {
622
+ // Validate and normalize report path
623
+ let reportPath = path.resolve(process.cwd(), options.report);
624
+
625
+ // Enforce .md extension
626
+ if (!reportPath.toLowerCase().endsWith(MARKDOWN_FILE_EXTENSION)) {
627
+ reportPath += MARKDOWN_FILE_EXTENSION;
628
+ warnUser(`Report path does not end with ${MARKDOWN_FILE_EXTENSION}, appending ${MARKDOWN_FILE_EXTENSION} extension: ${path.basename(reportPath)}`);
629
+ }
630
+
631
+ // Get full debate state
632
+ const debateState = await stateManager.getDebate(result.debateId);
633
+ if (!debateState) {
634
+ throw new Error(`Debate state not found for ID: ${result.debateId}`);
635
+ }
636
+
637
+ // Generate report content
638
+ const reportContent = generateDebateReport(
639
+ debateState,
640
+ agentConfigs,
641
+ judgeConfig,
642
+ problemDescription,
643
+ { verbose: options.verbose }
644
+ );
645
+
646
+ // Write report file (handles path normalization and directory creation)
647
+ const writtenPath = await writeFileWithDirectories(reportPath, reportContent);
648
+
649
+ // Notify user
650
+ infoUser(`Generated report: ${writtenPath}`);
651
+ } catch (error: any) {
652
+ warnUser(`Failed to generate report: ${error.message}`);
653
+ // Don't throw - report generation failure shouldn't fail the debate
654
+ }
655
+ }
656
+
657
+ export function debateCommand(program: Command) {
658
+ program
659
+ .command('debate')
660
+ .argument('[problem]', 'Problem statement to debate (provide exactly one of this or --problemDescription)')
661
+ .option('-a, --agents <roles>', 'Comma-separated agent roles (architect,performance,...)')
662
+ .option('-r, --rounds <number>', `Number of rounds (default ${DEFAULT_ROUNDS})`)
663
+ .option('-c, --config <path>', 'Path to configuration file (default ./debate-config.json)')
664
+ .option('-o, --output <path>', 'Output file; .json writes full state, others write final solution text')
665
+ .option('-p, --problemDescription <path>', 'Path to a text file containing the problem description')
666
+ .option('-e, --env-file <path>', 'Path to environment file (default: .env)')
667
+ .option('-v, --verbose', 'Verbose output')
668
+ .option('--report <path>', 'Generate markdown report file')
669
+ .option('--clarify', 'Run a one-time pre-debate clarifications phase')
670
+ .action(async (problem: string | undefined, options: any) => {
671
+ try {
672
+ // Load environment variables from .env file
673
+ loadEnvironmentFile(options.envFile, options.verbose);
674
+
675
+ const resolvedProblem = await resolveProblemDescription(problem, options);
676
+
677
+ const sysConfig = await loadConfig(options.config);
678
+ const debateCfg = debateConfigFromSysConfig(sysConfig, options);
679
+ const agentConfigs = agentConfigsFromSysConfig(sysConfig, options);
680
+
681
+ const promptSources: { agents: AgentPromptMetadata[]; judge: JudgePromptMetadata } = {
682
+ agents: [],
683
+ judge: { id: sysConfig.judge!.id, source: PROMPT_SOURCES.BUILT_IN },
684
+ };
685
+
686
+ // Get system-wide summarization config (use defaults if not in config)
687
+ const systemSummaryConfig: SummarizationConfig = debateCfg.summarization || {
688
+ enabled: DEFAULT_SUMMARIZATION_ENABLED,
689
+ threshold: DEFAULT_SUMMARIZATION_THRESHOLD,
690
+ maxLength: DEFAULT_SUMMARIZATION_MAX_LENGTH,
691
+ method: DEFAULT_SUMMARIZATION_METHOD,
692
+ };
693
+
694
+ const agents = buildAgents(agentConfigs, sysConfig.configDir || process.cwd(), systemSummaryConfig, promptSources);
695
+
696
+ // Create judge with prompt resolution
697
+ const judge = createJudgeWithPromptResolution(sysConfig, systemSummaryConfig, promptSources);
698
+
699
+ const stateManager = new StateManager();
700
+
701
+ // Clarifications phase (optional)
702
+ const shouldClarify: boolean = (options.clarify === true) || (sysConfig.debate?.interactiveClarifications === true);
703
+ let finalClarifications: AgentClarifications[] | undefined = undefined;
704
+ if (shouldClarify) {
705
+ const maxPer = sysConfig.debate?.clarificationsMaxPerAgent ?? DEFAULT_CLARIFICATIONS_MAX_PER_AGENT;
706
+ finalClarifications = await collectAndAnswerClarifications(resolvedProblem, agents, maxPer);
707
+ }
708
+
709
+ // Verbose header before run
710
+ if (options.verbose) {
711
+ writeStderr('Running debate (verbose)\n');
712
+ writeStderr('Active Agents:\n');
713
+ agentConfigs.forEach(a => {
714
+ const used = promptSources.agents.find(p => p.agentId === a.id);
715
+ writeStderr(` • ${a.name} (${a.model})\n`);
716
+ writeStderr(` - System prompt: ${used?.source === 'file' ? (used.path || 'file') : 'built-in default'}\n`);
717
+ });
718
+ writeStderr(`Judge: ${sysConfig.judge!.name} (${sysConfig.judge!.model})\n`);
719
+ writeStderr(` - System prompt: ${promptSources.judge.source === 'file' ? (promptSources.judge.path || 'file') : 'built-in default'}\n`);
720
+ writeStderr('\nSummarization:\n');
721
+ writeStderr(` - Enabled: ${systemSummaryConfig.enabled}\n`);
722
+ writeStderr(` - Threshold: ${systemSummaryConfig.threshold} characters\n`);
723
+ writeStderr(` - Max summary length: ${systemSummaryConfig.maxLength} characters\n`);
724
+ writeStderr(` - Method: ${systemSummaryConfig.method}\n`);
725
+ writeStderr('\n');
726
+ }
727
+
728
+ // Initialize progress UI
729
+ const progressUI = new DebateProgressUI();
730
+ progressUI.initialize(debateCfg.rounds);
731
+
732
+ // Create orchestrator hooks to drive progress UI
733
+ const hooks = createOrchestratorHooks(progressUI, options);
734
+
735
+ const orchestrator = new DebateOrchestrator(agents, judge, stateManager, debateCfg, hooks);
736
+
737
+ // Start progress UI and run debate
738
+ await progressUI.start();
739
+ const result: DebateResult = await orchestrator.runDebate(resolvedProblem, undefined, finalClarifications);
740
+ await progressUI.complete();
741
+
742
+ // Persist prompt sources once per debate
743
+ await stateManager.setPromptSources(result.debateId, promptSources);
744
+
745
+ // Persist path notice (StateManager already persisted during run)
746
+ infoUser(`Saved debate to ./debates/${result.debateId}.json`);
747
+
748
+ await outputResults(result, stateManager, options);
749
+
750
+ // Generate report if requested
751
+ if (options.report) {
752
+ await generateReport(result, stateManager, agentConfigs, sysConfig.judge!, resolvedProblem, options);
753
+ }
754
+ } catch (err: any) {
755
+ const code = typeof err?.code === 'number' ? err.code : EXIT_GENERAL_ERROR;
756
+ writeStderr((err?.message || 'Unknown error') + '\n');
757
+ // Rethrow for runCli catch to set process exit when direct run
758
+ throw Object.assign(new Error(err?.message || 'Unknown error'), { code });
759
+ }
760
+ });
761
+ }