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,546 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { runCli } from '../src/cli/index';
5
+ import { EXIT_INVALID_ARGS } from '../src/utils/exit-codes';
6
+ import { DebateState } from '../src/types/debate.types';
7
+ import { DEBATE_STATUS } from '../src/types/debate.types';
8
+
9
+ // Mock env-loader
10
+ jest.mock('../src/utils/env-loader', () => ({
11
+ loadEnvironmentFile: jest.fn()
12
+ }));
13
+
14
+ import { loadEnvironmentFile } from '../src/utils/env-loader';
15
+
16
+ const mockedLoadEnvironmentFile = loadEnvironmentFile as jest.MockedFunction<typeof loadEnvironmentFile>;
17
+
18
+ describe('CLI report command', () => {
19
+ const originalEnv = process.env;
20
+ let stderrSpy: jest.SpyInstance;
21
+ let stdoutSpy: jest.SpyInstance;
22
+ let exitSpy: jest.SpyInstance;
23
+ let tmpDir: string;
24
+
25
+ beforeEach(() => {
26
+ process.env = { ...originalEnv };
27
+ stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
28
+ stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
29
+ exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
30
+ throw new Error(`process.exit: ${code}`);
31
+ }) as any);
32
+ mockedLoadEnvironmentFile.mockClear();
33
+
34
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'report-test-'));
35
+ });
36
+
37
+ afterEach(() => {
38
+ process.env = originalEnv;
39
+ stderrSpy.mockRestore();
40
+ stdoutSpy.mockRestore();
41
+ exitSpy.mockRestore();
42
+
43
+ try {
44
+ fs.rmSync(tmpDir, { recursive: true, force: true });
45
+ } catch {}
46
+ });
47
+
48
+ /**
49
+ * Creates a minimal valid debate state for testing.
50
+ */
51
+ function createMinimalDebateState(): DebateState {
52
+ return {
53
+ id: 'deb-test-123',
54
+ problem: 'Test problem description',
55
+ status: DEBATE_STATUS.COMPLETED,
56
+ currentRound: 1,
57
+ rounds: [
58
+ {
59
+ roundNumber: 1,
60
+ contributions: [
61
+ {
62
+ agentId: 'agent-architect',
63
+ agentRole: 'architect',
64
+ type: 'proposal',
65
+ content: 'Test proposal content',
66
+ metadata: {}
67
+ }
68
+ ],
69
+ timestamp: new Date()
70
+ }
71
+ ],
72
+ createdAt: new Date(),
73
+ updatedAt: new Date(),
74
+ finalSolution: {
75
+ description: 'Test solution',
76
+ tradeoffs: [],
77
+ recommendations: [],
78
+ confidence: 80,
79
+ synthesizedBy: 'judge-main'
80
+ }
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Creates a minimal valid config file for testing.
86
+ */
87
+ function createMinimalConfig(): any {
88
+ return {
89
+ agents: [
90
+ {
91
+ id: 'agent-architect',
92
+ name: 'System Architect',
93
+ role: 'architect',
94
+ model: 'gpt-4',
95
+ provider: 'openai',
96
+ temperature: 0.5
97
+ }
98
+ ],
99
+ judge: {
100
+ id: 'judge-main',
101
+ name: 'Technical Judge',
102
+ role: 'generalist',
103
+ model: 'gpt-4',
104
+ provider: 'openai',
105
+ temperature: 0.3
106
+ }
107
+ };
108
+ }
109
+
110
+ describe('Required flag validation', () => {
111
+ it('should reject when --debate flag is missing', async () => {
112
+ await expect(runCli(['report']))
113
+ .rejects.toThrow();
114
+ });
115
+ });
116
+
117
+ describe('File existence validation', () => {
118
+ it('should exit with invalid args when debate file does not exist', async () => {
119
+ await expect(runCli(['report', '--debate', 'nonexistent.json']))
120
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
121
+ });
122
+
123
+ it('should exit with invalid args when debate path is a directory', async () => {
124
+ const dirPath = path.join(tmpDir, 'dir');
125
+ fs.mkdirSync(dirPath, { recursive: true });
126
+
127
+ await expect(runCli(['report', '--debate', dirPath]))
128
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
129
+ });
130
+ });
131
+
132
+ describe('JSON format validation', () => {
133
+ it('should exit with invalid args when debate file contains invalid JSON', async () => {
134
+ const debatePath = path.join(tmpDir, 'invalid.json');
135
+ fs.writeFileSync(debatePath, 'not valid json {');
136
+
137
+ await expect(runCli(['report', '--debate', debatePath]))
138
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
139
+ });
140
+
141
+ it('should exit with invalid args when debate JSON is missing required fields', async () => {
142
+ const debatePath = path.join(tmpDir, 'debate.json');
143
+ fs.writeFileSync(debatePath, JSON.stringify({ id: 'test' })); // Missing problem, status, rounds
144
+
145
+ await expect(runCli(['report', '--debate', debatePath]))
146
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
147
+ });
148
+
149
+ it('should exit with invalid args when debate JSON has invalid id field', async () => {
150
+ const debatePath = path.join(tmpDir, 'debate.json');
151
+ const invalidState = createMinimalDebateState();
152
+ invalidState.id = undefined as any;
153
+ fs.writeFileSync(debatePath, JSON.stringify(invalidState));
154
+
155
+ await expect(runCli(['report', '--debate', debatePath]))
156
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
157
+ });
158
+
159
+ it('should exit with invalid args when debate JSON has invalid problem field', async () => {
160
+ const debatePath = path.join(tmpDir, 'debate.json');
161
+ const invalidState = createMinimalDebateState();
162
+ invalidState.problem = undefined as any;
163
+ fs.writeFileSync(debatePath, JSON.stringify(invalidState));
164
+
165
+ await expect(runCli(['report', '--debate', debatePath]))
166
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
167
+ });
168
+
169
+ it('should exit with invalid args when debate JSON has invalid rounds field', async () => {
170
+ const debatePath = path.join(tmpDir, 'debate.json');
171
+ const invalidState = createMinimalDebateState();
172
+ invalidState.rounds = undefined as any;
173
+ fs.writeFileSync(debatePath, JSON.stringify(invalidState));
174
+
175
+ await expect(runCli(['report', '--debate', debatePath]))
176
+ .rejects.toHaveProperty('code', EXIT_INVALID_ARGS);
177
+ });
178
+ });
179
+
180
+ describe('Report generation', () => {
181
+ it('should generate report without config file when --config not provided', async () => {
182
+ const debatePath = path.join(tmpDir, 'debate.json');
183
+ const debateState = createMinimalDebateState();
184
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
185
+
186
+ // Ensure no config file exists
187
+ const defaultConfigPath = path.join(process.cwd(), 'debate-config.json');
188
+ const configExists = fs.existsSync(defaultConfigPath);
189
+ let configBackup: string | undefined;
190
+
191
+ if (configExists) {
192
+ configBackup = fs.readFileSync(defaultConfigPath, 'utf-8');
193
+ fs.unlinkSync(defaultConfigPath);
194
+ }
195
+
196
+ try {
197
+ await runCli(['report', '--debate', debatePath]);
198
+
199
+ // Should write to stdout
200
+ expect(stdoutSpy).toHaveBeenCalled();
201
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
202
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
203
+
204
+ // Should contain report content
205
+ expect(stdoutContent).toContain('# Debate:');
206
+ expect(stdoutContent).toContain('Test problem description');
207
+ expect(stdoutContent).toContain('## Problem Description');
208
+ expect(stdoutContent).toContain('## Agents');
209
+ expect(stdoutContent).toContain('## Judge');
210
+ expect(stdoutContent).toContain('## Rounds');
211
+
212
+ // Should contain minimal configs (agent ID as name, N/A for model/provider)
213
+ expect(stdoutContent).toContain('agent-architect');
214
+ expect(stdoutContent).toContain('N/A');
215
+ } finally {
216
+ // Restore config file if it existed
217
+ if (configExists && configBackup) {
218
+ fs.writeFileSync(defaultConfigPath, configBackup, 'utf-8');
219
+ }
220
+ }
221
+ });
222
+
223
+ it('should generate report with config file when --config provided', async () => {
224
+ const debatePath = path.join(tmpDir, 'debate.json');
225
+ const configPath = path.join(tmpDir, 'custom-config.json');
226
+ const debateState = createMinimalDebateState();
227
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
228
+
229
+ const config = createMinimalConfig();
230
+ fs.writeFileSync(configPath, JSON.stringify(config));
231
+
232
+ await runCli(['report', '--debate', debatePath, '--config', configPath]);
233
+
234
+ // Should write to stdout
235
+ expect(stdoutSpy).toHaveBeenCalled();
236
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
237
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
238
+
239
+ // Should contain report content
240
+ expect(stdoutContent).toContain('# Debate:');
241
+ expect(stdoutContent).toContain('Test problem description');
242
+ expect(stdoutContent).toContain('## Problem Description');
243
+ expect(stdoutContent).toContain('## Agents');
244
+ expect(stdoutContent).toContain('## Judge');
245
+ expect(stdoutContent).toContain('## Rounds');
246
+
247
+ // Should contain config values (not N/A)
248
+ expect(stdoutContent).toContain('System Architect');
249
+ expect(stdoutContent).toContain('gpt-4');
250
+ });
251
+
252
+ it('should generate report and write to stdout when no output path provided', async () => {
253
+ const debatePath = path.join(tmpDir, 'debate.json');
254
+ const debateState = createMinimalDebateState();
255
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
256
+
257
+ // Don't create config file - should work without it
258
+ await runCli(['report', '--debate', debatePath]);
259
+
260
+ // Should write to stdout
261
+ expect(stdoutSpy).toHaveBeenCalled();
262
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
263
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
264
+
265
+ // Should contain report content
266
+ expect(stdoutContent).toContain('# Debate:');
267
+ expect(stdoutContent).toContain('Test problem description');
268
+ expect(stdoutContent).toContain('## Problem Description');
269
+ expect(stdoutContent).toContain('## Agents');
270
+ expect(stdoutContent).toContain('## Judge');
271
+ expect(stdoutContent).toContain('## Rounds');
272
+ });
273
+
274
+ it('should generate report and write to file when output path provided', async () => {
275
+ const debatePath = path.join(tmpDir, 'debate.json');
276
+ const outputPath = path.join(tmpDir, 'report.md');
277
+ const debateState = createMinimalDebateState();
278
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
279
+
280
+ // Don't create config file - should work without it
281
+ await runCli(['report', '--debate', debatePath, '--output', outputPath]);
282
+
283
+ // Should create output file
284
+ expect(fs.existsSync(outputPath)).toBe(true);
285
+
286
+ // Should contain report content
287
+ const reportContent = fs.readFileSync(outputPath, 'utf-8');
288
+ expect(reportContent).toContain('# Debate:');
289
+ expect(reportContent).toContain('Test problem description');
290
+ expect(reportContent).toContain('## Problem Description');
291
+ expect(reportContent).toContain('## Agents');
292
+ expect(reportContent).toContain('## Judge');
293
+ expect(reportContent).toContain('## Rounds');
294
+ });
295
+
296
+ it('should create parent directories when output path is provided', async () => {
297
+ const debatePath = path.join(tmpDir, 'debate.json');
298
+ const outputPath = path.join(tmpDir, 'nested', 'dir', 'report.md');
299
+ const debateState = createMinimalDebateState();
300
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
301
+
302
+ // Don't create config file - should work without it
303
+ await runCli(['report', '--debate', debatePath, '--output', outputPath]);
304
+
305
+ // Should create nested directories and file
306
+ expect(fs.existsSync(outputPath)).toBe(true);
307
+
308
+ // Should contain report content
309
+ const reportContent = fs.readFileSync(outputPath, 'utf-8');
310
+ expect(reportContent).toContain('# Debate:');
311
+ });
312
+
313
+ it('should overwrite existing file when output path exists', async () => {
314
+ const debatePath = path.join(tmpDir, 'debate.json');
315
+ const outputPath = path.join(tmpDir, 'report.md');
316
+ const debateState = createMinimalDebateState();
317
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
318
+
319
+ // Create existing output file
320
+ fs.writeFileSync(outputPath, 'old content');
321
+
322
+ // Don't create config file - should work without it
323
+ await runCli(['report', '--debate', debatePath, '--output', outputPath]);
324
+
325
+ // Should overwrite with new content
326
+ const reportContent = fs.readFileSync(outputPath, 'utf-8');
327
+ expect(reportContent).not.toContain('old content');
328
+ expect(reportContent).toContain('# Debate:');
329
+ });
330
+
331
+ it('should use custom config file when --config provided', async () => {
332
+ const debatePath = path.join(tmpDir, 'debate.json');
333
+ const configPath = path.join(tmpDir, 'custom-config.json');
334
+ const debateState = createMinimalDebateState();
335
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
336
+
337
+ const config = createMinimalConfig();
338
+ fs.writeFileSync(configPath, JSON.stringify(config));
339
+
340
+ await runCli(['report', '--debate', debatePath, '--config', configPath]);
341
+
342
+ // Should succeed and write to stdout
343
+ expect(stdoutSpy).toHaveBeenCalled();
344
+ });
345
+
346
+ it('should include verbose metadata when --verbose flag is provided', async () => {
347
+ const debatePath = path.join(tmpDir, 'debate.json');
348
+ const debateState = createMinimalDebateState();
349
+ // Add metadata to contribution
350
+ if (debateState.rounds[0] && debateState.rounds[0].contributions[0]) {
351
+ debateState.rounds[0].contributions[0].metadata = {
352
+ latencyMs: 1234,
353
+ tokensUsed: 567,
354
+ model: 'gpt-4'
355
+ };
356
+ }
357
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
358
+
359
+ // Don't create config file - should work without it
360
+ await runCli(['report', '--debate', debatePath, '--verbose']);
361
+
362
+ // Should write to stdout
363
+ expect(stdoutSpy).toHaveBeenCalled();
364
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
365
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
366
+
367
+ // Should contain verbose metadata
368
+ expect(stdoutContent).toContain('latency=');
369
+ expect(stdoutContent).toContain('tokens=');
370
+ });
371
+
372
+ it('should match agent configs by ID from debate state when --config provided', async () => {
373
+ const debatePath = path.join(tmpDir, 'debate.json');
374
+ const configPath = path.join(tmpDir, 'custom-config.json');
375
+ const debateState = createMinimalDebateState();
376
+ // Add another agent to debate
377
+ if (debateState.rounds[0]) {
378
+ debateState.rounds[0].contributions.push({
379
+ agentId: 'agent-performance',
380
+ agentRole: 'performance',
381
+ type: 'proposal',
382
+ content: 'Performance proposal',
383
+ metadata: {}
384
+ });
385
+ }
386
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
387
+
388
+ // Create config with multiple agents
389
+ const config = createMinimalConfig();
390
+ config.agents.push({
391
+ id: 'agent-performance',
392
+ name: 'Performance Engineer',
393
+ role: 'performance',
394
+ model: 'gpt-4',
395
+ provider: 'openai',
396
+ temperature: 0.6
397
+ });
398
+ fs.writeFileSync(configPath, JSON.stringify(config));
399
+
400
+ await runCli(['report', '--debate', debatePath, '--config', configPath]);
401
+
402
+ // Should write to stdout
403
+ expect(stdoutSpy).toHaveBeenCalled();
404
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
405
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
406
+
407
+ // Should contain both agents from config
408
+ expect(stdoutContent).toContain('System Architect');
409
+ expect(stdoutContent).toContain('Performance Engineer');
410
+ });
411
+
412
+ it('should create minimal configs from debate state when --config not provided', async () => {
413
+ const debatePath = path.join(tmpDir, 'debate.json');
414
+ const debateState = createMinimalDebateState();
415
+ // Add another agent to debate
416
+ if (debateState.rounds[0]) {
417
+ debateState.rounds[0].contributions.push({
418
+ agentId: 'agent-performance',
419
+ agentRole: 'performance',
420
+ type: 'proposal',
421
+ content: 'Performance proposal',
422
+ metadata: {}
423
+ });
424
+ }
425
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
426
+
427
+ // Don't create config file - should work without it
428
+ await runCli(['report', '--debate', debatePath]);
429
+
430
+ // Should write to stdout
431
+ expect(stdoutSpy).toHaveBeenCalled();
432
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
433
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
434
+
435
+ // Should contain both agents (as IDs since no config)
436
+ expect(stdoutContent).toContain('agent-architect');
437
+ expect(stdoutContent).toContain('agent-performance');
438
+ // Should contain N/A values for minimal configs
439
+ expect(stdoutContent).toContain('N/A');
440
+ });
441
+
442
+ it('should handle clarifications in debate state', async () => {
443
+ const debatePath = path.join(tmpDir, 'debate.json');
444
+ const debateState = createMinimalDebateState();
445
+ debateState.clarifications = [
446
+ {
447
+ agentId: 'agent-architect',
448
+ agentName: 'System Architect',
449
+ role: 'architect',
450
+ items: [
451
+ {
452
+ id: 'q1',
453
+ question: 'What is the expected scale?',
454
+ answer: '1000 users'
455
+ }
456
+ ]
457
+ }
458
+ ];
459
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
460
+
461
+ // Don't create config file - should work without it
462
+ await runCli(['report', '--debate', debatePath]);
463
+
464
+ // Should write to stdout
465
+ expect(stdoutSpy).toHaveBeenCalled();
466
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
467
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
468
+
469
+ // Should contain clarifications section
470
+ expect(stdoutContent).toContain('## Clarifications');
471
+ expect(stdoutContent).toContain('What is the expected scale?');
472
+ expect(stdoutContent).toContain('1000 users');
473
+ });
474
+
475
+ it('should handle Date objects in debate state (revive from strings)', async () => {
476
+ const debatePath = path.join(tmpDir, 'debate.json');
477
+ const debateState = createMinimalDebateState();
478
+ // Serialize dates as strings (as they would be in JSON)
479
+ const serializedState = JSON.parse(JSON.stringify(debateState));
480
+ fs.writeFileSync(debatePath, JSON.stringify(serializedState));
481
+
482
+ // Don't create config file - should work without it
483
+ await runCli(['report', '--debate', debatePath]);
484
+
485
+ // Should succeed (dates are revived)
486
+ expect(stdoutSpy).toHaveBeenCalled();
487
+ });
488
+ });
489
+
490
+ describe('Error handling', () => {
491
+ it('should work when --config provided and config is missing judge (uses defaults)', async () => {
492
+ const debatePath = path.join(tmpDir, 'debate.json');
493
+ const configPath = path.join(tmpDir, 'config-no-judge.json');
494
+ const debateState = createMinimalDebateState();
495
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
496
+
497
+ // Create config without judge (loadConfig will add default judge)
498
+ const config = {
499
+ agents: createMinimalConfig().agents
500
+ // No judge - loadConfig will add default
501
+ };
502
+ fs.writeFileSync(configPath, JSON.stringify(config));
503
+
504
+ await runCli(['report', '--debate', debatePath, '--config', configPath]);
505
+
506
+ // Should succeed (loadConfig adds default judge)
507
+ expect(stdoutSpy).toHaveBeenCalled();
508
+ });
509
+
510
+ it('should work without config file when --config not provided', async () => {
511
+ const debatePath = path.join(tmpDir, 'debate.json');
512
+ const debateState = createMinimalDebateState();
513
+ fs.writeFileSync(debatePath, JSON.stringify(debateState));
514
+
515
+ // Ensure no config file exists
516
+ const defaultConfigPath = path.join(process.cwd(), 'debate-config.json');
517
+ const configExists = fs.existsSync(defaultConfigPath);
518
+ let configBackup: string | undefined;
519
+
520
+ if (configExists) {
521
+ configBackup = fs.readFileSync(defaultConfigPath, 'utf-8');
522
+ fs.unlinkSync(defaultConfigPath);
523
+ }
524
+
525
+ try {
526
+ await runCli(['report', '--debate', debatePath]);
527
+
528
+ // Should succeed without config (creates minimal configs from debate state)
529
+ expect(stdoutSpy).toHaveBeenCalled();
530
+ const stdoutCalls = (stdoutSpy as jest.Mock).mock.calls;
531
+ const stdoutContent = stdoutCalls.map((call: any[]) => call[0]).join('');
532
+
533
+ // Should contain report with minimal configs
534
+ expect(stdoutContent).toContain('# Debate:');
535
+ expect(stdoutContent).toContain('## Agents');
536
+ expect(stdoutContent).toContain('agent-architect');
537
+ } finally {
538
+ // Restore config file if it existed
539
+ if (configExists && configBackup) {
540
+ fs.writeFileSync(defaultConfigPath, configBackup, 'utf-8');
541
+ }
542
+ }
543
+ });
544
+ });
545
+ });
546
+