karajan-code 2.0.0 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -34,14 +34,21 @@ export async function startBoard(port = 4000) {
34
34
  const karajanHome = getKarajanHome();
35
35
  fs.mkdirSync(karajanHome, { recursive: true });
36
36
 
37
- const child = spawn("node", [serverPath], {
37
+ // Use process.execPath (absolute path to current node binary) instead of "node".
38
+ // Fixes ENOENT on nvm setups where `node` is not in the spawned process's PATH.
39
+ const child = spawn(process.execPath, [serverPath], {
38
40
  env: { ...process.env, PORT: String(port) },
39
41
  detached: true,
40
42
  stdio: "ignore",
41
43
  cwd: BOARD_DIR
42
44
  });
45
+ // Swallow spawn errors so an HU Board failure never crashes the pipeline.
46
+ child.on("error", () => { /* non-blocking — caller logs via tryAutoStartBoard catch */ });
43
47
  child.unref();
44
48
 
49
+ if (!child.pid) {
50
+ throw new Error("Failed to spawn HU Board server");
51
+ }
45
52
  fs.writeFileSync(PID_FILE, String(child.pid));
46
53
  return { ok: true, alreadyRunning: false, pid: child.pid, url: `http://localhost:${port}` };
47
54
  }
package/src/config.js CHANGED
@@ -34,8 +34,15 @@ const DEFAULTS = {
34
34
  discover: { enabled: false },
35
35
  architect: { enabled: false },
36
36
  hu_reviewer: { enabled: false },
37
+ brain: { enabled: true },
37
38
  auto_simplify: true
38
39
  },
40
+ brain: {
41
+ enabled: true,
42
+ provider: "claude",
43
+ bypass_solomon_on_correctness: true,
44
+ max_consecutive_verification_failures: 2
45
+ },
39
46
  review_mode: "standard",
40
47
  max_iterations: 5,
41
48
  max_budget_usd: null,
@@ -46,6 +46,9 @@ export class PipelineContext {
46
46
  // Planned task (may differ from original task after planner)
47
47
  this.plannedTask = null;
48
48
 
49
+ // Karajan Brain runtime context (feedback queue, verification tracker, compression stats)
50
+ this.brainCtx = null;
51
+
49
52
  // Timing
50
53
  this.startedAt = null;
51
54
  }
@@ -16,7 +16,7 @@ import { invokeSolomon } from "../solomon-escalation.js";
16
16
  import { detectRateLimit } from "../../utils/rate-limit-detector.js";
17
17
  import { createStallDetector } from "../../utils/stall-detector.js";
18
18
 
19
- export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
19
+ export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration, brainCtx }) {
20
20
  logger.setContext({ iteration, stage: "coder" });
21
21
  emitProgress(
22
22
  emitter,
@@ -26,6 +26,17 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
26
26
  })
27
27
  );
28
28
 
29
+ // Brain: if enabled and queue has entries, use enriched feedback instead of flat string
30
+ let reviewerFeedback = session.last_reviewer_feedback;
31
+ if (brainCtx?.enabled) {
32
+ const { buildCoderFeedbackPrompt } = await import("../brain-coordinator.js");
33
+ const enriched = buildCoderFeedbackPrompt(brainCtx);
34
+ if (enriched) {
35
+ reviewerFeedback = enriched;
36
+ logger.info(`Brain: using enriched feedback (${brainCtx.feedbackQueue.entries.length} entries)`);
37
+ }
38
+ }
39
+
29
40
  const coderOnOutput = ({ stream, line }) => {
30
41
  emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "coder" }, {
31
42
  message: line,
@@ -40,7 +51,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
40
51
  try {
41
52
  coderExecResult = await coderRoleInstance.execute({
42
53
  task: plannedTask,
43
- reviewerFeedback: session.last_reviewer_feedback,
54
+ reviewerFeedback,
44
55
  sonarSummary: session.last_sonar_summary,
45
56
  deferredContext: buildDeferredContext(session.deferred_issues),
46
57
  onOutput: coderStall.onOutput
@@ -123,14 +134,48 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
123
134
  throw new Error(`Coder failed: ${details}`);
124
135
  }
125
136
 
126
- await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null });
137
+ // Measure files changed so stale detection (solomon-rules) has accurate data
138
+ let filesChanged = 0;
139
+ try {
140
+ const { verifyCoderOutput } = await import("../verification-gate.js");
141
+ const verif = verifyCoderOutput({
142
+ baseRef: session.session_start_sha,
143
+ projectDir: config.projectDir || process.cwd()
144
+ });
145
+ filesChanged = verif.filesChanged || 0;
146
+ } catch { /* ignore verification errors */ }
147
+
148
+ await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null, filesChanged });
127
149
  emitProgress(
128
150
  emitter,
129
151
  makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
130
152
  message: "Coder completed",
131
- detail: { provider: coderRole.provider, executorType: "agent" }
153
+ detail: { provider: coderRole.provider, executorType: "agent", filesChanged }
132
154
  })
133
155
  );
