@thispointon/kondi-chat 0.1.2

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/bin/kondi-chat +56 -0
  4. package/bin/kondi-chat.js +72 -0
  5. package/package.json +55 -0
  6. package/scripts/demo.tape +49 -0
  7. package/scripts/postinstall.cjs +103 -0
  8. package/src/audit/analytics.ts +261 -0
  9. package/src/audit/ledger.ts +253 -0
  10. package/src/audit/telemetry.ts +165 -0
  11. package/src/cli/backend.ts +675 -0
  12. package/src/cli/commands.ts +419 -0
  13. package/src/cli/help.ts +182 -0
  14. package/src/cli/submit-helpers.ts +159 -0
  15. package/src/cli/submit.ts +539 -0
  16. package/src/cli/wizard.ts +121 -0
  17. package/src/context/bootstrap.ts +138 -0
  18. package/src/context/budget.ts +100 -0
  19. package/src/context/manager.ts +666 -0
  20. package/src/context/memory.ts +160 -0
  21. package/src/context/preflight.ts +176 -0
  22. package/src/context/project-brain.ts +101 -0
  23. package/src/context/receipts.ts +108 -0
  24. package/src/context/skills.ts +154 -0
  25. package/src/context/symbol-index.ts +240 -0
  26. package/src/council/profiles.ts +137 -0
  27. package/src/council/tool.ts +138 -0
  28. package/src/council-engine/cli/council-artifacts.ts +230 -0
  29. package/src/council-engine/cli/council-config.ts +178 -0
  30. package/src/council-engine/cli/council-session-export.ts +116 -0
  31. package/src/council-engine/cli/kondi.ts +98 -0
  32. package/src/council-engine/cli/llm-caller.ts +229 -0
  33. package/src/council-engine/cli/localStorage-shim.ts +119 -0
  34. package/src/council-engine/cli/node-platform.ts +68 -0
  35. package/src/council-engine/cli/run-council.ts +481 -0
  36. package/src/council-engine/cli/run-pipeline.ts +772 -0
  37. package/src/council-engine/cli/session-export.ts +153 -0
  38. package/src/council-engine/configs/councils/analysis.json +101 -0
  39. package/src/council-engine/configs/councils/code-planning.json +86 -0
  40. package/src/council-engine/configs/councils/coding.json +89 -0
  41. package/src/council-engine/configs/councils/debate.json +97 -0
  42. package/src/council-engine/configs/councils/solo-claude.json +34 -0
  43. package/src/council-engine/configs/councils/solo-gpt.json +34 -0
  44. package/src/council-engine/council/coding-orchestrator.ts +1205 -0
  45. package/src/council-engine/council/context-bootstrap.ts +147 -0
  46. package/src/council-engine/council/context-inspection.ts +42 -0
  47. package/src/council-engine/council/context-store.ts +763 -0
  48. package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
  49. package/src/council-engine/council/factory.ts +164 -0
  50. package/src/council-engine/council/index.ts +201 -0
  51. package/src/council-engine/council/ledger-store.ts +438 -0
  52. package/src/council-engine/council/prompts.ts +1689 -0
  53. package/src/council-engine/council/storage-cleanup.ts +164 -0
  54. package/src/council-engine/council/store.ts +1110 -0
  55. package/src/council-engine/council/synthesis.ts +291 -0
  56. package/src/council-engine/council/types.ts +845 -0
  57. package/src/council-engine/council/validation.ts +613 -0
  58. package/src/council-engine/pipeline/build-detect.ts +73 -0
  59. package/src/council-engine/pipeline/executor.ts +1048 -0
  60. package/src/council-engine/pipeline/index.ts +9 -0
  61. package/src/council-engine/pipeline/install-detect.ts +84 -0
  62. package/src/council-engine/pipeline/memory-store.ts +182 -0
  63. package/src/council-engine/pipeline/output-parsers.ts +146 -0
  64. package/src/council-engine/pipeline/run-output.ts +149 -0
  65. package/src/council-engine/pipeline/session-import.ts +177 -0
  66. package/src/council-engine/pipeline/store.ts +753 -0
  67. package/src/council-engine/pipeline/test-detect.ts +82 -0
  68. package/src/council-engine/pipeline/types.ts +401 -0
  69. package/src/council-engine/services/deliberationSummary.ts +114 -0
  70. package/src/council-engine/tsconfig.json +16 -0
  71. package/src/council-engine/types/mcp.ts +122 -0
  72. package/src/council-engine/utils/filterTools.ts +73 -0
  73. package/src/engine/apply.ts +238 -0
  74. package/src/engine/checkpoints.ts +237 -0
  75. package/src/engine/consultants.ts +347 -0
  76. package/src/engine/diff.ts +171 -0
  77. package/src/engine/errors.ts +102 -0
  78. package/src/engine/git-tools.ts +246 -0
  79. package/src/engine/hooks.ts +181 -0
  80. package/src/engine/loop-guard.ts +155 -0
  81. package/src/engine/permissions.ts +293 -0
  82. package/src/engine/pipeline.ts +376 -0
  83. package/src/engine/sub-agents.ts +133 -0
  84. package/src/engine/task-card.ts +185 -0
  85. package/src/engine/task-router.ts +256 -0
  86. package/src/engine/task-store.ts +86 -0
  87. package/src/engine/tools.ts +783 -0
  88. package/src/engine/verify.ts +111 -0
  89. package/src/mcp/client.ts +225 -0
  90. package/src/mcp/config.ts +120 -0
  91. package/src/mcp/tool-manager.ts +192 -0
  92. package/src/mcp/types.ts +61 -0
  93. package/src/providers/llm-caller.ts +943 -0
  94. package/src/providers/rate-limiter.ts +238 -0
  95. package/src/router/NOTES.md +28 -0
  96. package/src/router/collector.ts +474 -0
  97. package/src/router/embeddings.ts +286 -0
  98. package/src/router/index.ts +299 -0
  99. package/src/router/intent-router.ts +225 -0
  100. package/src/router/nn-router.ts +205 -0
  101. package/src/router/profiles.ts +309 -0
  102. package/src/router/registry.ts +565 -0
  103. package/src/router/rules.ts +274 -0
  104. package/src/router/train.py +408 -0
  105. package/src/session/store.ts +211 -0
  106. package/src/test-utils/mock-llm.ts +39 -0
  107. package/src/types.ts +322 -0
  108. package/src/web/manager.ts +311 -0
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env -S npx tsx
2
+ /**
3
+ * CLI Council Runner
4
+ *
5
+ * Usage:
6
+ * kondi council <council.json> [options]
7
+ * kondi council --task "Your task here" [options]
8
+ * kondi council --config <council-config.json> [options]
9
+ * kondi council [options] (auto-discovers council.json in cwd)
10
+ *
11
+ * Runs a standalone council deliberation. Supports all council types:
12
+ * council, coding, analysis, review, agent, enrich, code_planning.
13
+ *
14
+ * Examples:
15
+ * kondi council --task "Review this codebase for security issues" --working-dir ./myapp --type review
16
+ * kondi council --task "Build a REST API" --working-dir ./project --type coding
17
+ * kondi council --config ~/configs/security-review.json --working-dir ./target
18
+ * kondi council exported-council.json
19
+ * kondi council --task "Analyze logs" --output json --json-stdout --quiet
20
+ */
21
+
22
+ // ── localStorage shim MUST be imported first ──
23
+ import { storage } from './localStorage-shim';
24
+
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { councilStore } from '../council/store';
28
+ import { createCouncilFromSetup } from '../council/factory';
29
+ import { DeliberationOrchestrator } from '../council/deliberation-orchestrator';
30
+ import { CodingOrchestrator } from '../council/coding-orchestrator';
31
+ import { ledgerStore } from '../council/ledger-store';
32
+ import { buildAbbreviatedSummary } from '../services/deliberationSummary';
33
+ import { callLLM, DEFAULT_MODELS, type CallerResult } from './llm-caller';
34
+ import { loadCouncilConfig, mergeConfigWithArgs } from './council-config';
35
+ import { writeCouncilArtifacts, buildJsonResult } from './council-artifacts';
36
+ import { exportCouncilSession } from './council-session-export';
37
+ import type { CouncilCliArgs, OutputFormat } from './council-config';
38
+ import type { Council, Persona, CouncilStepType } from '../council/types';
39
+
40
+ // ── ANSI colors ──
41
+ const C = {
42
+ reset: '\x1b[0m',
43
+ bold: '\x1b[1m',
44
+ dim: '\x1b[2m',
45
+ green: '\x1b[32m',
46
+ yellow: '\x1b[33m',
47
+ red: '\x1b[31m',
48
+ cyan: '\x1b[36m',
49
+ magenta: '\x1b[35m',
50
+ blue: '\x1b[34m',
51
+ };
52
+
53
+ let quietMode = false;
54
+
55
+ function log(color: string, prefix: string, msg: string) {
56
+ if (quietMode) return;
57
+ const ts = new Date().toLocaleTimeString();
58
+ console.log(`${C.dim}${ts}${C.reset} ${color}${C.bold}[${prefix}]${C.reset} ${msg}`);
59
+ }
60
+
61
+ // ── Parse CLI args ──
62
+ function parseArgs(): CouncilCliArgs {
63
+ const args = process.argv.slice(2);
64
+ const parsed: CouncilCliArgs = {};
65
+
66
+ for (let i = 0; i < args.length; i++) {
67
+ const arg = args[i];
68
+ const next = args[i + 1];
69
+
70
+ switch (arg) {
71
+ case '--working-dir':
72
+ parsed.workingDir = next ? path.resolve(args[++i]) : undefined;
73
+ break;
74
+ case '--type':
75
+ parsed.type = next as CouncilStepType;
76
+ i++;
77
+ break;
78
+ case '--task':
79
+ parsed.task = args[++i];
80
+ break;
81
+ case '--model':
82
+ parsed.model = args[++i];
83
+ break;
84
+ case '--provider':
85
+ parsed.provider = args[++i];
86
+ break;
87
+ case '--config':
88
+ parsed.configPath = args[++i];
89
+ break;
90
+ case '--output':
91
+ parsed.outputFormat = next as OutputFormat;
92
+ i++;
93
+ break;
94
+ case '--output-dir':
95
+ parsed.outputDir = path.resolve(args[++i]);
96
+ break;
97
+ case '--no-session-export':
98
+ parsed.noSessionExport = true;
99
+ break;
100
+ case '--dry-run':
101
+ parsed.dryRun = true;
102
+ break;
103
+ case '--quiet':
104
+ parsed.quiet = true;
105
+ break;
106
+ case '--json-stdout':
107
+ parsed.jsonStdout = true;
108
+ break;
109
+ case '--help':
110
+ case '-h':
111
+ printHelp();
112
+ process.exit(0);
113
+ break;
114
+ default:
115
+ if (!arg.startsWith('--') && !parsed.councilJsonPath) {
116
+ parsed.councilJsonPath = path.resolve(arg);
117
+ }
118
+ break;
119
+ }
120
+ }
121
+
122
+ return parsed;
123
+ }
124
+
125
+ function printHelp() {
126
+ console.log(`
127
+ ${C.bold}Kondi Council Runner${C.reset}
128
+
129
+ Usage:
130
+ kondi council <council.json> [options]
131
+ kondi council --task "Your task" [options]
132
+ kondi council --config <config.json> [options]
133
+ kondi council [options] (auto-discovers council.json)
134
+
135
+ Options:
136
+ --config <path> Path to council config file (JSON)
137
+ --task "..." Task/problem for the council to work on
138
+ --type <type> Council type: council, coding, review, analysis, agent (default: council)
139
+ --working-dir <path> Working directory for file operations
140
+ --model <model> Override model for all personas
141
+ --provider <provider> Override provider for all personas
142
+ --output <format> Output format: full, abbreviated, output-only, json, none (default: full)
143
+ --output-dir <path> Override artifact output directory
144
+ --no-session-export Skip session export to ~/.local/share/kondi/sessions/
145
+ --dry-run Print council structure without running
146
+ --quiet Suppress progress output (for automation)
147
+ --json-stdout Print structured JSON result to stdout
148
+ --help Show this help
149
+
150
+ Config auto-discovery:
151
+ If no --config, --task, or council.json is provided, searches for:
152
+ 1. ./council.json (current directory)
153
+ 2. ~/.config/kondi/council.json
154
+
155
+ Output formats:
156
+ full deliberation.md + decision.md + output.md
157
+ abbreviated summary.md only
158
+ output-only output.md only (worker's final deliverable)
159
+ json council-result.json (structured data)
160
+ none no file output
161
+ `);
162
+ }
163
+
164
+ // ── Print council structure (for --dry-run) ──
165
+ function printCouncilStructure(council: Council, councilType: string, task: string, workingDir?: string) {
166
+ console.log(`\n${C.bold}${C.cyan}Council: ${council.name}${C.reset}`);
167
+ console.log(`${C.dim}Type: ${councilType}${C.reset}`);
168
+ console.log(`${C.dim}Task: ${task.slice(0, 300)}${task.length > 300 ? '...' : ''}${C.reset}`);
169
+ if (workingDir) console.log(`${C.dim}Working dir: ${workingDir}${C.reset}`);
170
+
171
+ console.log(`\n${C.bold}Personas:${C.reset}`);
172
+ for (const p of council.personas) {
173
+ const role = p.preferredDeliberationRole || 'unassigned';
174
+ const roleColor = role === 'manager' ? C.blue :
175
+ role === 'worker' ? C.yellow :
176
+ role === 'reviewer' ? C.cyan : C.green;
177
+ console.log(` ${roleColor}${p.avatar || ''} ${p.name}${C.reset} (${role}) — ${p.model} [${p.provider}]`);
178
+ if (p.predisposition?.domain) console.log(` ${C.dim}Domain: ${p.predisposition.domain}${C.reset}`);
179
+ if (p.predisposition?.traits?.length) console.log(` ${C.dim}Traits: ${p.predisposition.traits.join(', ')}${C.reset}`);
180
+ }
181
+
182
+ if (council.deliberation) {
183
+ console.log(`\n${C.bold}Orchestration:${C.reset}`);
184
+ console.log(` ${C.dim}Max rounds: ${council.deliberation.maxRounds}${C.reset}`);
185
+ console.log(` ${C.dim}Max revisions: ${council.deliberation.maxRevisions}${C.reset}`);
186
+ console.log(` ${C.dim}Context budget: ${council.deliberation.contextTokenBudget} tokens${C.reset}`);
187
+ }
188
+ console.log();
189
+ }
190
+
191
+ // ── Main ──
192
+ async function main() {
193
+ const args = parseArgs();
194
+ quietMode = args.quiet || false;
195
+
196
+ // ── Resolve council source ──
197
+ let council: Council;
198
+ let councilType: CouncilStepType = args.type || 'council';
199
+ let task: string;
200
+ let workingDir = args.workingDir;
201
+ let outputFormat: OutputFormat = args.outputFormat || 'full';
202
+ let outputDir = args.outputDir;
203
+ let sessionExport = !args.noSessionExport;
204
+
205
+ if (args.councilJsonPath) {
206
+ // Path 1: Explicit council JSON export
207
+ if (!fs.existsSync(args.councilJsonPath)) {
208
+ console.error(`File not found: ${args.councilJsonPath}`);
209
+ process.exit(1);
210
+ }
211
+ let raw: any;
212
+ try {
213
+ raw = JSON.parse(fs.readFileSync(args.councilJsonPath, 'utf-8'));
214
+ } catch (e) {
215
+ console.error(`Invalid JSON in council file: ${args.councilJsonPath}`);
216
+ process.exit(1);
217
+ }
218
+
219
+ if (raw.personas && raw.deliberation) {
220
+ council = raw as Council;
221
+ councilStore.create({
222
+ name: council.name,
223
+ topic: council.topic || council.name,
224
+ personas: council.personas,
225
+ orchestration: council.orchestration,
226
+ deliberation: council.deliberation,
227
+ });
228
+ council = councilStore.getAll().at(-1)!;
229
+ } else {
230
+ console.error('Invalid council JSON format — expected personas and deliberation fields');
231
+ process.exit(1);
232
+ }
233
+ task = args.task || council.deliberation?.savedProblem || council.topic;
234
+
235
+ } else {
236
+ // Path 2: Config file (explicit or auto-discovered)
237
+ const configFile = loadCouncilConfig(args.configPath);
238
+
239
+ if (configFile) {
240
+ const resolved = mergeConfigWithArgs(configFile, args);
241
+ councilType = resolved.type;
242
+ task = resolved.task;
243
+ outputFormat = resolved.outputFormat;
244
+ outputDir = resolved.outputDir;
245
+ sessionExport = resolved.sessionExport;
246
+
247
+ const defaultProvider = resolved.provider || 'anthropic-api';
248
+
249
+ council = createCouncilFromSetup({
250
+ name: configFile.name,
251
+ topic: task,
252
+ personas: configFile.personas.map(p => {
253
+ const prov = p.provider || defaultProvider;
254
+ return {
255
+ name: p.name,
256
+ role: p.role,
257
+ provider: prov,
258
+ model: p.model || resolved.model || DEFAULT_MODELS[prov] || '',
259
+ avatar: p.avatar,
260
+ systemPrompt: p.systemPrompt || `You are ${p.name}.`,
261
+ traits: p.traits || [],
262
+ stance: p.stance,
263
+ domain: p.domain,
264
+ temperature: p.temperature,
265
+ suppressPersona: p.suppressPersona,
266
+ toolAccess: p.toolAccess,
267
+ }}),
268
+ workingDirectory: workingDir,
269
+ stepType: councilType,
270
+ contextTokenBudget: configFile.orchestration?.contextTokenBudget || 80000,
271
+ summarizeAfterRound: configFile.orchestration?.summarizeAfterRound || 2,
272
+ maxRounds: configFile.orchestration?.maxRounds,
273
+ maxRevisions: configFile.orchestration?.maxRevisions,
274
+ consultantExecution: configFile.orchestration?.consultantExecution,
275
+ evolveContext: configFile.orchestration?.evolveContext,
276
+ bootstrapContext: configFile.orchestration?.bootstrapContext,
277
+ expectedOutput: configFile.expectedOutput,
278
+ decisionCriteria: configFile.decisionCriteria,
279
+ testCommand: configFile.testCommand,
280
+ maxDebugCycles: configFile.maxDebugCycles,
281
+ maxReviewCycles: configFile.maxReviewCycles,
282
+ });
283
+
284
+ } else if (args.task) {
285
+ // Path 3: Inline task with default personas
286
+ task = args.task;
287
+ const defaultProvider = args.provider || 'anthropic-api';
288
+ const defaultModel = args.model || 'claude-sonnet-4-5-20250929';
289
+
290
+ council = createCouncilFromSetup({
291
+ name: task.slice(0, 60),
292
+ topic: task,
293
+ personas: [
294
+ { name: 'Manager', role: 'manager', provider: defaultProvider, model: defaultModel, avatar: '👔', systemPrompt: 'You are the manager.', traits: ['analytical'], suppressPersona: true },
295
+ { name: 'Worker', role: 'worker', provider: defaultProvider, model: defaultModel, avatar: '🔧', systemPrompt: 'You are the worker.', traits: ['precise'], temperature: 0.5, suppressPersona: true },
296
+ { name: 'Consultant', role: 'consultant', provider: defaultProvider, model: defaultModel, avatar: '🌟', systemPrompt: 'You are a consultant.', traits: ['creative'], stance: 'advocate' },
297
+ ],
298
+ workingDirectory: workingDir,
299
+ stepType: councilType,
300
+ contextTokenBudget: 80000,
301
+ summarizeAfterRound: 2,
302
+ });
303
+
304
+ } else {
305
+ console.error('No council source provided. Use --task, --config, or pass a council.json file.');
306
+ console.error('Run with --help for usage information.');
307
+ process.exit(1);
308
+ }
309
+ }
310
+
311
+ // Apply overrides
312
+ if (args.model) {
313
+ council.personas = council.personas.map(p => ({ ...p, model: args.model! }));
314
+ }
315
+ if (args.provider) {
316
+ council.personas = council.personas.map(p => ({ ...p, provider: args.provider! }));
317
+ }
318
+ if (workingDir && council.deliberation) {
319
+ council.deliberation.workingDirectory = workingDir;
320
+ }
321
+
322
+ const rawProblem = task!;
323
+ const effectiveWorkingDir = workingDir || process.cwd();
324
+
325
+ // ── Dry run ──
326
+ if (args.dryRun) {
327
+ printCouncilStructure(council, councilType, rawProblem, workingDir);
328
+ console.log(`${C.dim}Output format: ${outputFormat}${C.reset}`);
329
+ console.log(`${C.dim}Session export: ${sessionExport}${C.reset}`);
330
+ console.log(`${C.dim}--dry-run: exiting without executing.${C.reset}`);
331
+ process.exit(0);
332
+ }
333
+
334
+ // ── Log startup ──
335
+ log(C.green, 'Council', `"${council.name}" — ${councilType} mode`);
336
+ log(C.green, 'Council', `Personas: ${council.personas.map(p => p.name).join(', ')}`);
337
+ if (workingDir) log(C.green, 'Council', `Working dir: ${workingDir}`);
338
+ log(C.green, 'Council', `Task: ${rawProblem.slice(0, 200)}${rawProblem.length > 200 ? '...' : ''}`);
339
+ log(C.green, 'Council', `Output: ${outputFormat}`);
340
+ if (!quietMode) console.log('');
341
+
342
+ // Council calls are stateless one-shot — no session resumption.
343
+ // Each persona call gets a fresh context. See CLAUDE.md rule #2.
344
+ const startTime = Date.now();
345
+
346
+ const invokeAgent = async (invocation: any, persona: Persona) => {
347
+ log(C.cyan, persona.name, `Thinking... (${persona.model})`);
348
+
349
+ const result = await callLLM({
350
+ provider: persona.provider || 'anthropic-api',
351
+ systemPrompt: invocation.systemPrompt,
352
+ userMessage: invocation.userMessage,
353
+ model: persona.model,
354
+ workingDir: invocation.workingDirectory || workingDir,
355
+ skipTools: invocation.skipTools,
356
+ allowedTools: invocation.allowedTools,
357
+ timeoutMs: invocation.timeoutMs || 900_000,
358
+ });
359
+
360
+ log(C.cyan, persona.name, `Done (${result.tokensUsed} tokens, ${(result.latencyMs / 1000).toFixed(1)}s)`);
361
+ return { ...result, sessionId: result.sessionId };
362
+ };
363
+
364
+ const callbacks = {
365
+ invokeAgent,
366
+ onPhaseChange: (from: string, to: string) => log(C.yellow, 'Phase', `${from} → ${to}`),
367
+ onError: (err: Error, ctx: string) => log(C.red, 'Error', `${ctx}: ${err.message}`),
368
+ onAgentThinkingStart: (_persona: Persona) => {},
369
+ onAgentThinkingEnd: (_persona: Persona) => {},
370
+ };
371
+
372
+ // ── Execute ──
373
+ let status: 'completed' | 'failed' = 'completed';
374
+ let errorMsg: string | undefined;
375
+
376
+ try {
377
+ if (councilType === 'coding') {
378
+ const orchestrator = new CodingOrchestrator({
379
+ ...callbacks,
380
+ runCommand: async (cmd: string, cwd?: string) => {
381
+ const { execFileSync } = await import('node:child_process');
382
+ try {
383
+ const stdout = execFileSync('/bin/sh', ['-c', cmd], { cwd: cwd || workingDir, encoding: 'utf-8', timeout: 120_000 });
384
+ return { stdout, stderr: '', exitCode: 0 };
385
+ } catch (err: any) {
386
+ return { stdout: err.stdout || '', stderr: err.stderr || err.message, exitCode: err.status || 1 };
387
+ }
388
+ },
389
+ readFile: async (filePath: string) => {
390
+ const resolved = path.resolve(filePath);
391
+ const base = path.resolve(effectiveWorkingDir);
392
+ if (!resolved.startsWith(base + path.sep) && resolved !== base) {
393
+ throw new Error(`Path traversal blocked: ${filePath} escapes working directory`);
394
+ }
395
+ return fs.readFileSync(filePath, 'utf-8');
396
+ },
397
+ });
398
+ await orchestrator.runCodingWorkflow(council, rawProblem);
399
+ } else {
400
+ const deliberator = new DeliberationOrchestrator(callbacks);
401
+ await deliberator.runFullDeliberation(council, rawProblem);
402
+ }
403
+
404
+ log(C.green, 'Done', 'Council completed');
405
+ } catch (error) {
406
+ status = 'failed';
407
+ errorMsg = error instanceof Error ? error.message : String(error);
408
+ log(C.red, 'Fatal', errorMsg);
409
+ }
410
+
411
+ const durationMs = Date.now() - startTime;
412
+
413
+ // ── Print summary ──
414
+ if (!quietMode) {
415
+ const completed = councilStore.get(council.id);
416
+ if (completed) {
417
+ const summary = buildAbbreviatedSummary(completed);
418
+ console.log('\n' + C.bold + '═══ Summary ═══' + C.reset);
419
+ console.log(summary);
420
+ }
421
+
422
+ const entries = ledgerStore.getAll(council.id);
423
+ const totalTokens = entries.reduce((s, e) => s + (e.tokensUsed || 0), 0);
424
+ console.log(`\n${C.dim}Entries: ${entries.length} | Tokens: ${totalTokens.toLocaleString()} | Time: ${(durationMs / 1000).toFixed(0)}s${C.reset}`);
425
+ }
426
+
427
+ // ── Write artifacts ──
428
+ let artifactPaths: string[] = [];
429
+ try {
430
+ artifactPaths = writeCouncilArtifacts(council, {
431
+ format: outputFormat,
432
+ outputDir,
433
+ workingDir: effectiveWorkingDir,
434
+ });
435
+ if (artifactPaths.length > 0 && !quietMode) {
436
+ log(C.cyan, 'Artifacts', `Written ${artifactPaths.length} file(s):`);
437
+ for (const p of artifactPaths) {
438
+ console.log(` ${C.dim}${p}${C.reset}`);
439
+ }
440
+ }
441
+ } catch (err) {
442
+ console.error(`${C.red}Failed to write artifacts:${C.reset}`, err);
443
+ }
444
+
445
+ // ── Session export ──
446
+ if (sessionExport) {
447
+ try {
448
+ const sessionPath = exportCouncilSession(council.id, storage, {
449
+ status,
450
+ startedAt: new Date(startTime).toISOString(),
451
+ completedAt: new Date().toISOString(),
452
+ durationMs,
453
+ workingDirectory: effectiveWorkingDir,
454
+ });
455
+ if (sessionPath && !quietMode) {
456
+ log(C.cyan, 'Session', `Exported to: ${sessionPath}`);
457
+ }
458
+ } catch (err) {
459
+ console.error(`${C.red}Failed to export session:${C.reset}`, err);
460
+ }
461
+ }
462
+
463
+ // ── JSON stdout ──
464
+ if (args.jsonStdout) {
465
+ const jsonResult = buildJsonResult(council, artifactPaths, {
466
+ status,
467
+ durationMs,
468
+ error: errorMsg,
469
+ });
470
+ console.log(JSON.stringify(jsonResult));
471
+ }
472
+
473
+ if (status === 'failed') {
474
+ process.exit(1);
475
+ }
476
+ }
477
+
478
+ main().catch((err) => {
479
+ console.error(`${C.red}Unhandled error:${C.reset}`, err);
480
+ process.exit(1);
481
+ });