@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,1205 @@
1
+ /**
2
+ * Council: Coding Orchestrator
3
+ * Structured multi-worker implementation flow for coding pipeline steps.
4
+ *
5
+ * Flow: spec → decompose → implement (parallel) → review → test → debug loop
6
+ *
7
+ * Reuses the same agent invocation, ledger, and store patterns as
8
+ * DeliberationOrchestrator but skips consultant deliberation entirely.
9
+ */
10
+
11
+ import type {
12
+ Council,
13
+ Persona,
14
+ LedgerEntry,
15
+ LedgerEntryType,
16
+ DeliberationPhase,
17
+ DeliberationRole,
18
+ ArtifactRef,
19
+ ReviewVerdict,
20
+ } from './types';
21
+
22
+ import {
23
+ ledgerStore,
24
+ } from './ledger-store';
25
+
26
+ import {
27
+ createOutput,
28
+ getCurrentContext,
29
+ createContextVersion,
30
+ } from './context-store';
31
+
32
+ import {
33
+ councilStore,
34
+ getPersonaByRole,
35
+ getRoleAssignment,
36
+ } from './store';
37
+
38
+ import {
39
+ buildDecompositionPrompt,
40
+ buildModuleDirectivePrompt,
41
+ buildCodeReviewPrompt,
42
+ buildReviewerSystemPrompt,
43
+ buildDebugFixPrompt,
44
+ buildRevisionFromReviewPrompt,
45
+ getMinimalWorkerSystemPrompt,
46
+ type WorkerPermissions,
47
+ } from './prompts';
48
+
49
+ import { bootstrapDirectoryContext } from './context-bootstrap';
50
+ import { detectTestCommand } from '../pipeline/test-detect';
51
+ import { detectBuildCommand } from '../pipeline/build-detect';
52
+ import { detectInstallCommand } from '../pipeline/install-detect';
53
+
54
+ // Re-use the same agent invocation types from deliberation-orchestrator
55
+ export interface AgentInvocation {
56
+ personaId: string;
57
+ systemPrompt: string;
58
+ userMessage: string;
59
+ skipTools?: boolean;
60
+ /** MCP servers this invocation can access (undefined = all servers) */
61
+ allowedServerIds?: string[];
62
+ /** Cacheable context (e.g., bootstrap directory scan) for Anthropic prompt caching */
63
+ cacheableContext?: string;
64
+ }
65
+
66
+ export interface AgentResponse {
67
+ content: string;
68
+ tokensUsed: number;
69
+ latencyMs: number;
70
+ structured?: Record<string, unknown>;
71
+ }
72
+
73
+ export type AgentInvoker = (invocation: AgentInvocation, persona: Persona) => Promise<AgentResponse>;
74
+
75
+ export interface CodingOrchestratorConfig {
76
+ invokeAgent: AgentInvoker;
77
+ runCommand?: (cmd: string, cwd: string) => Promise<{ stdout: string; stderr: string; exit_code: number; success: boolean }>;
78
+ readFile?: (path: string) => Promise<string | null>;
79
+ onPhaseChange?: (from: DeliberationPhase, to: DeliberationPhase) => void;
80
+ onEntryAdded?: (entry: LedgerEntry) => void;
81
+ onError?: (error: Error, context: string) => void;
82
+ onAgentThinkingStart?: (persona: Persona, startedAt: number) => void;
83
+ onAgentThinkingEnd?: (persona: Persona) => void;
84
+ onAgentTimeout?: (persona: Persona, context: string, elapsedMs: number) => void;
85
+ }
86
+
87
+ // ============================================================================
88
+ // Phase Transition Table (coding-specific)
89
+ // ============================================================================
90
+
91
+ const CODING_PHASE_TRANSITIONS: Record<string, {
92
+ validNext: DeliberationPhase[];
93
+ terminal: boolean;
94
+ }> = {
95
+ created: { validNext: ['decomposing'], terminal: false },
96
+ decomposing: { validNext: ['implementing'], terminal: false },
97
+ implementing: { validNext: ['code_reviewing', 'completed'], terminal: false },
98
+ code_reviewing: { validNext: ['implementing', 'testing', 'completed'], terminal: false },
99
+ testing: { validNext: ['completed', 'debugging'], terminal: false },
100
+ debugging: { validNext: ['testing'], terminal: false },
101
+ completed: { validNext: [], terminal: true },
102
+ failed: { validNext: [], terminal: true },
103
+ cancelled: { validNext: [], terminal: true },
104
+ };
105
+
106
+ // ============================================================================
107
+ // Coding Orchestrator
108
+ // ============================================================================
109
+
110
+ export class CodingOrchestrator {
111
+ private config: CodingOrchestratorConfig;
112
+ /** Active council ID for the current workflow */
113
+ private activeCouncilId: string | null = null;
114
+ /** Bootstrapped directory context — shared with all personas via prompt */
115
+ private bootstrappedContext: string = '';
116
+ /** Phase-level rate limit retry count (reset per workflow start) */
117
+ private phaseRetryCount = 0;
118
+
119
+ constructor(config: CodingOrchestratorConfig) {
120
+ this.config = config;
121
+ }
122
+
123
+ /**
124
+ * Main entry point: run the full coding workflow.
125
+ */
126
+ async runCodingWorkflow(council: Council, spec: string): Promise<void> {
127
+ console.log('[CodingOrchestrator] Starting coding workflow...');
128
+ this.activeCouncilId = council.id;
129
+
130
+ // Always read fresh from store
131
+ council = councilStore.get(council.id) || council;
132
+
133
+ const maxReviewCycles = council.deliberation?.maxReviewCycles ?? 2;
134
+ const maxDebugCycles = council.deliberation?.maxDebugCycles ?? 5;
135
+
136
+ let snapshotSha: string | null = null;
137
+ this.phaseRetryCount = 0;
138
+
139
+ const warnings: string[] = [];
140
+
141
+ // Bootstrap directory context if enabled — stored for all personas
142
+ let enrichedSpec = spec;
143
+ if (council.deliberation?.bootstrapContext && council.deliberation?.workingDirectory) {
144
+ try {
145
+ const dirContext = await bootstrapDirectoryContext(council.deliberation.workingDirectory, { deep: true });
146
+ if (dirContext) {
147
+ this.bootstrappedContext = dirContext;
148
+ enrichedSpec = `${dirContext}\n\n---\n\n${spec}`;
149
+ }
150
+ } catch (error) {
151
+ console.warn('[CodingOrchestrator] Directory context bootstrap failed:', error);
152
+ }
153
+ }
154
+
155
+ try {
156
+ // Git snapshot before any changes
157
+ snapshotSha = await this.createGitSnapshot(council);
158
+
159
+ // Phase 1: Decompose
160
+ this.transitionPhase(council.id, 'decomposing');
161
+ try {
162
+ await this.decomposeSpec(council, enrichedSpec);
163
+ } catch (decomposeError) {
164
+ const errMsg = (decomposeError as Error).message || String(decomposeError);
165
+ console.error('[CodingOrchestrator] Decompose failed, using fallback single-module:', errMsg);
166
+ warnings.push(`Decomposition failed (${errMsg}) — used raw spec as single module`);
167
+
168
+ const manager = this.getManager(council);
169
+ this.createEntry(
170
+ council.id, 'manager', manager.id, 'error', 'decomposing',
171
+ `Decomposition failed: ${errMsg}. Falling back to single-module plan.`,
172
+ 0, 0
173
+ );
174
+
175
+ // Fallback: single "main" module using the raw spec as the directive
176
+ councilStore.updateDeliberationState(council.id, {
177
+ moduleDecomposition: {
178
+ modules: [{ name: 'main', files: [], interfaces: '', dependencies: [], directive: spec }],
179
+ integrationNotes: '',
180
+ testStrategy: '',
181
+ buildCommand: '',
182
+ installCommand: '',
183
+ },
184
+ moduleOutputs: {},
185
+ });
186
+ }
187
+
188
+ // Phase 2+3: Implement → Review loop
189
+ let reviewCycleCount = 0;
190
+ while (true) {
191
+ council = councilStore.get(council.id)!;
192
+
193
+ this.transitionPhase(council.id, 'implementing');
194
+ await this.implementModules(council, spec, reviewCycleCount > 0);
195
+ council = councilStore.get(council.id)!;
196
+
197
+ // Check if reviewer exists
198
+ const reviewers = getPersonaByRole(council, 'reviewer');
199
+ if (reviewers.length === 0) {
200
+ console.log('[CodingOrchestrator] No reviewer configured — skipping code review');
201
+ break;
202
+ }
203
+
204
+ this.transitionPhase(council.id, 'code_reviewing');
205
+ let verdict: ReviewVerdict;
206
+ try {
207
+ verdict = await this.reviewCode(council, spec);
208
+ } catch (reviewError) {
209
+ const errMsg = (reviewError as Error).message || String(reviewError);
210
+ console.error('[CodingOrchestrator] Review failed, skipping:', errMsg);
211
+ warnings.push(`Code review skipped (${errMsg})`);
212
+
213
+ const reviewers = getPersonaByRole(council, 'reviewer');
214
+ const authorId = reviewers[0]?.id || this.getManager(council).id;
215
+ this.createEntry(
216
+ council.id, 'reviewer', authorId, 'error', 'code_reviewing',
217
+ `Code review failed: ${errMsg}. Skipping review phase.`,
218
+ 0, 0
219
+ );
220
+
221
+ verdict = { verdict: 'pass', issues: [], summary: `Review skipped due to error: ${errMsg}` };
222
+ }
223
+
224
+ if (verdict.verdict === 'pass') {
225
+ console.log('[CodingOrchestrator] Code review passed');
226
+ break;
227
+ }
228
+
229
+ reviewCycleCount++;
230
+ councilStore.updateDeliberationState(council.id, { reviewCycleCount });
231
+
232
+ if (reviewCycleCount >= maxReviewCycles) {
233
+ console.log(`[CodingOrchestrator] Max review cycles (${maxReviewCycles}) reached — proceeding`);
234
+ break;
235
+ }
236
+
237
+ // Store formatted review issues so revision workers get clear, actionable feedback
238
+ // instead of raw LLM output mixed with tool-use noise
239
+ this.storeFormattedReviewFeedback(council, verdict);
240
+
241
+ console.log(`[CodingOrchestrator] Review cycle ${reviewCycleCount}/${maxReviewCycles} — revising`);
242
+ council = councilStore.get(council.id)!;
243
+ }
244
+
245
+ // Phase 4: Test loop (build + test verification)
246
+ council = councilStore.get(council.id)!;
247
+ const decomposition = council.deliberationState?.moduleDecomposition;
248
+ const installCommand = decomposition?.installCommand || await this.autoDetectInstallCommand(council);
249
+ const buildCommand = decomposition?.buildCommand || await this.autoDetectBuildCommand(council);
250
+ const testCommand = council.deliberation?.testCommand || await this.autoDetectTestCommand(council);
251
+
252
+ // Combine install + build + test into one verification command
253
+ const commandParts = [installCommand, buildCommand, testCommand].filter(Boolean);
254
+ const verifyCommand = commandParts.length > 0 ? commandParts.join(' && ') : null;
255
+
256
+ if (verifyCommand) {
257
+ let debugCycleCount = 0;
258
+ while (true) {
259
+ this.transitionPhase(council.id, 'testing');
260
+ const testResult = await this.runTests(council, verifyCommand);
261
+
262
+ if (testResult.passed) {
263
+ console.log('[CodingOrchestrator] Tests passed!');
264
+ break;
265
+ }
266
+
267
+ debugCycleCount++;
268
+ councilStore.updateDeliberationState(council.id, { debugCycleCount });
269
+
270
+ if (debugCycleCount >= maxDebugCycles) {
271
+ console.log(`[CodingOrchestrator] Max debug cycles (${maxDebugCycles}) reached`);
272
+ break;
273
+ }
274
+
275
+ console.log(`[CodingOrchestrator] Debug cycle ${debugCycleCount}/${maxDebugCycles}`);
276
+ this.transitionPhase(council.id, 'debugging');
277
+ try {
278
+ await this.debugFix(council, testResult.output, spec);
279
+ } catch (debugError) {
280
+ const errMsg = (debugError as Error).message || String(debugError);
281
+ console.error('[CodingOrchestrator] Debug fix failed, proceeding with current code:', errMsg);
282
+ warnings.push(`Debug fix failed (${errMsg}) — proceeding with existing code`);
283
+
284
+ const workers = getPersonaByRole(council, 'worker');
285
+ const authorId = workers[0]?.id || this.getManager(council).id;
286
+ this.createEntry(
287
+ council.id, 'worker', authorId, 'error', 'debugging',
288
+ `Debug fix failed: ${errMsg}. Proceeding with current code.`,
289
+ 0, 0
290
+ );
291
+
292
+ break;
293
+ }
294
+ council = councilStore.get(council.id)!;
295
+ }
296
+ } else {
297
+ console.log('[CodingOrchestrator] No build or test command found — skipping verification phase');
298
+ }
299
+
300
+ // Phase 5: Merge outputs and complete
301
+ council = councilStore.get(council.id)!;
302
+ await this.mergeAndComplete(council, spec, warnings);
303
+
304
+ } catch (error) {
305
+ const errMsg = (error as Error).message || String(error);
306
+ const isRateLimit = /\b(429|rate.limit|too many requests)\b/i.test(errMsg);
307
+
308
+ council = councilStore.get(council.id)!;
309
+ const currentPhase = council.deliberationState?.currentPhase;
310
+
311
+ // Rate limit: wait and retry from the current phase instead of failing
312
+ if (isRateLimit && this.phaseRetryCount < 3) {
313
+ this.phaseRetryCount++;
314
+ const delayMs = 120_000 * Math.pow(1.5, this.phaseRetryCount - 1);
315
+ console.warn(
316
+ `[CodingOrchestrator] Rate limit during ${currentPhase}, retrying in ${Math.round(delayMs / 1000)}s ` +
317
+ `(attempt ${this.phaseRetryCount}/3)`
318
+ );
319
+ this.createEntry(
320
+ council.id, 'manager', this.getManager(council).id, 'error',
321
+ currentPhase || 'created',
322
+ `Rate limited during ${currentPhase} — waiting ${Math.round(delayMs / 1000)}s before retrying...`,
323
+ 0, 0
324
+ );
325
+ await new Promise(resolve => setTimeout(resolve, delayMs));
326
+ // Re-run from current phase — the workflow reads state from the store
327
+ // so it will resume from where it left off
328
+ return this.runCodingWorkflow(councilStore.get(council.id)!, spec);
329
+ }
330
+
331
+ console.error('[CodingOrchestrator] Workflow error:', errMsg);
332
+
333
+ // Attribute error to the phase's active role
334
+ try {
335
+ const isWorkerPhase = currentPhase === 'implementing' || currentPhase === 'debugging';
336
+ const isReviewerPhase = currentPhase === 'code_reviewing';
337
+ const role: DeliberationRole = isWorkerPhase ? 'worker' : isReviewerPhase ? 'reviewer' : 'manager';
338
+
339
+ let persona: Persona;
340
+ if (isWorkerPhase) {
341
+ const workers = getPersonaByRole(council, 'worker');
342
+ persona = workers[0] || this.getManager(council);
343
+ } else if (isReviewerPhase) {
344
+ const reviewers = getPersonaByRole(council, 'reviewer');
345
+ persona = reviewers[0] || this.getManager(council);
346
+ } else {
347
+ persona = this.getManager(council);
348
+ }
349
+
350
+ this.createEntry(
351
+ council.id, role, persona.id, 'error',
352
+ currentPhase || 'created',
353
+ `Coding workflow error during ${currentPhase}: ${errMsg}`,
354
+ 0, 0
355
+ );
356
+ } catch { /* best effort */ }
357
+
358
+ if (snapshotSha) {
359
+ console.log(`[CodingOrchestrator] Pre-pipeline snapshot: ${snapshotSha}. Rollback: git reset --hard ${snapshotSha}`);
360
+ }
361
+
362
+ this.transitionPhase(council.id, 'failed');
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ // ==========================================================================
368
+ // Phase 1: Decompose Spec
369
+ // ==========================================================================
370
+
371
+ private async decomposeSpec(council: Council, spec: string): Promise<void> {
372
+ const manager = this.getManager(council);
373
+ const workers = getPersonaByRole(council, 'worker');
374
+ const workerCount = Math.max(workers.length, 1);
375
+
376
+ console.log(`[CodingOrchestrator] Decomposing spec for ${workerCount} worker(s)`);
377
+
378
+ const systemPrompt = manager.predisposition.systemPrompt;
379
+ const userMessage = buildDecompositionPrompt(spec, workerCount);
380
+
381
+ const response = await this.invokeAgentSafe(
382
+ { personaId: manager.id, systemPrompt, userMessage },
383
+ manager,
384
+ 'decomposition'
385
+ );
386
+
387
+ // Parse decomposition JSON
388
+ const decomposition = this.parseDecompositionJson(response.content);
389
+
390
+ // Store in deliberation state
391
+ councilStore.updateDeliberationState(council.id, {
392
+ moduleDecomposition: decomposition,
393
+ moduleOutputs: {},
394
+ });
395
+
396
+ // Create ledger entry
397
+ this.createEntry(
398
+ council.id, 'manager', manager.id, 'decomposition', 'decomposing',
399
+ response.content, response.tokensUsed, response.latencyMs
400
+ );
401
+
402
+ console.log(`[CodingOrchestrator] Decomposed into ${decomposition.modules.length} module(s):`,
403
+ decomposition.modules.map(m => m.name));
404
+ }
405
+
406
+ // ==========================================================================
407
+ // Phase 2: Implement Modules (parallel)
408
+ // ==========================================================================
409
+
410
+ private async implementModules(council: Council, spec: string, isRevision: boolean): Promise<void> {
411
+ const workers = getPersonaByRole(council, 'worker');
412
+ if (workers.length === 0) {
413
+ throw new Error('No workers assigned to council');
414
+ }
415
+
416
+ const state = council.deliberationState;
417
+ const decomposition = state?.moduleDecomposition;
418
+ if (!decomposition) {
419
+ throw new Error('No module decomposition found — decompose first');
420
+ }
421
+
422
+ const modules = decomposition.modules;
423
+
424
+ // Get latest review feedback if this is a revision cycle
425
+ let reviewFeedback: string | undefined;
426
+ if (isRevision) {
427
+ // Find the most recent code_review entry
428
+ const allEntries = ledgerStore.getAll(council.id);
429
+ const lastReview = [...allEntries].reverse().find(e => e.entryType === 'code_review');
430
+ reviewFeedback = lastReview?.content;
431
+ }
432
+
433
+ // Assign modules to workers round-robin
434
+ const assignments: Array<{ module: typeof modules[0]; worker: Persona }> = [];
435
+ for (let i = 0; i < modules.length; i++) {
436
+ const worker = workers[i % workers.length];
437
+ modules[i].assignedWorkerId = worker.id;
438
+ assignments.push({ module: modules[i], worker });
439
+ }
440
+
441
+ console.log(`[CodingOrchestrator] Implementing ${modules.length} module(s) with ${workers.length} worker(s) sequentially`,
442
+ isRevision ? '(revision)' : '');
443
+
444
+ // Run workers sequentially to avoid filesystem conflicts when multiple
445
+ // workers have write permissions to the same working directory.
446
+ const moduleOutputs: Record<string, string> = { ...(state?.moduleOutputs || {}) };
447
+ let failureCount = 0;
448
+
449
+ for (const { module, worker } of assignments) {
450
+ try {
451
+ const assignment = getRoleAssignment(council, worker.id);
452
+ const suppressPersona = assignment?.suppressPersona !== false;
453
+
454
+ const workerPermissions: WorkerPermissions = {
455
+ writePermissions: assignment?.writePermissions,
456
+ workingDirectory: council.deliberation?.workingDirectory,
457
+ directoryConstrained: council.deliberation?.directoryConstrained,
458
+ };
459
+
460
+ const systemPrompt = suppressPersona
461
+ ? getMinimalWorkerSystemPrompt(workerPermissions)
462
+ : worker.predisposition.systemPrompt;
463
+
464
+ let userMessage: string;
465
+
466
+ if (isRevision && reviewFeedback) {
467
+ // Revision: include review feedback + previous output
468
+ const previousOutput = state?.moduleOutputs?.[module.name] || '';
469
+ userMessage = buildRevisionFromReviewPrompt(
470
+ reviewFeedback, previousOutput, module.directive, workerPermissions
471
+ );
472
+ } else {
473
+ // First pass: build module directive
474
+ const otherInterfaces = modules
475
+ .filter(m => m.name !== module.name)
476
+ .map(m => ({ name: m.name, interfaces: m.interfaces }));
477
+
478
+ userMessage = buildModuleDirectivePrompt(
479
+ module, otherInterfaces, decomposition.integrationNotes, workerPermissions
480
+ );
481
+ }
482
+
483
+ this.config.onAgentThinkingStart?.(worker, Date.now());
484
+
485
+ const response = await this.invokeAgentSafe(
486
+ { personaId: worker.id, systemPrompt, userMessage },
487
+ worker,
488
+ 'module_implementation'
489
+ );
490
+
491
+ // Create ledger entry
492
+ this.createEntry(
493
+ council.id, 'worker', worker.id,
494
+ 'module_output',
495
+ 'implementing',
496
+ `[Module: ${module.name}]\n\n${response.content}`,
497
+ response.tokensUsed, response.latencyMs
498
+ );
499
+
500
+ // Evolve context with module implementation
501
+ this.appendToContext(council.id, `Module Implementation (${module.name})`, worker.name, response.content, 'worker', worker.id);
502
+
503
+ moduleOutputs[module.name] = response.content;
504
+ } catch (error) {
505
+ failureCount++;
506
+ console.error(`[CodingOrchestrator] Worker failed on module ${module.name}:`, error);
507
+ }
508
+ }
509
+
510
+ if (failureCount === assignments.length) {
511
+ throw new Error('All workers failed during implementation');
512
+ }
513
+
514
+ councilStore.updateDeliberationState(council.id, { moduleOutputs });
515
+ }
516
+
517
+ // ==========================================================================
518
+ // Phase 3: Code Review
519
+ // ==========================================================================
520
+
521
+ private async reviewCode(council: Council, spec: string): Promise<ReviewVerdict> {
522
+ const reviewers = getPersonaByRole(council, 'reviewer');
523
+ if (reviewers.length === 0) {
524
+ return { verdict: 'pass', issues: [], summary: 'No reviewer configured — auto-pass' };
525
+ }
526
+
527
+ const reviewer = reviewers[0];
528
+ const state = council.deliberationState;
529
+ const moduleOutputs = state?.moduleOutputs || {};
530
+
531
+ const workerOutputs = Object.entries(moduleOutputs).map(
532
+ ([moduleName, output]) => ({ moduleName, output })
533
+ );
534
+
535
+ if (workerOutputs.length === 0) {
536
+ return { verdict: 'pass', issues: [], summary: 'No module outputs to review' };
537
+ }
538
+
539
+ const expectedOutput = council.deliberation?.expectedOutput;
540
+
541
+ const assignment = getRoleAssignment(council, reviewer.id);
542
+ const suppressPersona = assignment?.suppressPersona !== false;
543
+
544
+ const systemPrompt = suppressPersona
545
+ ? buildReviewerSystemPrompt()
546
+ : reviewer.predisposition.systemPrompt;
547
+
548
+ const userMessage = buildCodeReviewPrompt(spec, workerOutputs, expectedOutput);
549
+
550
+ console.log(`[CodingOrchestrator] Reviewer ${reviewer.name} reviewing ${workerOutputs.length} module(s)`);
551
+
552
+ const response = await this.invokeAgentSafe(
553
+ { personaId: reviewer.id, systemPrompt, userMessage },
554
+ reviewer,
555
+ 'code_review'
556
+ );
557
+
558
+ // Parse review verdict
559
+ const verdict = this.parseReviewVerdict(response.content);
560
+
561
+ // Create ledger entry
562
+ this.createEntry(
563
+ council.id, 'reviewer', reviewer.id, 'code_review', 'code_reviewing',
564
+ response.content, response.tokensUsed, response.latencyMs
565
+ );
566
+
567
+ // Evolve context with review findings
568
+ this.appendToContext(council.id, 'Code Review', reviewer.name, response.content, 'consultant', reviewer.id);
569
+
570
+ return verdict;
571
+ }
572
+
573
+ // ==========================================================================
574
+ // Phase 4: Test Execution
575
+ // ==========================================================================
576
+
577
+ private async runTests(
578
+ council: Council,
579
+ testCommand: string
580
+ ): Promise<{ passed: boolean; output: string }> {
581
+ const workingDir = council.deliberation?.workingDirectory;
582
+ if (!workingDir) {
583
+ console.warn('[CodingOrchestrator] No working directory set — cannot run tests');
584
+ return { passed: true, output: 'No working directory configured' };
585
+ }
586
+
587
+ console.log(`[CodingOrchestrator] Running tests: ${testCommand} in ${workingDir}`);
588
+
589
+ try {
590
+ if (!this.config.runCommand) {
591
+ console.warn('[CodingOrchestrator] No runCommand callback — cannot run tests');
592
+ return { passed: true, output: 'No runCommand configured' };
593
+ }
594
+ const result = await this.config.runCommand(testCommand, workingDir);
595
+
596
+ const output = (result.stdout + '\n' + result.stderr).trim();
597
+
598
+ // Create ledger entry
599
+ const manager = this.getManager(council);
600
+ this.createEntry(
601
+ council.id, 'manager', manager.id, 'test_result', 'testing',
602
+ `Test command: ${testCommand}\nExit code: ${result.exit_code}\n\n${output}`,
603
+ 0, 0
604
+ );
605
+
606
+ return {
607
+ passed: result.exit_code === 0,
608
+ output,
609
+ };
610
+ } catch (error) {
611
+ const errMsg = (error as Error).message || String(error);
612
+ console.error('[CodingOrchestrator] Test execution error:', errMsg);
613
+
614
+ const manager = this.getManager(council);
615
+ this.createEntry(
616
+ council.id, 'manager', manager.id, 'test_result', 'testing',
617
+ `Test command failed: ${testCommand}\nError: ${errMsg}`,
618
+ 0, 0
619
+ );
620
+
621
+ return { passed: false, output: errMsg };
622
+ }
623
+ }
624
+
625
+ // ==========================================================================
626
+ // Phase 5: Debug Fix
627
+ // ==========================================================================
628
+
629
+ private async debugFix(council: Council, testOutput: string, spec: string): Promise<void> {
630
+ const workers = getPersonaByRole(council, 'worker');
631
+ if (workers.length === 0) {
632
+ throw new Error('No workers available for debugging');
633
+ }
634
+
635
+ const state = council.deliberationState;
636
+ const decomposition = state?.moduleDecomposition;
637
+ const moduleOutputs = { ...(state?.moduleOutputs || {}) };
638
+
639
+ if (decomposition && decomposition.modules.length > 0) {
640
+ // Route errors to each module's assigned worker sequentially
641
+ for (const module of decomposition.modules) {
642
+ const worker = workers.find(w => w.id === module.assignedWorkerId) || workers[0];
643
+ const assignment = getRoleAssignment(council, worker.id);
644
+ const suppressPersona = assignment?.suppressPersona !== false;
645
+
646
+ const workerPermissions: WorkerPermissions = {
647
+ writePermissions: assignment?.writePermissions,
648
+ workingDirectory: council.deliberation?.workingDirectory,
649
+ directoryConstrained: council.deliberation?.directoryConstrained,
650
+ };
651
+
652
+ const systemPrompt = suppressPersona
653
+ ? getMinimalWorkerSystemPrompt(workerPermissions)
654
+ : worker.predisposition.systemPrompt;
655
+
656
+ const userMessage = buildDebugFixPrompt(
657
+ testOutput, moduleOutputs[module.name] || '', spec,
658
+ workerPermissions, module.name, module.files
659
+ );
660
+
661
+ console.log(`[CodingOrchestrator] Worker ${worker.name} fixing module ${module.name}`);
662
+
663
+ const response = await this.invokeAgentSafe(
664
+ { personaId: worker.id, systemPrompt, userMessage },
665
+ worker,
666
+ 'debug_fix'
667
+ );
668
+
669
+ moduleOutputs[module.name] = response.content;
670
+
671
+ this.createEntry(
672
+ council.id, 'worker', worker.id, 'debug_fix', 'debugging',
673
+ `[Module: ${module.name}]\n\n${response.content}`,
674
+ response.tokensUsed, response.latencyMs
675
+ );
676
+ }
677
+
678
+ councilStore.updateDeliberationState(council.id, { moduleOutputs });
679
+ } else {
680
+ // Fallback: single debugger (no decomposition available)
681
+ const debugger_ = workers[0];
682
+ const allCode = Object.entries(moduleOutputs)
683
+ .map(([name, output]) => `=== Module: ${name} ===\n${output}`)
684
+ .join('\n\n');
685
+
686
+ const assignment = getRoleAssignment(council, debugger_.id);
687
+ const suppressPersona = assignment?.suppressPersona !== false;
688
+
689
+ const workerPermissions: WorkerPermissions = {
690
+ writePermissions: assignment?.writePermissions,
691
+ workingDirectory: council.deliberation?.workingDirectory,
692
+ directoryConstrained: council.deliberation?.directoryConstrained,
693
+ };
694
+
695
+ const systemPrompt = suppressPersona
696
+ ? getMinimalWorkerSystemPrompt(workerPermissions)
697
+ : debugger_.predisposition.systemPrompt;
698
+
699
+ const userMessage = buildDebugFixPrompt(testOutput, allCode, spec, workerPermissions);
700
+
701
+ console.log(`[CodingOrchestrator] Debugger ${debugger_.name} fixing test failures`);
702
+
703
+ const response = await this.invokeAgentSafe(
704
+ { personaId: debugger_.id, systemPrompt, userMessage },
705
+ debugger_,
706
+ 'debug_fix'
707
+ );
708
+
709
+ this.createEntry(
710
+ council.id, 'worker', debugger_.id, 'debug_fix', 'debugging',
711
+ response.content, response.tokensUsed, response.latencyMs
712
+ );
713
+ }
714
+ }
715
+
716
+ // ==========================================================================
717
+ // Completion
718
+ // ==========================================================================
719
+
720
+ private async mergeAndComplete(council: Council, spec: string, warnings: string[] = []): Promise<void> {
721
+ const state = council.deliberationState;
722
+ const moduleOutputs = state?.moduleOutputs || {};
723
+
724
+ // Merge all module outputs into a single output artifact
725
+ const mergedContent = Object.entries(moduleOutputs)
726
+ .map(([name, output]) => `## Module: ${name}\n\n${output}`)
727
+ .join('\n\n---\n\n');
728
+
729
+ // Create output artifact (uses a dummy directive ID since coding flow
730
+ // doesn't have the traditional directive artifact)
731
+ const output = createOutput(council.id, mergedContent, 'coding-workflow');
732
+
733
+ councilStore.setCurrentOutput(council.id, output.id);
734
+
735
+ // Generate completion summary, appending any warnings from recovered phases
736
+ let summary = `Coding workflow completed. ${Object.keys(moduleOutputs).length} module(s) implemented: ${Object.keys(moduleOutputs).join(', ')}`;
737
+ if (warnings.length > 0) {
738
+ summary += `\n\nWarnings:\n${warnings.map(w => `- ${w}`).join('\n')}`;
739
+ }
740
+ councilStore.updateDeliberationState(council.id, { completionSummary: summary });
741
+
742
+ this.transitionPhase(council.id, 'completed');
743
+ councilStore.setStatus(council.id, 'resolved');
744
+
745
+ console.log('[CodingOrchestrator] Workflow completed successfully');
746
+ }
747
+
748
+ // ==========================================================================
749
+ // Helpers
750
+ // ==========================================================================
751
+
752
+ private getManager(council: Council): Persona {
753
+ const managers = getPersonaByRole(council, 'manager');
754
+ if (managers.length === 0) {
755
+ throw new Error('No manager assigned to council');
756
+ }
757
+ return managers[0];
758
+ }
759
+
760
+ private async autoDetectTestCommand(council: Council): Promise<string | null> {
761
+ const workingDir = council.deliberation?.workingDirectory;
762
+ if (!workingDir) return null;
763
+
764
+ try {
765
+ const detected = await detectTestCommand(workingDir, this.config.readFile);
766
+ if (detected) {
767
+ console.log(`[CodingOrchestrator] Auto-detected test command: ${detected.command} (${detected.framework})`);
768
+ return detected.command;
769
+ }
770
+ } catch (error) {
771
+ console.warn('[CodingOrchestrator] Test detection failed:', error);
772
+ }
773
+ return null;
774
+ }
775
+
776
+ private async autoDetectBuildCommand(council: Council): Promise<string | null> {
777
+ const workingDir = council.deliberation?.workingDirectory;
778
+ if (!workingDir) return null;
779
+
780
+ try {
781
+ const detected = await detectBuildCommand(workingDir, this.config.readFile);
782
+ if (detected) {
783
+ console.log(`[CodingOrchestrator] Auto-detected build command: ${detected.command} (${detected.framework})`);
784
+ return detected.command;
785
+ }
786
+ } catch (error) {
787
+ console.warn('[CodingOrchestrator] Build detection failed:', error);
788
+ }
789
+ return null;
790
+ }
791
+
792
+ private async autoDetectInstallCommand(council: Council): Promise<string | null> {
793
+ const workingDir = council.deliberation?.workingDirectory;
794
+ if (!workingDir) return null;
795
+
796
+ try {
797
+ const detected = await detectInstallCommand(workingDir, this.config.readFile);
798
+ if (detected) {
799
+ console.log(`[CodingOrchestrator] Auto-detected install command: ${detected.command} (${detected.framework})`);
800
+ return detected.command;
801
+ }
802
+ } catch (error) {
803
+ console.warn('[CodingOrchestrator] Install detection failed:', error);
804
+ }
805
+ return null;
806
+ }
807
+
808
+ /**
809
+ * Create a git snapshot before implementation begins so the user can
810
+ * roll back if workers destroy the codebase.
811
+ * Returns the commit SHA, or null if not in a git repo / no runCommand.
812
+ */
813
+ private async createGitSnapshot(council: Council): Promise<string | null> {
814
+ const workingDir = council.deliberation?.workingDirectory;
815
+ if (!workingDir || !this.config.runCommand) return null;
816
+
817
+ try {
818
+ // Check if inside a git repo
819
+ const check = await this.config.runCommand('git rev-parse --is-inside-work-tree', workingDir);
820
+ if (check.exit_code !== 0) return null;
821
+
822
+ // Stage everything and commit (allow-empty in case there are no changes)
823
+ await this.config.runCommand('git add -A', workingDir);
824
+ await this.config.runCommand(
825
+ 'git commit -m "kondi: pre-pipeline snapshot" --allow-empty',
826
+ workingDir
827
+ );
828
+
829
+ // Capture SHA
830
+ const shaResult = await this.config.runCommand('git rev-parse HEAD', workingDir);
831
+ const sha = shaResult.stdout.trim();
832
+
833
+ console.log(`[CodingOrchestrator] Git snapshot created: ${sha}`);
834
+
835
+ // Create ledger entry
836
+ const manager = this.getManager(council);
837
+ this.createEntry(
838
+ council.id, 'manager', manager.id, 'decomposition', 'decomposing',
839
+ `Pre-pipeline git snapshot: ${sha}\nRollback: git reset --hard ${sha}`,
840
+ 0, 0
841
+ );
842
+
843
+ return sha;
844
+ } catch (error) {
845
+ console.warn('[CodingOrchestrator] Git snapshot failed:', error);
846
+ return null;
847
+ }
848
+ }
849
+
850
+ private transitionPhase(councilId: string, to: DeliberationPhase): void {
851
+ const council = councilStore.get(councilId);
852
+ const from = council?.deliberationState?.currentPhase || 'created';
853
+
854
+ // Validate transition
855
+ const rule = CODING_PHASE_TRANSITIONS[from];
856
+ if (rule && !rule.validNext.includes(to) && to !== 'failed') {
857
+ console.warn(`[CodingOrchestrator] Unexpected phase transition: ${from} -> ${to}`);
858
+ }
859
+
860
+ councilStore.setDeliberationPhase(councilId, to, from);
861
+ this.config.onPhaseChange?.(from, to);
862
+
863
+ console.log(`[CodingOrchestrator] Phase: ${from} -> ${to}`);
864
+ }
865
+
866
+ private async invokeAgentSafe(
867
+ invocation: AgentInvocation,
868
+ persona: Persona,
869
+ context: string
870
+ ): Promise<AgentResponse> {
871
+ // Scope tool access by phase:
872
+ // Workers (module_implementation, debug_fix): full tools
873
+ // Reviewers (code_review) & decomposition: read-only tools
874
+ // Other phases: no tools
875
+ const BUILTIN_TOOLS = ['WebSearch', 'WebFetch'];
876
+ const READ_ONLY_TOOLS = [...BUILTIN_TOOLS, 'Read', 'Grep', 'Glob'];
877
+
878
+ const toolPhases = ['module_implementation', 'debug_fix'];
879
+ const readOnlyToolPhases = ['code_review', 'decomposition'];
880
+
881
+ if (toolPhases.includes(context)) {
882
+ // Workers keep default full tool access (no change needed)
883
+ } else if (readOnlyToolPhases.includes(context)) {
884
+ invocation = { ...invocation, allowedTools: READ_ONLY_TOOLS, skipTools: undefined };
885
+ } else if (!invocation.skipTools) {
886
+ invocation = { ...invocation, skipTools: true };
887
+ }
888
+ console.log(`[CodingOrchestrator] invokeAgentSafe context=${context} skipTools=${invocation.skipTools} persona=${persona.name} provider=${persona.provider}`);
889
+
890
+ // Compute effective allowed servers (intersection of step + persona)
891
+ const council = this.activeCouncilId ? councilStore.get(this.activeCouncilId) : null;
892
+ const stepServers = council?.deliberation?.allowedServerIds;
893
+ const personaServers = persona.allowedServerIds;
894
+ let effectiveServers: string[] | undefined;
895
+ if (stepServers && personaServers) {
896
+ effectiveServers = stepServers.filter(id => personaServers.includes(id));
897
+ } else {
898
+ effectiveServers = personaServers || stepServers;
899
+ }
900
+ if (effectiveServers) {
901
+ invocation = { ...invocation, allowedServerIds: effectiveServers };
902
+ }
903
+
904
+ // Apply per-persona tool access overrides from role assignment
905
+ const roleAssignment = council?.deliberation?.roleAssignments
906
+ ?.find(r => r.personaId === persona.id);
907
+
908
+ if (roleAssignment?.toolAccess === 'none') {
909
+ invocation = { ...invocation, skipTools: true };
910
+ } else if (roleAssignment?.toolAccess === 'full') {
911
+ const { skipTools: _, ...rest } = invocation;
912
+ invocation = rest as AgentInvocation;
913
+ }
914
+
915
+ if (roleAssignment?.allowedServerIds) {
916
+ invocation = { ...invocation, allowedServerIds: roleAssignment.allowedServerIds };
917
+ }
918
+
919
+ // Inject bootstrapped context into every prompt — cacheable if prompt caching is enabled.
920
+ if (this.bootstrappedContext) {
921
+ const hasContext = invocation.userMessage.includes(this.bootstrappedContext.slice(0, 100));
922
+ if (!hasContext) {
923
+ // Instead of injecting into userMessage, pass as separate cacheableContext
924
+ // so Anthropic's prompt caching can deduplicate it across calls
925
+ invocation = {
926
+ ...invocation,
927
+ cacheableContext: `## PROJECT CONTEXT\n\nThe complete source code and project structure are provided below. This is your primary source of information. Analyze the code directly from what is provided here.\n\n${this.bootstrappedContext}`,
928
+ userMessage: invocation.userMessage, // Keep userMessage clean
929
+ };
930
+ }
931
+ }
932
+
933
+ // Inject brevity instruction if configured
934
+ const wordLimit = (() => {
935
+ try {
936
+ const councils = councilStore.getAll();
937
+ for (const c of councils) {
938
+ if (c.personas.some(p => p.id === persona.id)) {
939
+ return c.deliberation?.maxWordsPerResponse;
940
+ }
941
+ }
942
+ } catch { /* ignore */ }
943
+ return undefined;
944
+ })();
945
+
946
+ if (wordLimit && wordLimit > 0) {
947
+ invocation = {
948
+ ...invocation,
949
+ userMessage: invocation.userMessage +
950
+ `\n\nIMPORTANT: Keep your response concise — aim for approximately ${wordLimit} words or fewer.`,
951
+ };
952
+ }
953
+
954
+ // Thread working directory from council so each invocation uses its own dir
955
+ const councilDir = council?.deliberation?.workingDirectory;
956
+ if (councilDir) {
957
+ invocation = { ...invocation, workingDirectory: councilDir };
958
+ }
959
+
960
+ // Retry transient errors (rate limits, overload) with exponential backoff.
961
+ // This covers ALL roles (manager, worker, reviewer) uniformly.
962
+ const MAX_TRANSIENT_RETRIES = 5;
963
+ const BASE_DELAY_MS = 15_000; // 15 seconds
964
+
965
+ for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
966
+ try {
967
+ this.config.onAgentThinkingStart?.(persona, Date.now());
968
+ const response = await this.config.invokeAgent(invocation, persona);
969
+ this.config.onAgentThinkingEnd?.(persona);
970
+ return response;
971
+ } catch (error) {
972
+ this.config.onAgentThinkingEnd?.(persona);
973
+
974
+ const errMsg = error instanceof Error ? error.message : String(error);
975
+ const isTransient = /\b(429|529|rate.limit|overloaded|too many requests)\b/i.test(errMsg);
976
+
977
+ if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
978
+ const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
979
+ console.warn(
980
+ `[CodingOrchestrator] Transient error for ${persona.name} (${context}), ` +
981
+ `retrying in ${Math.round(delayMs / 1000)}s (attempt ${attempt + 1}/${MAX_TRANSIENT_RETRIES}): ${errMsg}`
982
+ );
983
+ await new Promise(resolve => setTimeout(resolve, delayMs));
984
+ continue;
985
+ }
986
+
987
+ this.config.onError?.(error as Error, context);
988
+ throw error;
989
+ }
990
+ }
991
+
992
+ // Should not reach here
993
+ throw new Error('Unexpected: exhausted transient retry loop');
994
+ }
995
+
996
+ private createEntry(
997
+ councilId: string,
998
+ role: DeliberationRole,
999
+ authorId: string,
1000
+ entryType: LedgerEntryType,
1001
+ phase: DeliberationPhase,
1002
+ content: string,
1003
+ tokensUsed: number,
1004
+ latencyMs: number,
1005
+ artifactRefs?: ArtifactRef[],
1006
+ roundNumber?: number,
1007
+ ): LedgerEntry {
1008
+ const entry: LedgerEntry = {
1009
+ id: crypto.randomUUID(),
1010
+ timestamp: new Date().toISOString(),
1011
+ authorRole: role,
1012
+ authorPersonaId: authorId,
1013
+ entryType,
1014
+ phase,
1015
+ content,
1016
+ tokensUsed,
1017
+ latencyMs,
1018
+ artifactRefs,
1019
+ roundNumber,
1020
+ };
1021
+
1022
+ ledgerStore.append(councilId, entry);
1023
+ this.config.onEntryAdded?.(entry);
1024
+
1025
+ return entry;
1026
+ }
1027
+
1028
+ /**
1029
+ * Append a summary to the shared context document when evolveContext is enabled.
1030
+ */
1031
+ private appendToContext(
1032
+ councilId: string,
1033
+ label: string,
1034
+ personaName: string,
1035
+ content: string,
1036
+ role: 'consultant' | 'worker' | 'manager',
1037
+ personaId?: string,
1038
+ ): void {
1039
+ const council = councilStore.get(councilId);
1040
+ if (!council?.deliberation?.evolveContext) return;
1041
+
1042
+ const currentContext = getCurrentContext(councilId);
1043
+ if (!currentContext) return;
1044
+
1045
+ const summaryMatch = content.match(/## COMPLETION SUMMARY[\s\S]*/i);
1046
+ const summary = summaryMatch
1047
+ ? summaryMatch[0].slice(0, 1500)
1048
+ : content.slice(0, 2000);
1049
+
1050
+ const appendText = `\n\n---\n[${label} by ${personaName}]:\n${summary}`;
1051
+
1052
+ const updated = createContextVersion(
1053
+ councilId,
1054
+ currentContext.content + appendText,
1055
+ `${label} by ${personaName}`,
1056
+ role,
1057
+ personaId,
1058
+ );
1059
+
1060
+ councilStore.setActiveContext(councilId, updated.id, updated.version);
1061
+ console.log(`[CodingOrchestrator] Context evolved to v${updated.version}: ${label} by ${personaName}`);
1062
+ }
1063
+
1064
+ private parseDecompositionJson(content: string): {
1065
+ modules: Array<{
1066
+ name: string;
1067
+ files: string[];
1068
+ interfaces: string;
1069
+ dependencies: string[];
1070
+ directive: string;
1071
+ assignedWorkerId?: string;
1072
+ }>;
1073
+ integrationNotes: string;
1074
+ testStrategy: string;
1075
+ buildCommand?: string;
1076
+ installCommand?: string;
1077
+ } {
1078
+ try {
1079
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
1080
+ if (jsonMatch) {
1081
+ const parsed = JSON.parse(jsonMatch[0]);
1082
+ if (parsed.modules && Array.isArray(parsed.modules)) {
1083
+ return {
1084
+ modules: parsed.modules.map((m: any) => ({
1085
+ name: m.name || 'unnamed',
1086
+ files: Array.isArray(m.files) ? m.files : [],
1087
+ interfaces: m.interfaces || '',
1088
+ dependencies: Array.isArray(m.dependencies) ? m.dependencies : [],
1089
+ directive: m.directive || '',
1090
+ })),
1091
+ integrationNotes: parsed.integrationNotes || '',
1092
+ testStrategy: parsed.testStrategy || '',
1093
+ buildCommand: parsed.buildCommand || '',
1094
+ installCommand: parsed.installCommand || '',
1095
+ };
1096
+ }
1097
+ }
1098
+ } catch (e) {
1099
+ console.warn('[CodingOrchestrator] Failed to parse decomposition JSON:', e);
1100
+ }
1101
+
1102
+ // Fallback: single monolithic module
1103
+ return {
1104
+ modules: [{
1105
+ name: 'main',
1106
+ files: [],
1107
+ interfaces: '',
1108
+ dependencies: [],
1109
+ directive: content,
1110
+ }],
1111
+ integrationNotes: '',
1112
+ testStrategy: '',
1113
+ buildCommand: '',
1114
+ installCommand: '',
1115
+ };
1116
+ }
1117
+
1118
+ /**
1119
+ * Write a clean, formatted ledger entry from the parsed review verdict
1120
+ * so revision workers see actionable feedback instead of raw LLM output.
1121
+ */
1122
+ private storeFormattedReviewFeedback(council: Council, verdict: ReviewVerdict): void {
1123
+ if (verdict.issues.length === 0 && !verdict.summary) return;
1124
+
1125
+ const lines: string[] = [];
1126
+ lines.push(`## Code Review: ${verdict.verdict.toUpperCase()}`);
1127
+ lines.push('');
1128
+ if (verdict.summary) {
1129
+ lines.push(verdict.summary);
1130
+ lines.push('');
1131
+ }
1132
+ if (verdict.issues.length > 0) {
1133
+ lines.push('## Issues to Fix');
1134
+ for (const issue of verdict.issues) {
1135
+ lines.push(`- **[${issue.severity.toUpperCase()}] ${issue.module}**: ${issue.description}`);
1136
+ if (issue.suggestion) {
1137
+ lines.push(` → Fix: ${issue.suggestion}`);
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ const reviewer = getPersonaByRole(council, 'reviewer')[0];
1143
+ const authorId = reviewer?.id || 'system';
1144
+
1145
+ // This entry replaces the raw code_review entry as the revision worker's
1146
+ // feedback source (implementModules looks for the last code_review entry)
1147
+ this.createEntry(
1148
+ council.id, 'reviewer', authorId, 'code_review', 'code_reviewing',
1149
+ lines.join('\n'), 0, 0
1150
+ );
1151
+ }
1152
+
1153
+ private parseReviewVerdict(content: string): ReviewVerdict {
1154
+ // Try multiple JSON extraction strategies (tool-use output may wrap the JSON
1155
+ // in markdown fences or place it after tool-call text)
1156
+ const candidates: string[] = [];
1157
+
1158
+ // Strategy 1: markdown code block ```json ... ```
1159
+ const fencedMatch = content.match(/```(?:json)?\s*\n?(\{[\s\S]*?\})\s*\n?```/);
1160
+ if (fencedMatch) candidates.push(fencedMatch[1]);
1161
+
1162
+ // Strategy 2: last JSON object in the content (skip tool-use JSON earlier in the text)
1163
+ const allJsonMatches = [...content.matchAll(/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g)];
1164
+ if (allJsonMatches.length > 0) {
1165
+ // Work backwards — the review JSON is typically at the end
1166
+ for (let i = allJsonMatches.length - 1; i >= 0; i--) {
1167
+ candidates.push(allJsonMatches[i][0]);
1168
+ }
1169
+ }
1170
+
1171
+ // Strategy 3: greedy match (original fallback)
1172
+ const greedyMatch = content.match(/\{[\s\S]*\}/);
1173
+ if (greedyMatch) candidates.push(greedyMatch[0]);
1174
+
1175
+ for (const candidate of candidates) {
1176
+ try {
1177
+ const parsed = JSON.parse(candidate);
1178
+ // Must look like a review verdict (has verdict field)
1179
+ if (parsed.verdict) {
1180
+ return {
1181
+ verdict: parsed.verdict === 'needs_revision' ? 'needs_revision' : 'pass',
1182
+ issues: Array.isArray(parsed.issues) ? parsed.issues.map((i: any) => ({
1183
+ module: i.module || 'unknown',
1184
+ severity: ['critical', 'major', 'minor'].includes(i.severity) ? i.severity : 'minor',
1185
+ description: i.description || '',
1186
+ suggestion: i.suggestion || '',
1187
+ })) : [],
1188
+ summary: parsed.summary || content,
1189
+ };
1190
+ }
1191
+ } catch {
1192
+ // Not valid JSON or wrong shape, try next candidate
1193
+ }
1194
+ }
1195
+
1196
+ // Text fallback: if content mentions "needs_revision", use the full content as summary
1197
+ // so the worker at least sees the reviewer's prose
1198
+ const lower = content.toLowerCase();
1199
+ if (lower.includes('needs_revision') || lower.includes('needs revision')) {
1200
+ return { verdict: 'needs_revision', issues: [], summary: content };
1201
+ }
1202
+
1203
+ return { verdict: 'pass', issues: [], summary: content };
1204
+ }
1205
+ }