156
+
157
+ // Brain: verify coder produced real changes + clear feedback queue (now addressed)
158
+ if (brainCtx?.enabled) {
159
+ const { verifyCoderRan, clearFeedback } = await import("../brain-coordinator.js");
160
+ const result = verifyCoderRan(brainCtx, {
161
+ baseRef: session.session_start_sha,
162
+ projectDir: config.projectDir || process.cwd()
163
+ });
164
+ const maxFailures = config.brain?.max_consecutive_verification_failures ?? 2;
165
+ if (!result.passed) {
166
+ logger.warn(`Brain verification: coder made 0 changes (consecutive failures: ${brainCtx.verificationTracker.consecutiveFailures}/${maxFailures})`);
167
+ emitProgress(emitter, makeEvent("brain:verification", { ...eventBase, stage: "coder" }, {
168
+ message: `Brain: coder produced no changes (${brainCtx.verificationTracker.consecutiveFailures}/${maxFailures} consecutive failures)`,
169
+ detail: { filesChanged: result.filesChanged, consecutiveFailures: brainCtx.verificationTracker.consecutiveFailures }
170
+ }));
171
+ if (brainCtx.verificationTracker.consecutiveFailures >= maxFailures) {
172
+ await markSessionStatus(session, "stalled");
173
+ throw new Error(`Brain: ${maxFailures} consecutive coder iterations with 0 file changes — pipeline stalled`);
174
+ }
175
+ } else {
176
+ clearFeedback(brainCtx);
177
+ }
178
+ }
134
179
  }
135
180
 
136
181
  export async function runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
@@ -117,12 +117,38 @@ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState,
117
117
  return { review, stalled: true, stalledResult: { approved: false, sessionId: session.id, reason: "stalled" } };
118
118
  }
119
119
 
120
- async function handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary }) {
120
+ async function handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, brainCtx }) {
121
121
  repeatDetector.addIteration([], review.blocking_issues);
122
122
  const repeatState = repeatDetector.isStalled();
123
123
 
124
124
  const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
125
125
 
126
+ // Brain: if enabled and bypass_solomon_on_correctness, push issues to queue and skip Solomon
127
+ // for non-style rejections. Solomon is still consulted on genuine dilemmas (style-only, deadlock).
128
+ if (brainCtx?.enabled && config.brain?.bypass_solomon_on_correctness !== false) {
129
+ const cats = categorizeIssues(review.blocking_issues);
130
+ const nonStyleIssues = cats.security + cats.correctness + cats.tests;
131
+ const styleIssues = cats.style;
132
+ // Only bypass if there ARE non-style issues (correctness, tests, security, or "other" is correctness)
133
+ // If it's style-only, let Solomon evaluate whether to override
134
+ if (nonStyleIssues > 0 || cats.other > 0) {
135
+ const { processRoleOutput } = await import("../brain-coordinator.js");
136
+ processRoleOutput(brainCtx, { roleName: "reviewer", output: review, iteration });
137
+ logger.info(`Brain: reviewer rejected with ${nonStyleIssues} non-style issue(s) — bypassing Solomon, returning to coder`);
138
+ emitProgress(emitter, makeEvent("brain:bypass-solomon", { ...eventBase, stage: "reviewer" }, {
139
+ message: `Brain: ${nonStyleIssues} non-style issue(s) — sending to coder without Solomon`,
140
+ detail: { categories: cats, queueSize: brainCtx.feedbackQueue.entries.length }
141
+ }));
142
+ // Persist flat feedback for compat with non-Brain flow
143
+ session.last_reviewer_feedback = review.blocking_issues
144
+ .map((x, idx) => `R-${idx + 1} [${x.severity || "medium"}]: ${x.description}`)
145
+ .join("\n");
146
+ await saveSession(session);
147
+ return null; // null = continue to next iteration (coder fixes)
148
+ }
149
+ logger.info("Brain: reviewer rejected with style-only issues — consulting Solomon for dilemma");
150
+ }
151
+
126
152
  // When Solomon is disabled, only act on stalls (legacy behavior)
127
153
  if (!solomonEnabled) {
128
154
  if (!repeatState.stalled) return null;
@@ -159,7 +185,7 @@ export async function fetchReviewDiff(session, logger) {
159
185
  return generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
160
186
  }
161
187
 
162
- export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
188
+ export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
163
189
  logger.setContext({ iteration, stage: "reviewer" });
164
190
  emitProgress(
165
191
  emitter,
@@ -326,13 +352,19 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
326
352
  });
327
353
  }
328
354
 
355
+ // Feedback signature for stale detection: concatenated issue descriptions
356
+ const feedbackSignature = review.blocking_issues
357
+ .map(x => (x.description || "").slice(0, 120))
358
+ .join("|") || "approved";
359
+
329
360
  await addCheckpoint(session, {
330
361
  stage: "reviewer",
331
362
  iteration,
332
363
  approved: review.approved,
333
364
  blocking_issues: review.blocking_issues.length,
334
365
  provider: reviewerRole.provider,
335
- model: reviewerRole.model || null
366
+ model: reviewerRole.model || null,
367
+ note: feedbackSignature
336
368
  });
