@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.
- package/LICENSE +21 -0
- package/README.md +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- 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
|
+
}
|