karajan-code 1.49.0 → 1.50.1
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/README.md +1 -1
- package/docs/README.es.md +1 -1
- package/package.json +1 -1
- package/src/mcp/handlers/direct-handlers.js +393 -0
- package/src/mcp/handlers/hu-handlers.js +93 -0
- package/src/mcp/handlers/management-handlers.js +182 -0
- package/src/mcp/handlers/run-handler.js +283 -0
- package/src/mcp/server-handlers.js +40 -870
- package/src/orchestrator/iteration-stages.js +9 -890
- package/src/orchestrator/pre-loop-stages.js +8 -906
- package/src/orchestrator/preflight-checks.js +3 -1
- package/src/orchestrator/solomon-escalation.js +34 -10
- package/src/orchestrator/stages/architect-stage.js +176 -0
- package/src/orchestrator/stages/coder-stage.js +307 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
- package/src/orchestrator/stages/planner-stage.js +90 -0
- package/src/orchestrator/stages/research-stage.js +74 -0
- package/src/orchestrator/stages/reviewer-stage.js +350 -0
- package/src/orchestrator/stages/sonar-stage.js +267 -0
- package/src/orchestrator/stages/triage-stage.js +137 -0
- package/src/orchestrator.js +25 -9
- package/src/planning-game/decomposition.js +7 -5
- package/src/planning-game/pipeline-adapter.js +6 -2
- package/src/utils/messages.js +61 -0
|
@@ -13,6 +13,7 @@ import { checkBinary } from "../utils/agent-detect.js";
|
|
|
13
13
|
import { isSonarReachable, sonarUp } from "../sonar/manager.js";
|
|
14
14
|
import { runCommand } from "../utils/process.js";
|
|
15
15
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
16
|
+
import { msg, getLang } from "../utils/messages.js";
|
|
16
17
|
import {
|
|
17
18
|
resolveSonarHost,
|
|
18
19
|
resolveSonarToken,
|
|
@@ -273,13 +274,14 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
273
274
|
|
|
274
275
|
const hasErrors = result.errors.length > 0;
|
|
275
276
|
const hasWarnings = result.warnings.length > 0;
|
|
277
|
+
const preflightLang = getLang(config);
|
|
276
278
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
277
279
|
status: hasErrors ? "fail" : hasWarnings ? "warn" : "ok",
|
|
278
280
|
message: hasErrors
|
|
279
281
|
? `Preflight FAILED — ${result.errors.length} blocking issue(s)`
|
|
280
282
|
: hasWarnings
|
|
281
283
|
? `Preflight completed with ${result.warnings.length} warning(s)`
|
|
282
|
-
: "
|
|
284
|
+
: msg("preflight_passed", preflightLang),
|
|
283
285
|
detail: { ...result, executorType: "local" }
|
|
284
286
|
}));
|
|
285
287
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SolomonRole } from "../roles/solomon-role.js";
|
|
2
2
|
import { addCheckpoint, pauseSession, saveSession } from "../session-store.js";
|
|
3
3
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
4
|
+
import { msg, getLang } from "../utils/messages.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Build a human-readable escalation message from raw Solomon conflict data.
|
|
@@ -14,15 +15,16 @@ export function formatEscalationMessage(solomonResult, session) {
|
|
|
14
15
|
const stage = solomonResult?.stage || "unknown";
|
|
15
16
|
const solomonReason = solomonResult?.solomonReason || null;
|
|
16
17
|
const history = solomonResult?.history || [];
|
|
18
|
+
const lang = getLang(session?.config_snapshot);
|
|
17
19
|
|
|
18
20
|
const lines = [];
|
|
19
|
-
lines.push(
|
|
21
|
+
lines.push(msg("solomon_conflict", lang, { stage }));
|
|
20
22
|
lines.push("");
|
|
21
23
|
|
|
22
24
|
// Extract reviewer/agent feedback from history
|
|
23
25
|
const feedbackEntries = history.filter(entry => entry && typeof entry === "object");
|
|
24
26
|
if (feedbackEntries.length > 0) {
|
|
25
|
-
lines.push("
|
|
27
|
+
lines.push(msg("solomon_feedback", lang));
|
|
26
28
|
for (const entry of feedbackEntries) {
|
|
27
29
|
const agent = entry.agent || "unknown";
|
|
28
30
|
const feedback = entry.feedback || "No feedback provided";
|
|
@@ -42,7 +44,7 @@ export function formatEscalationMessage(solomonResult, session) {
|
|
|
42
44
|
|
|
43
45
|
// Solomon's reason (if it tried and failed)
|
|
44
46
|
if (solomonReason) {
|
|
45
|
-
lines.push(
|
|
47
|
+
lines.push(`${msg("solomon_reason", lang)} ${solomonReason}`);
|
|
46
48
|
lines.push("");
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -50,16 +52,13 @@ export function formatEscalationMessage(solomonResult, session) {
|
|
|
50
52
|
const iterationCount = solomonResult?.iterationCount;
|
|
51
53
|
const maxIterations = solomonResult?.maxIterations;
|
|
52
54
|
if (iterationCount !== undefined && maxIterations !== undefined) {
|
|
53
|
-
lines.push(
|
|
55
|
+
lines.push(msg("pipeline_iteration", lang, { current: iterationCount, max: maxIterations }));
|
|
54
56
|
lines.push("");
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
lines.push("
|
|
58
|
-
lines.push(" 1. Accept coder's work as-is");
|
|
59
|
-
lines.push(" 2. Retry with reviewer's feedback");
|
|
60
|
-
lines.push(" 3. Stop the session");
|
|
59
|
+
lines.push(msg("solomon_options", lang));
|
|
61
60
|
lines.push("");
|
|
62
|
-
lines.push("
|
|
61
|
+
lines.push(msg("solomon_proceed", lang));
|
|
63
62
|
|
|
64
63
|
return lines.join("\n");
|
|
65
64
|
}
|
|
@@ -161,12 +160,37 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
161
160
|
return { action: "continue", conditions: [], ruling };
|
|
162
161
|
}
|
|
163
162
|
|
|
163
|
+
export function parseEscalationAnswer(answer) {
|
|
164
|
+
if (!answer) return null;
|
|
165
|
+
const trimmed = answer.trim().toLowerCase();
|
|
166
|
+
// Option 1: Accept coder's work as-is
|
|
167
|
+
if (trimmed === "1" || trimmed === "accept" || trimmed === "yes" || trimmed === "sí" || trimmed === "si") {
|
|
168
|
+
return { action: "continue", humanGuidance: "Accept coder's work as-is" };
|
|
169
|
+
}
|
|
170
|
+
// Option 2: Retry with reviewer's feedback
|
|
171
|
+
if (trimmed === "2" || trimmed === "retry") {
|
|
172
|
+
return { action: "continue", humanGuidance: "Retry with reviewer feedback" };
|
|
173
|
+
}
|
|
174
|
+
// Option 3: Stop the session
|
|
175
|
+
if (trimmed === "3" || trimmed.startsWith("stop") || trimmed.startsWith("pause")) {
|
|
176
|
+
return { action: "stop" };
|
|
177
|
+
}
|
|
178
|
+
// Free-text: treat as human guidance (backward compat)
|
|
179
|
+
return { action: "continue", humanGuidance: answer.trim() };
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
export async function escalateToHuman({ askQuestion, session, emitter, eventBase, stage, conflict, iteration }) {
|
|
165
183
|
const question = formatEscalationMessage({ ...conflict, stage }, session);
|
|
166
184
|
|
|
167
185
|
if (askQuestion) {
|
|
168
186
|
const answer = await askQuestion(question, { iteration, stage });
|
|
169
|
-
|
|
187
|
+
const parsed = parseEscalationAnswer(answer);
|
|
188
|
+
if (parsed && parsed.action === "continue") {
|
|
189
|
+
return { action: "continue", humanGuidance: parsed.humanGuidance };
|
|
190
|
+
}
|
|
191
|
+
if (parsed && parsed.action === "stop") {
|
|
192
|
+
// Fall through to pause session
|
|
193
|
+
} else if (answer) {
|
|
170
194
|
return { action: "continue", humanGuidance: answer };
|
|
171
195
|
}
|
|
172
196
|
}
|
|
@@ -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
|
+
}
|