337
369
 
338
370
  emitProgress(
@@ -353,7 +385,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
353
385
  );
354
386
 
355
387
  if (!review.approved) {
356
- const rejectionResult = await handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary });
388
+ const rejectionResult = await handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, brainCtx });
357
389
  if (rejectionResult) return rejectionResult;
358
390
  }
359
391
 
@@ -210,7 +210,7 @@ function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
210
210
  }
211
211
  }
212
212
 
213
- async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues }) {
213
+ async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
214
214
  if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
215
215
 
216
216
  try {
@@ -220,8 +220,45 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
220
220
 
221
221
  if (rulesResult.alerts.length > 0) {
222
222
  emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
223
- const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
224
- if (pauseResult) return pauseResult;
223
+ // Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
224
+ // On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
225
+ // resolve does Brain escalate to human. Solomon-rules never prompts directly.
226
+ if (!brainCtx?.enabled) {
227
+ const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
228
+ if (pauseResult) return pauseResult;
229
+ } else if (rulesResult.hasCritical) {
230
+ const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
231
+ brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
232
+ brainCtx.ruleAlerts.push(...criticalAlerts);
233
+ logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
234
+
235
+ const { invokeSolomon } = await import("./orchestrator/solomon-escalation.js");
236
+ const alertSummary = criticalAlerts.map(a => a.message).join("; ");
237
+ const solomonOpinion = await invokeSolomon({
238
+ config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
239
+ conflict: {
240
+ stage: "brain-dilemma",
241
+ task,
242
+ iterationCount: i,
243
+ maxIterations: config.max_iterations,
244
+ dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
245
+ ruleAlerts: criticalAlerts,
246
+ blockingIssues: blockingIssues || [],
247
+ history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
248
+ }
249
+ });
250
+
251
+ if (solomonOpinion.action === "pause") {
252
+ logger.info("Brain: Solomon advised pause — escalating to human");
253
+ return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
254
+ }
255
+ if (solomonOpinion.action === "approve") {
256
+ logger.info("Brain: Solomon advised proceeding — treating as approved");
257
+ return { action: "continue", approved: true };
258
+ }
259
+ // action === "continue" | "subtask" | fallback → Brain continues the loop
260
+ logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
261
+ }
225
262
  }
226
263
 
227
264
  if (ciEnabled && session.ci_pr_number) {
@@ -661,8 +698,8 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
661
698
  return { plannedTask, updatedConfig };
662
699
  }
663
700
 
664
- async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
665
- const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
701
+ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
702
+ const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
666
703
  if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
667
704
  const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
668
705
  if (coderStandby.handled) {
@@ -795,7 +832,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
795
832
  return { action: "ok" };
796
833
  }
797
834
 
798
- async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
835
+ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
799
836
  if (!pipelineFlags.reviewerEnabled) {
800
837
  return {
801
838
  action: "ok",
@@ -805,7 +842,7 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
805
842
 
806
843
  const reviewerResult = await runReviewerStage({
807
844
  reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
808
- iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
845
+ iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx
809
846
  });
810
847
  if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
811
848
  const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
@@ -1014,6 +1051,13 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1014
1051
  ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
1015
1052
  ctx.eventBase.sessionId = ctx.session.id;
1016
1053
 
1054
+ // Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
1055
+ const { createBrainContext, isBrainEnabled } = await import("./orchestrator/brain-coordinator.js");
1056
+ ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
1057
+ if (ctx.brainCtx.enabled) {
1058
+ logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
1059
+ }
1060
+
1017
1061
  const { initPgAdapter } = await import("./planning-game/pipeline-adapter.js");
1018
1062
  const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
1019
1063
  ctx.pgCard = pgAdapterResult.pgCard;
@@ -1094,7 +1138,7 @@ async function runSingleIteration(ctx) {
1094
1138
  const crResult = await runCoderAndRefactorerStages({
1095
1139
  coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
1096
1140
  pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
1097
- plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i
1141
+ plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i, brainCtx: ctx.brainCtx
1098
1142
  });
1099
1143
  if (crResult.action === "return" || crResult.action === "retry") return crResult;
1100
1144
 
@@ -1117,7 +1161,8 @@ async function runSingleIteration(ctx) {
1117
1161
  const revResult = await runReviewerGateStage({
1118
1162
  pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
1119
1163
  session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
1120
- repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion
1164
+ repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion,
1165
+ brainCtx: ctx.brainCtx
1121
1166
  });
1122
1167
  if (revResult.action === "return" || revResult.action === "retry") return revResult;
1123
1168
  const review = revResult.review;
@@ -1143,7 +1188,7 @@ async function runSingleIteration(ctx) {
1143
1188
 
1144
1189
  const solomonResult = await handleSolomonCheck({
1145
1190
  config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
1146
- ciEnabled, blockingIssues: review?.blocking_issues
1191
+ ciEnabled, blockingIssues: review?.blocking_issues, brainCtx: ctx.brainCtx
1147
1192
  });
1148
1193
  if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
1149
1194