karajan-code 1.48.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/agents/resolve-bin.js +1 -1
  3. package/src/audit/basal-cost.js +26 -23
  4. package/src/bootstrap.js +2 -2
  5. package/src/commands/agents.js +1 -1
  6. package/src/commands/board.js +3 -3
  7. package/src/commands/code.js +1 -1
  8. package/src/commands/doctor.js +4 -4
  9. package/src/commands/init.js +2 -2
  10. package/src/commands/report.js +1 -1
  11. package/src/commands/review.js +1 -1
  12. package/src/commands/roles.js +1 -1
  13. package/src/hu/store.js +9 -5
  14. package/src/mcp/handlers/direct-handlers.js +393 -0
  15. package/src/mcp/handlers/hu-handlers.js +93 -0
  16. package/src/mcp/handlers/management-handlers.js +182 -0
  17. package/src/mcp/handlers/run-handler.js +283 -0
  18. package/src/mcp/orphan-guard.js +1 -1
  19. package/src/mcp/server-handlers.js +41 -871
  20. package/src/mcp/suggest-handler.js +2 -2
  21. package/src/orchestrator/hu-sub-pipeline.js +1 -1
  22. package/src/orchestrator/iteration-stages.js +9 -890
  23. package/src/orchestrator/pre-loop-stages.js +8 -906
  24. package/src/orchestrator/preflight-checks.js +13 -19
  25. package/src/orchestrator/stages/architect-stage.js +176 -0
  26. package/src/orchestrator/stages/coder-stage.js +307 -0
  27. package/src/orchestrator/stages/hu-reviewer-stage.js +467 -0
  28. package/src/orchestrator/stages/planner-stage.js +90 -0
  29. package/src/orchestrator/stages/research-stage.js +74 -0
  30. package/src/orchestrator/stages/reviewer-stage.js +350 -0
  31. package/src/orchestrator/stages/sonar-stage.js +267 -0
  32. package/src/orchestrator/stages/triage-stage.js +137 -0
  33. package/src/plugins/loader.js +1 -1
  34. package/src/prompts/architect.js +1 -1
  35. package/src/review/parser.js +3 -3
  36. package/src/roles/architect-role.js +1 -1
  37. package/src/roles/audit-role.js +3 -3
  38. package/src/roles/base-role.js +1 -1
  39. package/src/roles/discover-role.js +1 -1
  40. package/src/roles/hu-reviewer-role.js +1 -1
  41. package/src/roles/researcher-role.js +1 -1
  42. package/src/roles/triage-role.js +1 -1
  43. package/src/session-cleanup.js +1 -1
  44. package/src/session-store.js +2 -2
  45. package/src/skills/openskills-client.js +1 -1
  46. package/src/skills/skill-detector.js +2 -2
  47. package/src/sonar/api.js +4 -3
  48. package/src/sonar/config-resolver.js +69 -0
  49. package/src/sonar/credentials.js +1 -1
  50. package/src/sonar/project-key.js +1 -1
  51. package/src/sonar/scanner.js +13 -21
  52. package/src/utils/fs.js +1 -1
  53. package/src/utils/json-extract.js +1 -1
  54. package/src/utils/project-detect.js +1 -1
  55. package/src/utils/rtk-detect.js +1 -1
@@ -13,16 +13,16 @@ 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 { loadSonarCredentials } from "../sonar/credentials.js";
17
-
18
- function normalizeApiHost(rawHost) {
19
- return String(rawHost || "http://localhost:9000").replace(/host\.docker\.internal/g, "localhost");
20
- }
16
+ import {
17
+ resolveSonarHost,
18
+ resolveSonarToken,
19
+ resolveSonarCredentials,
20
+ } from "../sonar/config-resolver.js";
21
21
 
22
22
  function parseJsonSafe(text) {
23
23
  try {
24
24
  return JSON.parse(text);
25
- } catch {
25
+ } catch { /* invalid JSON */
26
26
  return null;
27
27
  }
28
28
  }
@@ -60,10 +60,10 @@ async function checkSonarReachable(host) {
60
60
  }
61
61
 
62
62
  async function checkSonarAuth(config) {
63
- const host = normalizeApiHost(config.sonarqube?.host);
63
+ const host = resolveSonarHost(config.sonarqube?.host);
64
64
 
65
65
  // Check explicit token first
66
- const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube?.token;
66
+ const explicitToken = resolveSonarToken(config);
67
67
  if (explicitToken) {
68
68
  // Validate the token works
69
69
  const res = await runCommand("curl", [
@@ -77,20 +77,14 @@ async function checkSonarAuth(config) {
77
77
  }
78
78
  }
79
79
 
80
- // Try admin credentials: env vars → config → ~/.karajan/sonar-credentials.json (NO default admin/admin)
81
- const fileCreds = await loadSonarCredentials() || {};
82
- const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube?.admin_user || fileCreds.user;
83
- const candidates = [
84
- process.env.KJ_SONAR_ADMIN_PASSWORD,
85
- config.sonarqube?.admin_password,
86
- fileCreds.password
87
- ].filter(Boolean);
80
+ // Try admin credentials via centralized resolver
81
+ const { user: adminUser, passwords } = await resolveSonarCredentials(config);
88
82
 
89
- if (!adminUser || candidates.length === 0) {
83
+ if (!adminUser || passwords.length === 0) {
90
84
  return { name: "sonar-auth", ok: false, detail: "No Sonar token or admin credentials configured. Set KJ_SONAR_TOKEN, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json." };
91
85
  }
92
86
 
93
- for (const password of [...new Set(candidates)]) {
87
+ for (const password of passwords) {
94
88
  const validateRes = await runCommand("curl", [
95
89
  "-sS", "-u", `${adminUser}:${password}`,
96
90
  `${host}/api/authentication/validate`
@@ -151,7 +145,7 @@ async function checkSecurityAgent(config) {
151
145
  export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
152
146
  const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
153
147
  const isExternalSonar = Boolean(config.sonarqube?.external);
154
- const sonarHost = normalizeApiHost(config.sonarqube?.host);
148
+ const sonarHost = resolveSonarHost(config.sonarqube?.host);
155
149
 
156
150
  const result = {
157
151
  ok: true,
@@ -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
+ }