karajan-code 1.49.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Architect stage logic.
3
+ * Extracted from pre-loop-stages.js for maintainability.
4
+ */
5
+
6
+ import { ArchitectRole } from "../../roles/architect-role.js";
7
+ import { createAgent } from "../../agents/index.js";
8
+ import { createArchitectADRs } from "../../planning-game/architect-adrs.js";
9
+ import { addCheckpoint } from "../../session-store.js";
10
+ import { emitProgress, makeEvent } from "../../utils/events.js";
11
+ import { createStallDetector } from "../../utils/stall-detector.js";
12
+
13
+ async function handleArchitectClarification({ architectOutput, askQuestion, config, logger, emitter, eventBase, session, architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget }) {
14
+ if (!architectOutput.ok
15
+ || architectOutput.result?.verdict !== "needs_clarification"
16
+ || !architectOutput.result?.questions?.length) {
17
+ return architectOutput;
18
+ }
19
+
20
+ const questions = architectOutput.result.questions;
21
+ if (!askQuestion) {
22
+ logger.warn("Architect returned needs_clarification but no interactive input available — continuing with best-effort decisions");
23
+ return architectOutput;
24
+ }
25
+
26
+ const formatted = "The architect needs clarification before proceeding:\n\n"
27
+ + questions.map((q, i) => `${i + 1}. ${q}`).join("\n")
28
+ + "\n\nPlease provide your answers:";
29
+
30
+ emitProgress(
31
+ emitter,
32
+ makeEvent("architect:clarification", { ...eventBase, stage: "architect" }, {
33
+ message: "Architect needs clarification — pausing for human input",
34
+ detail: { questions }
35
+ })
36
+ );
37
+
38
+ const answer = await askQuestion(formatted, { iteration: 0, stage: "architect" });
39
+ if (!answer) return architectOutput;
40
+
41
+ const architect2 = new ArchitectRole({ config, logger, emitter, createAgentFn: createAgent });
42
+ await architect2.init({ task: session.task, sessionId: session.id, iteration: 0 });
43
+ const rerunStart = Date.now();
44
+ const rerunStall = createStallDetector({
45
+ onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectProvider
46
+ });
47
+ let result;
48
+ try {
49
+ result = await architect2.execute({
50
+ task: session.task,
51
+ onOutput: rerunStall.onOutput,
52
+ researchContext,
53
+ discoverResult,
54
+ triageLevel,
55
+ humanAnswers: answer
56
+ });
57
+ } finally {
58
+ rerunStall.stop();
59
+ }
60
+ trackBudget({
61
+ role: "architect",
62
+ provider: architectProvider,
63
+ model: config?.roles?.architect?.model || coderRole.model,
64
+ result,
65
+ duration_ms: Date.now() - rerunStart
66
+ });
67
+ return result;
68
+ }
69
+
70
+ export async function runArchitectStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, researchContext = null, discoverResult = null, triageLevel = null, askQuestion = null }) {
71
+ logger.setContext({ iteration: 0, stage: "architect" });
72
+ const architectProvider = config?.roles?.architect?.provider || coderRole.provider;
73
+ emitProgress(
74
+ emitter,
75
+ makeEvent("architect:start", { ...eventBase, stage: "architect" }, {
76
+ message: "Architect designing solution architecture",
77
+ detail: { architect: architectProvider, provider: architectProvider, executorType: "agent" }
78
+ })
79
+ );
80
+ const architectOnOutput = ({ stream, line }) => {
81
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "architect" }, {
82
+ message: line,
83
+ detail: { stream, agent: architectProvider }
84
+ }));
85
+ };
86
+ const architectStall = createStallDetector({
87
+ onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectProvider
88
+ });
89
+
90
+ const architect = new ArchitectRole({ config, logger, emitter, createAgentFn: createAgent });
91
+ await architect.init({ task: session.task, sessionId: session.id, iteration: 0 });
92
+ const architectStart = Date.now();
93
+ let architectOutput;
94
+ try {
95
+ architectOutput = await architect.execute({
96
+ task: session.task,
97
+ onOutput: architectStall.onOutput,
98
+ researchContext,
99
+ discoverResult,
100
+ triageLevel
101
+ });
102
+ } catch (err) {
103
+ logger.warn(`Architect threw: ${err.message}`);
104
+ architectOutput = { ok: false, summary: `Architect error: ${err.message}`, result: { error: err.message } };
105
+ } finally {
106
+ architectStall.stop();
107
+ }
108
+ trackBudget({
109
+ role: "architect",
110
+ provider: architectProvider,
111
+ model: config?.roles?.architect?.model || coderRole.model,
112
+ result: architectOutput,
113
+ duration_ms: Date.now() - architectStart
114
+ });
115
+
116
+ await addCheckpoint(session, {
117
+ stage: "architect",
118
+ iteration: 0,
119
+ ok: architectOutput.ok,
120
+ provider: architectProvider,
121
+ model: config?.roles?.architect?.model || coderRole.model || null
122
+ });
123
+
124
+ // --- Interactive clarification loop ---
125
+ architectOutput = await handleArchitectClarification({
126
+ architectOutput, askQuestion, config, logger, emitter, eventBase, session,
127
+ architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget
128
+ });
129
+
130
+ const stageResult = {
131
+ ok: architectOutput.ok,
132
+ verdict: architectOutput.result?.verdict || null,
133
+ architecture: architectOutput.result?.architecture || null,
134
+ questions: architectOutput.result?.questions || []
135
+ };
136
+
137
+ emitProgress(
138
+ emitter,
139
+ makeEvent("architect:end", { ...eventBase, stage: "architect" }, {
140
+ status: architectOutput.ok ? "ok" : "fail",
141
+ message: architectOutput.ok ? "Architecture completed" : `Architecture failed: ${architectOutput.summary}`,
142
+ detail: { ...stageResult, provider: architectProvider, executorType: "agent" }
143
+ })
144
+ );
145
+
146
+ const architectContext = architectOutput.ok ? architectOutput.result : null;
147
+
148
+ // TODO: Move ADR creation to planning-game/pipeline-adapter.js (PG coupling still here because
149
+ // stageResult.adrs is consumed synchronously within runArchitectStage's return value).
150
+ // Generate ADRs from architect tradeoffs when PG is linked
151
+ const tradeoffs = architectOutput.result?.architecture?.tradeoffs;
152
+ if (architectOutput.ok
153
+ && architectOutput.result?.verdict === "ready"
154
+ && tradeoffs?.length > 0
155
+ && session.pg_task_id
156
+ && session.pg_project_id) {
157
+ try {
158
+ const pgClient = await import("../../planning-game/client.js");
159
+ const adrResult = await createArchitectADRs({
160
+ tradeoffs,
161
+ pgTaskId: session.pg_task_id,
162
+ pgProject: session.pg_project_id,
163
+ taskTitle: session.task,
164
+ mcpClient: pgClient
165
+ });
166
+ stageResult.adrs = adrResult;
167
+ if (adrResult.created > 0) {
168
+ logger.info(`Architect: created ${adrResult.created} ADR(s) in Planning Game`);
169
+ }
170
+ } catch (err) {
171
+ logger.warn(`Architect: failed to create ADRs: ${err.message}`);
172
+ }
173
+ }
174
+
175
+ return { architectContext, stageResult };
176
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Coder stage logic.
3
+ * Extracted from iteration-stages.js for maintainability.
4
+ */
5
+
6
+ import { createAgent } from "../../agents/index.js";
7
+ import { CoderRole } from "../../roles/coder-role.js";
8
+ import { RefactorerRole } from "../../roles/refactorer-role.js";
9
+ import { addCheckpoint, markSessionStatus, saveSession } from "../../session-store.js";
10
+ import { generateDiff, getUntrackedFiles } from "../../review/diff-generator.js";
11
+ import { evaluateTddPolicy } from "../../review/tdd-policy.js";
12
+ import { buildDeferredContext } from "../../review/scope-filter.js";
13
+ import { emitProgress, makeEvent } from "../../utils/events.js";
14
+ import { runCoderWithFallback } from "../agent-fallback.js";
15
+ import { invokeSolomon } from "../solomon-escalation.js";
16
+ import { detectRateLimit } from "../../utils/rate-limit-detector.js";
17
+ import { createStallDetector } from "../../utils/stall-detector.js";
18
+
19
+ export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
20
+ logger.setContext({ iteration, stage: "coder" });
21
+ emitProgress(
22
+ emitter,
23
+ makeEvent("coder:start", { ...eventBase, stage: "coder" }, {
24
+ message: `Coder (${coderRole.provider}) running`,
25
+ detail: { coder: coderRole.provider, provider: coderRole.provider, executorType: "agent" }
26
+ })
27
+ );
28
+
29
+ const coderOnOutput = ({ stream, line }) => {
30
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "coder" }, {
31
+ message: line,
32
+ detail: { stream, agent: coderRole.provider }
33
+ }));
34
+ };
35
+ const coderStall = createStallDetector({
36
+ onOutput: coderOnOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
37
+ });
38
+ const coderStart = Date.now();
39
+ let coderExecResult;
40
+ try {
41
+ coderExecResult = await coderRoleInstance.execute({
42
+ task: plannedTask,
43
+ reviewerFeedback: session.last_reviewer_feedback,
44
+ sonarSummary: session.last_sonar_summary,
45
+ deferredContext: buildDeferredContext(session.deferred_issues),
46
+ onOutput: coderStall.onOutput
47
+ });
48
+ } finally {
49
+ coderStall.stop();
50
+ }
51
+ trackBudget({ role: "coder", provider: coderRole.provider, model: coderRole.model, result: coderExecResult.result, duration_ms: Date.now() - coderStart });
52
+
53
+ if (!coderExecResult.ok) {
54
+ const details = coderExecResult.result?.error || coderExecResult.summary || "unknown error";
55
+ const rateLimitCheck = detectRateLimit({
56
+ stderr: coderExecResult.result?.error || "",
57
+ stdout: coderExecResult.result?.output || ""
58
+ });
59
+
60
+ if (rateLimitCheck.isRateLimit) {
61
+ // Try fallback agent if configured
62
+ const fallbackCoder = config.coder_options?.fallback_coder;
63
+ if (fallbackCoder && fallbackCoder !== coderRole.provider) {
64
+ logger.warn(`Coder ${coderRole.provider} hit rate limit, falling back to ${fallbackCoder}`);
65
+ emitProgress(
66
+ emitter,
67
+ makeEvent("coder:fallback", { ...eventBase, stage: "coder" }, {
68
+ message: `Coder ${coderRole.provider} rate-limited, switching to ${fallbackCoder}`,
69
+ detail: { primary: coderRole.provider, fallback: fallbackCoder }
70
+ })
71
+ );
72
+
73
+ const fallbackResult = await runCoderWithFallback({
74
+ coderName: fallbackCoder,
75
+ fallbackCoder: null,
76
+ config,
77
+ logger,
78
+ emitter,
79
+ RoleClass: CoderRole,
80
+ roleInput: { task: plannedTask, reviewerFeedback: session.last_reviewer_feedback, sonarSummary: session.last_sonar_summary, onOutput: coderOnOutput },
81
+ session,
82
+ iteration,
83
+ onAttemptResult: ({ coder, result }) => {
84
+ trackBudget({ role: "coder", provider: coder, model: coderRole.model, result, duration_ms: Date.now() - coderStart });
85
+ }
86
+ });
87
+
88
+ if (fallbackResult.execResult?.ok) {
89
+ await addCheckpoint(session, { stage: "coder", iteration, note: `Coder completed via fallback (${fallbackCoder})`, provider: fallbackCoder, model: null });
90
+ emitProgress(
91
+ emitter,
92
+ makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
93
+ message: `Coder completed (fallback: ${fallbackCoder})`,
94
+ detail: { provider: fallbackCoder, executorType: "agent" }
95
+ })
96
+ );
97
+ return;
98
+ }
99
+ }
100
+
101
+ // No fallback or fallback also failed — enter standby
102
+ return {
103
+ action: "standby",
104
+ standbyInfo: {
105
+ agent: coderRole.provider,
106
+ cooldownMs: rateLimitCheck.cooldownMs || (rateLimitCheck.isProviderOutage ? 30000 : null),
107
+ cooldownUntil: rateLimitCheck.cooldownUntil,
108
+ message: rateLimitCheck.message,
109
+ isProviderOutage: rateLimitCheck.isProviderOutage || false
110
+ }
111
+ };
112
+ }
113
+
114
+ await markSessionStatus(session, "failed");
115
+ emitProgress(
116
+ emitter,
117
+ makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
118
+ status: "fail",
119
+ message: `Coder failed: ${details}`,
120
+ detail: { provider: coderRole.provider, executorType: "agent" }
121
+ })
122
+ );
123
+ throw new Error(`Coder failed: ${details}`);
124
+ }
125
+
126
+ await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null });
127
+ emitProgress(
128
+ emitter,
129
+ makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
130
+ message: "Coder completed",
131
+ detail: { provider: coderRole.provider, executorType: "agent" }
132
+ })
133
+ );
134
+ }
135
+
136
+ export async function runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
137
+ logger.setContext({ iteration, stage: "refactorer" });
138
+ emitProgress(
139
+ emitter,
140
+ makeEvent("refactorer:start", { ...eventBase, stage: "refactorer" }, {
141
+ message: `Refactorer (${refactorerRole.provider}) running`,
142
+ detail: { refactorer: refactorerRole.provider, provider: refactorerRole.provider, executorType: "agent" }
143
+ })
144
+ );
145
+ const refactorerOnOutput = ({ stream, line }) => {
146
+ emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "refactorer" }, {
147
+ message: line,
148
+ detail: { stream, agent: refactorerRole.provider }
149
+ }));
150
+ };
151
+ const refactorerStall = createStallDetector({
152
+ onOutput: refactorerOnOutput, emitter, eventBase, stage: "refactorer", provider: refactorerRole.provider
153
+ });
154
+
155
+ const refRole = new RefactorerRole({ config, logger, emitter, createAgentFn: createAgent });
156
+ await refRole.init();
157
+ const refactorerStart = Date.now();
158
+ let refResult;
159
+ try {
160
+ refResult = await refRole.execute({ task: plannedTask, onOutput: refactorerStall.onOutput });
161
+ } finally {
162
+ refactorerStall.stop();
163
+ }
164
+ trackBudget({ role: "refactorer", provider: refactorerRole.provider, model: refactorerRole.model, result: refResult.result, duration_ms: Date.now() - refactorerStart });
165
+ if (!refResult.ok) {
166
+ const details = refResult.result?.error || refResult.summary || "unknown error";
167
+ const rateLimitCheck = detectRateLimit({
168
+ stderr: refResult.result?.error || "",
169
+ stdout: refResult.result?.output || ""
170
+ });
171
+
172
+ if (rateLimitCheck.isRateLimit) {
173
+ // Enter standby instead of pausing
174
+ return {
175
+ action: "standby",
176
+ standbyInfo: {
177
+ agent: refactorerRole.provider,
178
+ cooldownMs: rateLimitCheck.cooldownMs || (rateLimitCheck.isProviderOutage ? 30000 : null),
179
+ cooldownUntil: rateLimitCheck.cooldownUntil,
180
+ message: rateLimitCheck.message,
181
+ isProviderOutage: rateLimitCheck.isProviderOutage || false
182
+ }
183
+ };
184
+ }
185
+
186
+ await markSessionStatus(session, "failed");
187
+ emitProgress(
188
+ emitter,
189
+ makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
190
+ status: "fail",
191
+ message: `Refactorer failed: ${details}`,
192
+ detail: { provider: refactorerRole.provider, executorType: "agent" }
193
+ })
194
+ );
195
+ throw new Error(`Refactorer failed: ${details}`);
196
+ }
197
+ await addCheckpoint(session, { stage: "refactorer", iteration, note: "Refactorer applied cleanups", provider: refactorerRole.provider, model: refactorerRole.model || null });
198
+ emitProgress(
199
+ emitter,
200
+ makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
201
+ message: "Refactorer completed",
202
+ detail: { provider: refactorerRole.provider, executorType: "agent" }
203
+ })
204
+ );
205
+ }
206
+
207
+ function handleSolomonAction(solomonResult, session, contextPrefix) {
208
+ if (solomonResult.action === "pause") {
209
+ return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: `${contextPrefix}_fail_fast` } };
210
+ }
211
+ if (solomonResult.action === "subtask") {
212
+ return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: `${contextPrefix}_subtask` } };
213
+ }
214
+ return null;
215
+ }
216
+
217
+ async function handleSolomonContinue(solomonResult, session, counterField) {
218
+ if (solomonResult.action !== "continue") return false;
219
+ if (solomonResult.humanGuidance) {
220
+ session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
221
+ }
222
+ session[counterField] = 0;
223
+ await saveSession(session);
224
+ return true;
225
+ }
226
+
227
+ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion }) {
228
+ session.last_reviewer_feedback = tddEval.message;
229
+ session.repeated_issue_count += 1;
230
+ await saveSession(session);
231
+
232
+ if (session.repeated_issue_count < config.session.fail_fast_repeats) {
233
+ return { action: "continue" };
234
+ }
235
+
236
+ emitProgress(
237
+ emitter,
238
+ makeEvent("solomon:escalate", { ...eventBase, stage: "tdd" }, {
239
+ message: `TDD sub-loop limit reached (${session.repeated_issue_count}/${config.session.fail_fast_repeats})`,
240
+ detail: { subloop: "tdd", retryCount: session.repeated_issue_count, reason: tddEval.reason }
241
+ })
242
+ );
243
+
244
+ const solomonResult = await invokeSolomon({
245
+ config, logger, emitter, eventBase, stage: "tdd", askQuestion, session, iteration,
246
+ conflict: {
247
+ stage: "tdd",
248
+ task: session.task,
249
+ iterationCount: session.repeated_issue_count,
250
+ maxIterations: config.session.fail_fast_repeats,
251
+ reason: tddEval.reason,
252
+ sourceFiles: tddEval.sourceFiles,
253
+ testFiles: tddEval.testFiles,
254
+ history: [{ agent: "tdd-policy", feedback: tddEval.message }]
255
+ }
256
+ });
257
+
258
+ const actionResult = handleSolomonAction(solomonResult, session, "tdd");
259
+ if (actionResult) return actionResult;
260
+ const continued = await handleSolomonContinue(solomonResult, session, "repeated_issue_count");
261
+ if (continued) return { action: "continue" };
262
+
263
+ return { action: "continue" };
264
+ }
265
+
266
+ export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion }) {
267
+ logger.setContext({ iteration, stage: "tdd" });
268
+ let tddDiff, untrackedFiles;
269
+ try {
270
+ tddDiff = await generateDiff({ baseRef: session.session_start_sha });
271
+ untrackedFiles = await getUntrackedFiles();
272
+ } catch (err) {
273
+ logger.warn(`TDD diff generation failed: ${err.message}`);
274
+ return { action: "continue", stageResult: { ok: false, summary: `TDD check failed: ${err.message}` } };
275
+ }
276
+ const effectiveTaskType = session.resolved_policies?.taskType || null;
277
+ const tddEval = evaluateTddPolicy(tddDiff, config.development, untrackedFiles, effectiveTaskType);
278
+ await addCheckpoint(session, {
279
+ stage: "tdd-policy",
280
+ iteration,
281
+ ok: tddEval.ok,
282
+ reason: tddEval.reason,
283
+ source_files: tddEval.sourceFiles?.length || 0,
284
+ test_files: tddEval.testFiles?.length || 0
285
+ });
286
+
287
+ emitProgress(
288
+ emitter,
289
+ makeEvent("tdd:result", { ...eventBase, stage: "tdd" }, {
290
+ status: tddEval.ok ? "ok" : "fail",
291
+ message: tddEval.ok ? "TDD policy passed" : `TDD policy failed: ${tddEval.reason}`,
292
+ detail: {
293
+ ok: tddEval.ok,
294
+ reason: tddEval.reason,
295
+ sourceFiles: tddEval.sourceFiles?.length || 0,
296
+ testFiles: tddEval.testFiles?.length || 0,
297
+ executorType: "local"
298
+ }
299
+ })
300
+ );
301
+
302
+ if (!tddEval.ok) {
303
+ return handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion });
304
+ }
305
+
306
+ return { action: "ok" };
307
+ }