@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,1110 @@
1
+ /**
2
+ * Council: Persistence Store
3
+ * CRUD operations for councils with localStorage persistence
4
+ */
5
+
6
+ import type {
7
+ Council,
8
+ Persona,
9
+ CouncilMessage,
10
+ OrchestrationConfig,
11
+ SharedContext,
12
+ Resolution,
13
+ DeliberationConfig,
14
+ DeliberationState,
15
+ DeliberationRoleAssignment,
16
+ DeliberationPhase,
17
+ DeliberationRole,
18
+ } from './types';
19
+ import { validateCouncil } from './validation';
20
+ import { deleteLedger } from './ledger-store';
21
+ import { deleteAllArtifacts } from './context-store';
22
+ import { councilDataStore } from './storage-cleanup';
23
+
24
+ const STORAGE_KEY = 'mcp-councils';
25
+ const STORAGE_VERSION = 2;
26
+
27
+ interface StorageData {
28
+ version: number;
29
+ councils: Council[];
30
+ lastUpdated: string;
31
+ }
32
+
33
+ // ============================================================================
34
+ // Storage Helpers
35
+ // ============================================================================
36
+
37
+ function loadFromStorage(): StorageData {
38
+ try {
39
+ const raw = councilDataStore.getItem(STORAGE_KEY);
40
+ if (!raw) {
41
+ return { version: STORAGE_VERSION, councils: [], lastUpdated: new Date().toISOString() };
42
+ }
43
+ const data = JSON.parse(raw) as StorageData;
44
+ // Handle version migrations
45
+ if (data.version < STORAGE_VERSION) {
46
+ console.log('[CouncilStore] Migrating from version', data.version, 'to', STORAGE_VERSION);
47
+ data.councils = migrateCouncils(data.councils, data.version);
48
+ data.version = STORAGE_VERSION;
49
+ saveToStorage(data);
50
+ }
51
+ return data;
52
+ } catch (error) {
53
+ console.error('[CouncilStore] Failed to load from storage:', error);
54
+ return { version: STORAGE_VERSION, councils: [], lastUpdated: new Date().toISOString() };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Migrate councils from older versions
60
+ */
61
+ function migrateCouncils(councils: Council[], fromVersion: number): Council[] {
62
+ let migrated = councils;
63
+
64
+ // Migration from v1 to v2: Add deliberation fields
65
+ if (fromVersion < 2) {
66
+ migrated = migrated.map((council) => ({
67
+ ...council,
68
+ // Existing councils get undefined deliberation (backward compatible)
69
+ deliberation: undefined,
70
+ deliberationState: undefined,
71
+ }));
72
+ console.log('[CouncilStore] Migrated', migrated.length, 'councils from v1 to v2');
73
+ }
74
+
75
+ // Always sanitize status — old duplicates may have invalid values like 'created'
76
+ migrated = migrated.map((council) => ({
77
+ ...council,
78
+ status: ['active', 'paused', 'resolved'].includes(council.status) ? council.status : 'active',
79
+ }));
80
+
81
+ return migrated;
82
+ }
83
+
84
+ function saveToStorage(data: StorageData): void {
85
+ data.lastUpdated = new Date().toISOString();
86
+ const json = JSON.stringify(data);
87
+ councilDataStore.setItem(STORAGE_KEY, json);
88
+ console.log('[CouncilStore] Saved', data.councils.length, 'councils');
89
+ }
90
+
91
+ // ============================================================================
92
+ // Council CRUD
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Get all councils
97
+ */
98
+ export function getAllCouncils(): Council[] {
99
+ const data = loadFromStorage();
100
+ return data.councils.sort(
101
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Get a council by ID
107
+ */
108
+ export function getCouncil(id: string): Council | null {
109
+ const data = loadFromStorage();
110
+ return data.councils.find((c) => c.id === id) || null;
111
+ }
112
+
113
+ /**
114
+ * Create a new council
115
+ */
116
+ export function createCouncil(params: {
117
+ name: string;
118
+ topic: string;
119
+ sharedContext?: Partial<SharedContext>;
120
+ personas?: Persona[];
121
+ orchestration?: Partial<OrchestrationConfig>;
122
+ deliberation?: Partial<DeliberationConfig>;
123
+ pipelineId?: string;
124
+ }): Council {
125
+ const now = new Date().toISOString();
126
+ const isDeliberationMode = params.orchestration?.mode === 'deliberation';
127
+
128
+ const council: Council = {
129
+ id: crypto.randomUUID(),
130
+ name: params.name,
131
+ createdAt: now,
132
+ updatedAt: now,
133
+ topic: params.topic,
134
+ sharedContext: {
135
+ description: params.sharedContext?.description || params.topic,
136
+ documents: params.sharedContext?.documents || [],
137
+ data: params.sharedContext?.data,
138
+ constraints: params.sharedContext?.constraints,
139
+ },
140
+ personas: params.personas || [],
141
+ orchestration: {
142
+ mode: params.orchestration?.mode || 'debate',
143
+ turnStrategy: params.orchestration?.turnStrategy || 'round-robin',
144
+ maxTurnsPerRound: params.orchestration?.maxTurnsPerRound || 5,
145
+ maxTotalTurns: params.orchestration?.maxTotalTurns,
146
+ autoSynthesize: params.orchestration?.autoSynthesize ?? true,
147
+ synthesizerId: params.orchestration?.synthesizerId,
148
+ convergenceCriteria: params.orchestration?.convergenceCriteria,
149
+ requiresResolution: params.orchestration?.requiresResolution ?? false,
150
+ },
151
+ messages: [],
152
+ status: 'active',
153
+ totalTokensUsed: 0,
154
+ estimatedCost: 0,
155
+ // Deliberation config (only for deliberation mode)
156
+ deliberation: isDeliberationMode ? {
157
+ enabled: true,
158
+ roleAssignments: params.deliberation?.roleAssignments || [],
159
+ minRounds: params.deliberation?.minRounds ?? 1,
160
+ maxRounds: params.deliberation?.maxRounds ?? 4,
161
+ maxRevisions: params.deliberation?.maxRevisions ?? 3,
162
+ expectedOutput: params.deliberation?.expectedOutput,
163
+ decisionCriteria: params.deliberation?.decisionCriteria,
164
+ summaryMode: params.deliberation?.summaryMode ?? 'hybrid',
165
+ summarizeAfterRound: params.deliberation?.summarizeAfterRound ?? 1,
166
+ contextTokenBudget: params.deliberation?.contextTokenBudget ?? 40000,
167
+ consultantErrorPolicy: params.deliberation?.consultantErrorPolicy ?? 'retry',
168
+ maxRetries: params.deliberation?.maxRetries ?? 2,
169
+ requirePlan: params.deliberation?.requirePlan ?? false,
170
+ consultantExecution: params.deliberation?.consultantExecution ?? 'sequential',
171
+ workingDirectory: params.deliberation?.workingDirectory,
172
+ directoryConstrained: params.deliberation?.directoryConstrained ?? true,
173
+ saveDeliberation: params.deliberation?.saveDeliberation ?? false,
174
+ saveDeliberationMode: params.deliberation?.saveDeliberationMode ?? 'full',
175
+ maxWordsPerResponse: params.deliberation?.maxWordsPerResponse,
176
+ bootstrapContext: params.deliberation?.bootstrapContext,
177
+ stepType: params.deliberation?.stepType,
178
+ testCommand: params.deliberation?.testCommand,
179
+ maxDebugCycles: params.deliberation?.maxDebugCycles,
180
+ maxReviewCycles: params.deliberation?.maxReviewCycles,
181
+ allowedServerIds: params.deliberation?.allowedServerIds,
182
+ } : undefined,
183
+ // Deliberation state (initialized when deliberation starts)
184
+ deliberationState: undefined,
185
+ // Pipeline linkage
186
+ pipelineId: params.pipelineId,
187
+ };
188
+
189
+ // Validate before saving
190
+ const validation = validateCouncil(council);
191
+ if (!validation.success) {
192
+ const issues = validation.error.issues
193
+ .map((issue) => {
194
+ const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
195
+ return `${path}: ${issue.message}`;
196
+ })
197
+ .join('; ');
198
+ console.error('[CouncilStore] Invalid council on create:', issues, validation.error);
199
+ throw new Error(`Invalid council data: ${issues}`);
200
+ }
201
+
202
+ const data = loadFromStorage();
203
+ data.councils.push(council);
204
+ saveToStorage(data);
205
+
206
+ console.log('[CouncilStore] Created council:', council.id, council.name, isDeliberationMode ? '(deliberation mode)' : '');
207
+ return council;
208
+ }
209
+
210
+ /**
211
+ * Update a council
212
+ */
213
+ export function updateCouncil(
214
+ id: string,
215
+ updates: Partial<Omit<Council, 'id' | 'createdAt'>>
216
+ ): Council | null {
217
+ const data = loadFromStorage();
218
+ const index = data.councils.findIndex((c) => c.id === id);
219
+
220
+ if (index === -1) {
221
+ console.warn('[CouncilStore] Council not found:', id);
222
+ return null;
223
+ }
224
+
225
+ const council = data.councils[index];
226
+ const updated: Council = {
227
+ ...council,
228
+ ...updates,
229
+ updatedAt: new Date().toISOString(),
230
+ };
231
+
232
+ // Coerce invalid status values to 'active' (e.g. 'created' from old duplicates)
233
+ if (!['active', 'paused', 'resolved'].includes(updated.status)) {
234
+ updated.status = 'active';
235
+ }
236
+
237
+ // Validate the updated council
238
+ const validation = validateCouncil(updated);
239
+ if (!validation.success) {
240
+ const issues = validation.error.issues
241
+ .map((issue) => {
242
+ const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
243
+ return `${path}: ${issue.message}`;
244
+ })
245
+ .join('; ');
246
+ console.error('[CouncilStore] Invalid council update:', issues, validation.error);
247
+ throw new Error(`Invalid council data: ${issues}`);
248
+ }
249
+
250
+ data.councils[index] = updated;
251
+ saveToStorage(data);
252
+
253
+ console.log('[CouncilStore] Updated council:', id);
254
+ return updated;
255
+ }
256
+
257
+ /**
258
+ * Delete a council
259
+ */
260
+ export function deleteCouncil(id: string): boolean {
261
+ const data = loadFromStorage();
262
+ const index = data.councils.findIndex((c) => c.id === id);
263
+
264
+ if (index === -1) {
265
+ console.warn('[CouncilStore] Council not found:', id);
266
+ return false;
267
+ }
268
+
269
+ data.councils.splice(index, 1);
270
+ saveToStorage(data);
271
+
272
+ console.log('[CouncilStore] Deleted council:', id);
273
+ return true;
274
+ }
275
+
276
+ // ============================================================================
277
+ // Persona Operations
278
+ // ============================================================================
279
+
280
+ /**
281
+ * Add a persona to a council
282
+ */
283
+ export function addPersona(councilId: string, persona: Persona): Council | null {
284
+ const council = getCouncil(councilId);
285
+ if (!council) return null;
286
+
287
+ // Check for duplicate names
288
+ if (council.personas.some((p) => p.name === persona.name)) {
289
+ throw new Error(`Persona "${persona.name}" already exists in this council`);
290
+ }
291
+
292
+ // Also add a role assignment if deliberation config exists
293
+ const updates: Partial<Council> = {
294
+ personas: [...council.personas, persona],
295
+ };
296
+
297
+ if (council.deliberation) {
298
+ const existingAssignment = council.deliberation.roleAssignments.find(
299
+ (r) => r.personaId === persona.id
300
+ );
301
+ if (!existingAssignment) {
302
+ // Default new personas to 'consultant' role
303
+ const role = persona.preferredDeliberationRole || 'consultant';
304
+ updates.deliberation = {
305
+ ...council.deliberation,
306
+ roleAssignments: [
307
+ ...council.deliberation.roleAssignments,
308
+ { personaId: persona.id, role },
309
+ ],
310
+ };
311
+ }
312
+ }
313
+
314
+ return updateCouncil(councilId, updates);
315
+ }
316
+
317
+ /**
318
+ * Update a persona in a council
319
+ */
320
+ export function updatePersona(
321
+ councilId: string,
322
+ personaId: string,
323
+ updates: Partial<Omit<Persona, 'id'>>
324
+ ): Council | null {
325
+ const council = getCouncil(councilId);
326
+ if (!council) return null;
327
+
328
+ const personaIndex = council.personas.findIndex((p) => p.id === personaId);
329
+ if (personaIndex === -1) {
330
+ throw new Error(`Persona not found: ${personaId}`);
331
+ }
332
+
333
+ const updatedPersonas = [...council.personas];
334
+ updatedPersonas[personaIndex] = {
335
+ ...updatedPersonas[personaIndex],
336
+ ...updates,
337
+ };
338
+
339
+ return updateCouncil(councilId, { personas: updatedPersonas });
340
+ }
341
+
342
+ /**
343
+ * Remove a persona from a council
344
+ */
345
+ export function removePersona(councilId: string, personaId: string): Council | null {
346
+ const council = getCouncil(councilId);
347
+ if (!council) return null;
348
+
349
+ const updates: Partial<Council> = {
350
+ personas: council.personas.filter((p) => p.id !== personaId),
351
+ };
352
+
353
+ // Also remove the role assignment if deliberation config exists
354
+ if (council.deliberation) {
355
+ updates.deliberation = {
356
+ ...council.deliberation,
357
+ roleAssignments: council.deliberation.roleAssignments.filter(
358
+ (r) => r.personaId !== personaId
359
+ ),
360
+ };
361
+ }
362
+
363
+ return updateCouncil(councilId, updates);
364
+ }
365
+
366
+ /**
367
+ * Mute/unmute a persona
368
+ */
369
+ export function setPersonaMuted(
370
+ councilId: string,
371
+ personaId: string,
372
+ muted: boolean
373
+ ): Council | null {
374
+ return updatePersona(councilId, personaId, { muted });
375
+ }
376
+
377
+ // ============================================================================
378
+ // Message Operations
379
+ // ============================================================================
380
+
381
+ /**
382
+ * Add a message to a council
383
+ */
384
+ export function addMessage(councilId: string, message: CouncilMessage): Council | null {
385
+ const council = getCouncil(councilId);
386
+ if (!council) return null;
387
+
388
+ return updateCouncil(councilId, {
389
+ messages: [...council.messages, message],
390
+ totalTokensUsed: council.totalTokensUsed + message.tokensUsed,
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Get messages for a council, optionally filtered
396
+ */
397
+ export function getMessages(
398
+ councilId: string,
399
+ options?: {
400
+ speakerId?: string;
401
+ speakerType?: 'persona' | 'user' | 'system';
402
+ limit?: number;
403
+ offset?: number;
404
+ }
405
+ ): CouncilMessage[] {
406
+ const council = getCouncil(councilId);
407
+ if (!council) return [];
408
+
409
+ let messages = council.messages;
410
+
411
+ if (options?.speakerId) {
412
+ messages = messages.filter((m) => m.speakerId === options.speakerId);
413
+ }
414
+
415
+ if (options?.speakerType) {
416
+ messages = messages.filter((m) => m.speakerType === options.speakerType);
417
+ }
418
+
419
+ if (options?.offset) {
420
+ messages = messages.slice(options.offset);
421
+ }
422
+
423
+ if (options?.limit) {
424
+ messages = messages.slice(0, options.limit);
425
+ }
426
+
427
+ return messages;
428
+ }
429
+
430
+ // ============================================================================
431
+ // Status & Resolution Operations
432
+ // ============================================================================
433
+
434
+ /**
435
+ * Update council status
436
+ */
437
+ export function setCouncilStatus(
438
+ councilId: string,
439
+ status: Council['status']
440
+ ): Council | null {
441
+ return updateCouncil(councilId, { status });
442
+ }
443
+
444
+ /**
445
+ * Set council resolution
446
+ */
447
+ export function setResolution(
448
+ councilId: string,
449
+ resolution: Resolution
450
+ ): Council | null {
451
+ return updateCouncil(councilId, {
452
+ resolution,
453
+ status: 'resolved',
454
+ });
455
+ }
456
+
457
+ /**
458
+ * Update cost tracking
459
+ */
460
+ export function updateCost(
461
+ councilId: string,
462
+ additionalTokens: number,
463
+ additionalCost: number
464
+ ): Council | null {
465
+ const council = getCouncil(councilId);
466
+ if (!council) return null;
467
+
468
+ return updateCouncil(councilId, {
469
+ totalTokensUsed: council.totalTokensUsed + additionalTokens,
470
+ estimatedCost: council.estimatedCost + additionalCost,
471
+ });
472
+ }
473
+
474
+ // ============================================================================
475
+ // Query Operations
476
+ // ============================================================================
477
+
478
+ /**
479
+ * Search councils by name or topic
480
+ */
481
+ export function searchCouncils(query: string): Council[] {
482
+ const councils = getAllCouncils();
483
+ const lowerQuery = query.toLowerCase();
484
+
485
+ return councils.filter(
486
+ (c) =>
487
+ c.name.toLowerCase().includes(lowerQuery) ||
488
+ c.topic.toLowerCase().includes(lowerQuery)
489
+ );
490
+ }
491
+
492
+ /**
493
+ * Get councils by status
494
+ */
495
+ export function getCouncilsByStatus(status: Council['status']): Council[] {
496
+ const councils = getAllCouncils();
497
+ return councils.filter((c) => c.status === status);
498
+ }
499
+
500
+ /**
501
+ * Get active councils (not resolved)
502
+ */
503
+ export function getActiveCouncils(): Council[] {
504
+ return getCouncilsByStatus('active');
505
+ }
506
+
507
+ /**
508
+ * Get recent councils
509
+ */
510
+ export function getRecentCouncils(limit = 10): Council[] {
511
+ return getAllCouncils().slice(0, limit);
512
+ }
513
+
514
+ // ============================================================================
515
+ // Export/Import
516
+ // ============================================================================
517
+
518
+ /**
519
+ * Export a council to JSON
520
+ */
521
+ export function exportCouncil(councilId: string): string | null {
522
+ const council = getCouncil(councilId);
523
+ if (!council) return null;
524
+ return JSON.stringify(council, null, 2);
525
+ }
526
+
527
+ /**
528
+ * Import a council from JSON
529
+ */
530
+ export function importCouncil(json: string): Council {
531
+ const data = JSON.parse(json);
532
+
533
+ // Validate the imported data
534
+ const validation = validateCouncil(data);
535
+ if (!validation.success) {
536
+ throw new Error(`Invalid council data: ${validation.error.message}`);
537
+ }
538
+
539
+ // Generate new ID to avoid conflicts
540
+ const council: Council = {
541
+ ...validation.data,
542
+ id: crypto.randomUUID(),
543
+ createdAt: new Date().toISOString(),
544
+ updatedAt: new Date().toISOString(),
545
+ };
546
+
547
+ const storageData = loadFromStorage();
548
+ storageData.councils.push(council);
549
+ saveToStorage(storageData);
550
+
551
+ return council;
552
+ }
553
+
554
+ /**
555
+ * Duplicate a council
556
+ */
557
+ export function duplicateCouncil(councilId: string, newName?: string): Council | null {
558
+ const original = getCouncil(councilId);
559
+ if (!original) return null;
560
+
561
+ const now = new Date().toISOString();
562
+
563
+ // Build persona ID mapping (old → new) so role assignments stay linked
564
+ const personaIdMap = new Map<string, string>();
565
+ const newPersonas = original.personas.map((p) => {
566
+ const newId = crypto.randomUUID();
567
+ personaIdMap.set(p.id, newId);
568
+ return { ...p, id: newId };
569
+ });
570
+
571
+ // Remap role assignments to new persona IDs
572
+ const newRoleAssignments = original.deliberation?.roleAssignments?.map((ra) => ({
573
+ ...ra,
574
+ personaId: personaIdMap.get(ra.personaId) || ra.personaId,
575
+ }));
576
+
577
+ const duplicate: Council = {
578
+ ...original,
579
+ id: crypto.randomUUID(),
580
+ name: newName || `${original.name} (Copy)`,
581
+ createdAt: now,
582
+ updatedAt: now,
583
+ messages: [],
584
+ status: 'active',
585
+ resolution: undefined,
586
+ totalTokensUsed: 0,
587
+ estimatedCost: 0,
588
+ personas: newPersonas,
589
+ deliberation: original.deliberation ? {
590
+ ...original.deliberation,
591
+ roleAssignments: newRoleAssignments || [],
592
+ } : undefined,
593
+ deliberationState: undefined,
594
+ };
595
+
596
+ const data = loadFromStorage();
597
+ data.councils.push(duplicate);
598
+ saveToStorage(data);
599
+
600
+ return duplicate;
601
+ }
602
+
603
+ // ============================================================================
604
+ // Deliberation State Operations
605
+ // ============================================================================
606
+
607
+ /**
608
+ * Initialize deliberation state for a council
609
+ */
610
+ export function initializeDeliberationState(councilId: string): Council | null {
611
+ const council = getCouncil(councilId);
612
+ if (!council || !council.deliberation) return null;
613
+
614
+ const initialState: DeliberationState = {
615
+ currentPhase: 'created',
616
+ currentRound: 0,
617
+ roundRunId: crypto.randomUUID(),
618
+ maxRounds: council.deliberation.maxRounds,
619
+ revisionCount: 0,
620
+ maxRevisions: council.deliberation.maxRevisions,
621
+ roundSubmissions: {},
622
+ roundSummaries: {},
623
+ activeContextId: '',
624
+ activeContextVersion: 0,
625
+ pendingPatches: [],
626
+ reDeliberationCount: 0,
627
+ errorLog: [],
628
+ };
629
+
630
+ return updateCouncil(councilId, { deliberationState: initialState });
631
+ }
632
+
633
+ /**
634
+ * Update deliberation state
635
+ */
636
+ export function updateDeliberationState(
637
+ councilId: string,
638
+ updates: Partial<DeliberationState>
639
+ ): Council | null {
640
+ const council = getCouncil(councilId);
641
+ if (!council || !council.deliberationState) return null;
642
+
643
+ const updatedState: DeliberationState = {
644
+ ...council.deliberationState,
645
+ ...updates,
646
+ };
647
+
648
+ return updateCouncil(councilId, { deliberationState: updatedState });
649
+ }
650
+
651
+ /**
652
+ * Update deliberation phase
653
+ */
654
+ export function setDeliberationPhase(
655
+ councilId: string,
656
+ phase: DeliberationPhase,
657
+ previousPhase?: DeliberationPhase
658
+ ): Council | null {
659
+ let council = getCouncil(councilId);
660
+ if (!council) return null;
661
+
662
+ // Initialize deliberationState if it doesn't exist
663
+ if (!council.deliberationState) {
664
+ council = initializeDeliberationState(councilId);
665
+ if (!council) return null;
666
+ }
667
+
668
+ const updates: Partial<DeliberationState> = { currentPhase: phase };
669
+ if (previousPhase !== undefined) {
670
+ updates.previousPhase = previousPhase;
671
+ }
672
+
673
+ return updateDeliberationState(councilId, updates);
674
+ }
675
+
676
+ /**
677
+ * Advance to next round
678
+ */
679
+ export function advanceDeliberationRound(councilId: string): Council | null {
680
+ const council = getCouncil(councilId);
681
+ if (!council || !council.deliberationState) return null;
682
+
683
+ const newRound = council.deliberationState.currentRound + 1;
684
+
685
+ return updateDeliberationState(councilId, {
686
+ currentRound: newRound,
687
+ roundRunId: crypto.randomUUID(),
688
+ });
689
+ }
690
+
691
+ /**
692
+ * Record a consultant submission for the current round
693
+ */
694
+ export function recordRoundSubmission(
695
+ councilId: string,
696
+ personaId: string
697
+ ): Council | null {
698
+ const council = getCouncil(councilId);
699
+ if (!council || !council.deliberationState) return null;
700
+
701
+ const currentRound = council.deliberationState.currentRound;
702
+ const submissions = { ...council.deliberationState.roundSubmissions };
703
+
704
+ if (!submissions[currentRound]) {
705
+ submissions[currentRound] = [];
706
+ }
707
+
708
+ if (!submissions[currentRound].includes(personaId)) {
709
+ submissions[currentRound].push(personaId);
710
+ }
711
+
712
+ return updateDeliberationState(councilId, { roundSubmissions: submissions });
713
+ }
714
+
715
+ /**
716
+ * Check if all consultants have submitted for the current round
717
+ */
718
+ export function isRoundComplete(councilId: string): boolean {
719
+ const council = getCouncil(councilId);
720
+ if (!council || !council.deliberation || !council.deliberationState) return false;
721
+
722
+ const currentRound = council.deliberationState.currentRound;
723
+ const submissions = council.deliberationState.roundSubmissions[currentRound] || [];
724
+
725
+ // Get consultant persona IDs from role assignments
726
+ const consultantIds = council.deliberation.roleAssignments
727
+ .filter((r) => r.role === 'consultant')
728
+ .map((r) => r.personaId);
729
+
730
+ return consultantIds.every((id) => submissions.includes(id));
731
+ }
732
+
733
+ /**
734
+ * Set role assignments for a council
735
+ */
736
+ export function setRoleAssignments(
737
+ councilId: string,
738
+ assignments: DeliberationRoleAssignment[]
739
+ ): Council | null {
740
+ const council = getCouncil(councilId);
741
+ if (!council || !council.deliberation) return null;
742
+
743
+ const updatedDeliberation: DeliberationConfig = {
744
+ ...council.deliberation,
745
+ roleAssignments: assignments,
746
+ };
747
+
748
+ return updateCouncil(councilId, { deliberation: updatedDeliberation });
749
+ }
750
+
751
+ /**
752
+ * Add a pending patch to deliberation state
753
+ */
754
+ export function addPendingPatch(councilId: string, patchId: string): Council | null {
755
+ const council = getCouncil(councilId);
756
+ if (!council || !council.deliberationState) return null;
757
+
758
+ const pendingPatches = [...council.deliberationState.pendingPatches, patchId];
759
+
760
+ return updateDeliberationState(councilId, { pendingPatches });
761
+ }
762
+
763
+ /**
764
+ * Remove a pending patch from deliberation state
765
+ */
766
+ export function removePendingPatch(councilId: string, patchId: string): Council | null {
767
+ const council = getCouncil(councilId);
768
+ if (!council || !council.deliberationState) return null;
769
+
770
+ const pendingPatches = council.deliberationState.pendingPatches.filter(
771
+ (id) => id !== patchId
772
+ );
773
+
774
+ return updateDeliberationState(councilId, { pendingPatches });
775
+ }
776
+
777
+ /**
778
+ * Set round summary
779
+ */
780
+ export function setRoundSummary(
781
+ councilId: string,
782
+ round: number,
783
+ summary: string
784
+ ): Council | null {
785
+ const council = getCouncil(councilId);
786
+ if (!council || !council.deliberationState) return null;
787
+
788
+ const roundSummaries = {
789
+ ...council.deliberationState.roundSummaries,
790
+ [round]: summary,
791
+ };
792
+
793
+ return updateDeliberationState(councilId, { roundSummaries });
794
+ }
795
+
796
+ /**
797
+ * Set active context
798
+ */
799
+ export function setActiveContext(
800
+ councilId: string,
801
+ contextId: string,
802
+ version: number
803
+ ): Council | null {
804
+ return updateDeliberationState(councilId, {
805
+ activeContextId: contextId,
806
+ activeContextVersion: version,
807
+ });
808
+ }
809
+
810
+ /**
811
+ * Set manager's last evaluation
812
+ */
813
+ export function setManagerEvaluation(
814
+ councilId: string,
815
+ evaluation: DeliberationState['managerLastEvaluation']
816
+ ): Council | null {
817
+ return updateDeliberationState(councilId, { managerLastEvaluation: evaluation });
818
+ }
819
+
820
+ /**
821
+ * Set final decision ID
822
+ */
823
+ export function setFinalDecision(councilId: string, decisionId: string): Council | null {
824
+ return updateDeliberationState(councilId, { finalDecisionId: decisionId });
825
+ }
826
+
827
+ /**
828
+ * Set work directive ID
829
+ */
830
+ export function setWorkDirective(councilId: string, directiveId: string): Council | null {
831
+ return updateDeliberationState(councilId, { workDirectiveId: directiveId });
832
+ }
833
+
834
+ /**
835
+ * Set current output ID
836
+ */
837
+ export function setCurrentOutput(councilId: string, outputId: string): Council | null {
838
+ return updateDeliberationState(councilId, { currentOutputId: outputId });
839
+ }
840
+
841
+ /**
842
+ * Increment revision count
843
+ */
844
+ export function incrementRevisionCount(councilId: string): Council | null {
845
+ const council = getCouncil(councilId);
846
+ if (!council || !council.deliberationState) return null;
847
+
848
+ return updateDeliberationState(councilId, {
849
+ revisionCount: council.deliberationState.revisionCount + 1,
850
+ });
851
+ }
852
+
853
+ /**
854
+ * Add error to log
855
+ */
856
+ export function addErrorToLog(councilId: string, error: string): Council | null {
857
+ const council = getCouncil(councilId);
858
+ if (!council || !council.deliberationState) return null;
859
+
860
+ const errorLog = [...council.deliberationState.errorLog, error];
861
+
862
+ return updateDeliberationState(councilId, { errorLog });
863
+ }
864
+
865
+ /**
866
+ * Get persona by role
867
+ */
868
+ export function getPersonaByRole(
869
+ council: Council,
870
+ role: DeliberationRole
871
+ ): Persona[] {
872
+ if (!council.deliberation) return [];
873
+
874
+ const roleAssignments = council.deliberation.roleAssignments.filter(
875
+ (r) => r.role === role
876
+ );
877
+
878
+ return roleAssignments
879
+ .map((r) => council.personas.find((p) => p.id === r.personaId))
880
+ .filter((p): p is Persona => p !== undefined);
881
+ }
882
+
883
+ /**
884
+ * Get role assignment for a persona
885
+ */
886
+ export function getRoleAssignment(
887
+ council: Council,
888
+ personaId: string
889
+ ): DeliberationRoleAssignment | undefined {
890
+ return council.deliberation?.roleAssignments.find((r) => r.personaId === personaId);
891
+ }
892
+
893
+ /**
894
+ * Check if council is in deliberation mode
895
+ */
896
+ export function isDeliberationMode(council: Council): boolean {
897
+ return council.orchestration.mode === 'deliberation' && council.deliberation?.enabled === true;
898
+ }
899
+
900
+ /**
901
+ * Delete council and all associated deliberation data
902
+ */
903
+ export function deleteCouncilWithData(id: string): boolean {
904
+ // Delete ledger
905
+ deleteLedger(id);
906
+
907
+ // Delete artifacts
908
+ deleteAllArtifacts(id);
909
+
910
+ // Delete council
911
+ return deleteCouncil(id);
912
+ }
913
+
914
+ // ============================================================================
915
+ // Store Class (for React integration)
916
+ // ============================================================================
917
+
918
+ export class CouncilStore {
919
+ private listeners: Set<() => void> = new Set();
920
+
921
+ subscribe(listener: () => void): () => void {
922
+ this.listeners.add(listener);
923
+ return () => this.listeners.delete(listener);
924
+ }
925
+
926
+ private notify(): void {
927
+ this.listeners.forEach((listener) => listener());
928
+ }
929
+
930
+ getAll = getAllCouncils;
931
+ get = getCouncil;
932
+
933
+ create(params: Parameters<typeof createCouncil>[0]): Council {
934
+ const council = createCouncil(params);
935
+ this.notify();
936
+ return council;
937
+ }
938
+
939
+ update(id: string, updates: Parameters<typeof updateCouncil>[1]): Council | null {
940
+ const council = updateCouncil(id, updates);
941
+ if (council) this.notify();
942
+ return council;
943
+ }
944
+
945
+ delete(id: string): boolean {
946
+ const success = deleteCouncilWithData(id);
947
+ if (success) this.notify();
948
+ return success;
949
+ }
950
+
951
+ addPersona(councilId: string, persona: Persona): Council | null {
952
+ const council = addPersona(councilId, persona);
953
+ if (council) this.notify();
954
+ return council;
955
+ }
956
+
957
+ removePersona(councilId: string, personaId: string): Council | null {
958
+ const council = removePersona(councilId, personaId);
959
+ if (council) this.notify();
960
+ return council;
961
+ }
962
+
963
+ updatePersona(councilId: string, personaId: string, updates: Partial<Omit<Persona, 'id'>>): Council | null {
964
+ const council = updatePersona(councilId, personaId, updates);
965
+ if (council) this.notify();
966
+ return council;
967
+ }
968
+
969
+ addMessage(councilId: string, message: CouncilMessage): Council | null {
970
+ const council = addMessage(councilId, message);
971
+ if (council) this.notify();
972
+ return council;
973
+ }
974
+
975
+ setStatus(councilId: string, status: Council['status']): Council | null {
976
+ const council = setCouncilStatus(councilId, status);
977
+ if (council) this.notify();
978
+ return council;
979
+ }
980
+
981
+ resolve(councilId: string, resolution: Resolution): Council | null {
982
+ const council = setResolution(councilId, resolution);
983
+ if (council) this.notify();
984
+ return council;
985
+ }
986
+
987
+ // ============================================================================
988
+ // Deliberation Methods
989
+ // ============================================================================
990
+
991
+ initializeDeliberation(councilId: string): Council | null {
992
+ const council = initializeDeliberationState(councilId);
993
+ if (council) this.notify();
994
+ return council;
995
+ }
996
+
997
+ updateDeliberationState(
998
+ councilId: string,
999
+ updates: Partial<DeliberationState>
1000
+ ): Council | null {
1001
+ const council = updateDeliberationState(councilId, updates);
1002
+ if (council) this.notify();
1003
+ return council;
1004
+ }
1005
+
1006
+ setDeliberationPhase(
1007
+ councilId: string,
1008
+ phase: DeliberationPhase,
1009
+ previousPhase?: DeliberationPhase
1010
+ ): Council | null {
1011
+ const council = setDeliberationPhase(councilId, phase, previousPhase);
1012
+ if (council) this.notify();
1013
+ return council;
1014
+ }
1015
+
1016
+ advanceRound(councilId: string): Council | null {
1017
+ const council = advanceDeliberationRound(councilId);
1018
+ if (council) this.notify();
1019
+ return council;
1020
+ }
1021
+
1022
+ recordSubmission(councilId: string, personaId: string): Council | null {
1023
+ const council = recordRoundSubmission(councilId, personaId);
1024
+ if (council) this.notify();
1025
+ return council;
1026
+ }
1027
+
1028
+ isRoundComplete(councilId: string): boolean {
1029
+ return isRoundComplete(councilId);
1030
+ }
1031
+
1032
+ setRoleAssignments(
1033
+ councilId: string,
1034
+ assignments: DeliberationRoleAssignment[]
1035
+ ): Council | null {
1036
+ const council = setRoleAssignments(councilId, assignments);
1037
+ if (council) this.notify();
1038
+ return council;
1039
+ }
1040
+
1041
+ addPendingPatch(councilId: string, patchId: string): Council | null {
1042
+ const council = addPendingPatch(councilId, patchId);
1043
+ if (council) this.notify();
1044
+ return council;
1045
+ }
1046
+
1047
+ removePendingPatch(councilId: string, patchId: string): Council | null {
1048
+ const council = removePendingPatch(councilId, patchId);
1049
+ if (council) this.notify();
1050
+ return council;
1051
+ }
1052
+
1053
+ setRoundSummary(councilId: string, round: number, summary: string): Council | null {
1054
+ const council = setRoundSummary(councilId, round, summary);
1055
+ if (council) this.notify();
1056
+ return council;
1057
+ }
1058
+
1059
+ setActiveContext(councilId: string, contextId: string, version: number): Council | null {
1060
+ const council = setActiveContext(councilId, contextId, version);
1061
+ if (council) this.notify();
1062
+ return council;
1063
+ }
1064
+
1065
+ setManagerEvaluation(
1066
+ councilId: string,
1067
+ evaluation: DeliberationState['managerLastEvaluation']
1068
+ ): Council | null {
1069
+ const council = setManagerEvaluation(councilId, evaluation);
1070
+ if (council) this.notify();
1071
+ return council;
1072
+ }
1073
+
1074
+ setFinalDecision(councilId: string, decisionId: string): Council | null {
1075
+ const council = setFinalDecision(councilId, decisionId);
1076
+ if (council) this.notify();
1077
+ return council;
1078
+ }
1079
+
1080
+ setWorkDirective(councilId: string, directiveId: string): Council | null {
1081
+ const council = setWorkDirective(councilId, directiveId);
1082
+ if (council) this.notify();
1083
+ return council;
1084
+ }
1085
+
1086
+ setCurrentOutput(councilId: string, outputId: string): Council | null {
1087
+ const council = setCurrentOutput(councilId, outputId);
1088
+ if (council) this.notify();
1089
+ return council;
1090
+ }
1091
+
1092
+ incrementRevisionCount(councilId: string): Council | null {
1093
+ const council = incrementRevisionCount(councilId);
1094
+ if (council) this.notify();
1095
+ return council;
1096
+ }
1097
+
1098
+ addError(councilId: string, error: string): Council | null {
1099
+ const council = addErrorToLog(councilId, error);
1100
+ if (council) this.notify();
1101
+ return council;
1102
+ }
1103
+
1104
+ getPersonaByRole = getPersonaByRole;
1105
+ getRoleAssignment = getRoleAssignment;
1106
+ isDeliberationMode = isDeliberationMode;
1107
+ }
1108
+
1109
+ // Singleton instance for app-wide use
1110
+ export const councilStore = new CouncilStore();