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.
@@ -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
- : "Preflight passed — all checks OK",
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(`--- Conflict: ${stage} ---`);
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("Reviewer feedback:");
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(`Solomon could not resolve the conflict: ${solomonReason}`);
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(`Iterations: ${iterationCount}/${maxIterations}`);
55
+ lines.push(msg("pipeline_iteration", lang, { current: iterationCount, max: maxIterations }));
54
56
  lines.push("");
55
57
  }
56
58
 
57
- lines.push("Options:");
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("How should we proceed?");
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
- if (answer) {
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
+ }