karajan-code 1.59.1 → 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.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +1 -4
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +2 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/board.js +8 -1
  14. package/src/commands/doctor.js +22 -40
  15. package/src/commands/init.js +8 -8
  16. package/src/commands/report.js +0 -13
  17. package/src/commands/review.js +17 -17
  18. package/src/commands/run.js +1 -2
  19. package/src/commands/status.js +1 -7
  20. package/src/config.js +17 -52
  21. package/src/git/automation.js +9 -9
  22. package/src/guards/output-guard.js +0 -17
  23. package/src/mcp/handlers/direct-handlers.js +1 -1
  24. package/src/mcp/handlers/run-handler.js +1 -2
  25. package/src/mcp/sovereignty-guard.js +1 -1
  26. package/src/mcp/tools.js +1 -1
  27. package/src/orchestrator/brain-coordinator.js +165 -0
  28. package/src/orchestrator/ci-integration.js +139 -0
  29. package/src/orchestrator/config-init.js +435 -0
  30. package/src/orchestrator/direct-actions.js +186 -0
  31. package/src/orchestrator/feedback-enrichment.js +140 -0
  32. package/src/orchestrator/feedback-queue.js +152 -0
  33. package/src/orchestrator/flow-control.js +159 -0
  34. package/src/orchestrator/pipeline-context.js +3 -0
  35. package/src/orchestrator/post-loop-stages.js +20 -15
  36. package/src/orchestrator/preflight-checks.js +7 -3
  37. package/src/orchestrator/role-output-compressor.js +190 -0
  38. package/src/orchestrator/session-journal.js +344 -0
  39. package/src/orchestrator/solomon-escalation.js +38 -5
  40. package/src/orchestrator/solomon-rules.js +32 -33
  41. package/src/orchestrator/stages/coder-stage.js +49 -4
  42. package/src/orchestrator/stages/reviewer-stage.js +52 -9
  43. package/src/orchestrator/stages/sonar-stage.js +24 -1
  44. package/src/orchestrator/verification-gate.js +146 -0
  45. package/src/orchestrator.js +481 -672
  46. package/src/planning-game/client.js +2 -2
  47. package/src/planning-game/pipeline-adapter.js +1 -1
  48. package/src/prompts/coder.js +17 -3
  49. package/src/prompts/reviewer.js +2 -2
  50. package/src/prompts/rtk-snippet.js +3 -5
  51. package/src/review/diff-generator.js +66 -17
  52. package/src/review/snapshot-diff.js +122 -0
  53. package/src/review/tdd-policy.js +4 -2
  54. package/src/roles/agent-role.js +135 -0
  55. package/src/roles/architect-role.js +61 -108
  56. package/src/roles/audit-role.js +25 -71
  57. package/src/roles/coder-role.js +32 -57
  58. package/src/roles/discover-role.js +30 -71
  59. package/src/roles/hu-reviewer-role.js +51 -99
  60. package/src/roles/impeccable-role.js +33 -88
  61. package/src/roles/index.js +2 -0
  62. package/src/roles/karajan-brain-role.js +134 -0
  63. package/src/roles/planner-role.js +40 -75
  64. package/src/roles/refactorer-role.js +22 -64
  65. package/src/roles/researcher-role.js +51 -115
  66. package/src/roles/reviewer-role.js +68 -125
  67. package/src/roles/security-role.js +38 -102
  68. package/src/roles/solomon-role.js +69 -11
  69. package/src/roles/tester-role.js +121 -95
  70. package/src/roles/triage-role.js +50 -111
  71. package/src/session-store.js +1 -1
  72. package/src/skills/skill-detector.js +11 -0
  73. package/src/sonar/config-resolver.js +20 -1
  74. package/src/sonar/credentials.js +17 -0
  75. package/src/sonar/project-key.js +1 -1
  76. package/src/sonar/scanner.js +1 -1
  77. package/src/utils/display.js +33 -14
  78. package/src/utils/injection-guard.js +23 -8
  79. package/src/utils/logger.js +6 -1
  80. package/src/utils/process.js +3 -4
  81. package/src/utils/role-assigner.js +131 -0
  82. package/src/utils/update-check.js +5 -1
  83. package/src/webperf/devtools-detect.js +70 -23
  84. package/templates/roles/coder.md +7 -0
  85. package/templates/roles/karajan-brain.md +95 -0
  86. package/templates/roles/solomon.md +67 -106
  87. package/templates/workflows/automerge.yml +4 -4
  88. package/templates/workflows/houston-override.yml +4 -4
  89. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  90. package/src/proxy/adapters/anthropic.js +0 -131
  91. package/src/proxy/adapters/gemini.js +0 -106
  92. package/src/proxy/adapters/index.js +0 -51
  93. package/src/proxy/adapters/openai.js +0 -71
  94. package/src/proxy/compression/ai-compressor.js +0 -209
  95. package/src/proxy/compression/dedup-cache.js +0 -100
  96. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  97. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  98. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  99. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  100. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  101. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  102. package/src/proxy/compression/deterministic/glob.js +0 -26
  103. package/src/proxy/compression/deterministic/grep.js +0 -53
  104. package/src/proxy/compression/deterministic/index.js +0 -58
  105. package/src/proxy/compression/deterministic/read.js +0 -22
  106. package/src/proxy/compression/deterministic/utils.js +0 -33
  107. package/src/proxy/compression/pipeline.js +0 -270
  108. package/src/proxy/middleware/prompt-injector.js +0 -165
  109. package/src/proxy/middleware/response-interceptor.js +0 -223
  110. package/src/proxy/progress-monitor.js +0 -98
  111. package/src/proxy/proxy-lifecycle.js +0 -201
  112. package/src/proxy/proxy-server.js +0 -239
  113. package/src/proxy/security.js +0 -45
  114. /package/src/{becaria → ci}/index.js +0 -0
  115. /package/src/{becaria → ci}/repo.js +0 -0
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  const DEFAULT_RULES = {
7
- max_files_per_iteration: 10,
8
7
  max_stale_iterations: 3,
9
8
  no_new_dependencies_without_task: true,
10
9
  scope_guard: true,
@@ -16,17 +15,7 @@ export function evaluateRules(context, rulesConfig = {}) {
16
15
  const rules = { ...DEFAULT_RULES, ...rulesConfig };
17
16
  const alerts = [];
18
17
 
19
- // Rule 1: Too many files modified
20
- if (rules.max_files_per_iteration && context.filesChanged > rules.max_files_per_iteration) {
21
- alerts.push({
22
- rule: "max_files_per_iteration",
23
- severity: "critical",
24
- message: `Coder modified ${context.filesChanged} files (limit: ${rules.max_files_per_iteration}). Possible scope drift.`,
25
- detail: { filesChanged: context.filesChanged, limit: rules.max_files_per_iteration }
26
- });
27
- }
28
-
29
- // Rule 2: Stale iterations (no progress)
18
+ // Rule 1: Stale iterations (no progress)
30
19
  if (rules.max_stale_iterations && context.staleIterations >= rules.max_stale_iterations) {
31
20
  alerts.push({
32
21
  rule: "max_stale_iterations",
@@ -36,11 +25,15 @@ export function evaluateRules(context, rulesConfig = {}) {
36
25
  });
37
26
  }
38
27
 
39
- // Rule 3: New dependencies not in task
28
+ // Rule 3: New dependencies not in task (only flag suspicious runtime deps, not dev/test tooling)
40
29
  if (rules.no_new_dependencies_without_task && context.newDependencies?.length > 0) {
41
- const depsNotInTask = context.newDependencies.filter(
42
- dep => !context.task?.toLowerCase().includes(dep.toLowerCase())
43
- );
30
+ const testTooling = /vitest|jest|mocha|coverage|eslint|prettier|typescript|@types\//i;
31
+ const buildTooling = /webpack|vite|rollup|esbuild|tsup|unbuild/i;
32
+ const depsNotInTask = context.newDependencies.filter(dep => {
33
+ if (testTooling.test(dep) || buildTooling.test(dep)) return false;
34
+ if (dep.startsWith("dev:") || dep.startsWith("@vitest/") || dep.startsWith("@types/")) return false;
35
+ return !context.task?.toLowerCase().includes(dep.toLowerCase());
36
+ });
44
37
  if (depsNotInTask.length > 0) {
45
38
  alerts.push({
46
39
  rule: "no_new_dependencies_without_task",
@@ -135,10 +128,16 @@ export async function buildRulesContext({ session, task, iteration, blockingIssu
135
128
  context.filesChanged = files.length;
136
129
 
137
130
  // Detect scope: config files, CI/CD, etc. that are often out of scope
138
- const scopePatterns = [".github/", ".gitlab-ci", "docker-compose", ".env", "firebase.json", "firestore.rules"];
139
- context.outOfScopeFiles = files.filter(f =>
140
- scopePatterns.some(pattern => f.includes(pattern))
141
- );
131
+ // Exclude .env.example (legitimate for new projects) and files within projectDir
132
+ const scopePatterns = [".github/", ".gitlab-ci", "docker-compose", "firebase.json", "firestore.rules"];
133
+ const projectDir = session?.config_snapshot?.projectDir || null;
134
+ context.outOfScopeFiles = files.filter(f => {
135
+ // Files inside projectDir are always in scope
136
+ if (projectDir && f.startsWith(projectDir.replace(/^\.\//, ""))) return false;
137
+ // .env (but not .env.example) is out of scope
138
+ if (f.endsWith(".env") && !f.endsWith(".env.example")) return true;
139
+ return scopePatterns.some(pattern => f.includes(pattern));
140
+ });
142
141
 
143
142
  // Detect new dependencies
144
143
  if (files.includes("package.json")) {
@@ -159,21 +158,21 @@ export async function buildRulesContext({ session, task, iteration, blockingIssu
159
158
  } catch { /* git not available */ }
160
159
 
161
160
  // Count stale iterations from session checkpoints
161
+ // "Stale" means TRULY no progress: same exact feedback AND zero files changed
162
162
  const checkpoints = session.checkpoints || [];
163
- const recentCoderCheckpoints = checkpoints
164
- .filter(cp => cp.stage === "coder" || cp.stage === "reviewer")
165
- .slice(-6); // Last 3 iterations (coder+reviewer each)
166
-
167
- // Simple heuristic: if last N reviewer checkpoints all have the same note/feedback, it's stale
168
- if (recentCoderCheckpoints.length >= 4) {
169
- const lastFeedbacks = checkpoints
170
- .filter(cp => cp.stage === "reviewer")
171
- .slice(-3)
172
- .map(cp => cp.note || "");
173
- const uniqueFeedbacks = new Set(lastFeedbacks);
174
- if (uniqueFeedbacks.size === 1 && lastFeedbacks.length >= 2) {
175
- context.staleIterations = lastFeedbacks.length;
163
+ const reviewerCps = checkpoints.filter(cp => cp.stage === "reviewer").slice(-3);
164
+ const coderCps = checkpoints.filter(cp => cp.stage === "coder").slice(-3);
165
+
166
+ if (reviewerCps.length >= 3) {
167
+ const feedbacks = reviewerCps.map(cp => cp.note || "");
168
+ const uniqueFeedbacks = new Set(feedbacks);
169
+ const filesChanged = coderCps.some(cp => (cp.filesChanged || 0) > 0);
170
+
171
+ // Only stale if feedback is identical AND no files changed
172
+ if (uniqueFeedbacks.size === 1 && !filesChanged) {
173
+ context.staleIterations = reviewerCps.length;
176
174
  }
175
+ // If issues are changing between iterations, there IS progress
177
176
  }
178
177
 
179
178
  return context;
@@ -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 }) {
@@ -15,7 +15,7 @@ import { createStallDetector } from "../../utils/stall-detector.js";
15
15
 
16
16
  function categorizeIssues(issues) {
17
17
  const categories = { security: 0, correctness: 0, tests: 0, style: 0, other: 0 };
18
- const securityKw = /inject|xss|csrf|secret|credential|auth|vulnerab|exploit/i;
18
+ const securityKw = /inject(?:ion)?|xss|csrf|secret.?leak|credential.?expos|auth(?:entication|orization).?(?:bypass|fail|miss|broken|weak)|vulnerab|exploit|httponly|token.?expos/i;
19
19
  const styleKw = /naming|name|rename|style|format|indent|spacing|convention|cosmetic|readability|comment|jsdoc|whitespace/i;
20
20
  const testKw = /test|coverage|assert|spec|mock/i;
21
21
 
@@ -37,6 +37,17 @@ function buildReviewHistory(session) {
37
37
  }
38
38
 
39
39
  async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, repeatDetector }) {
40
+ // DETERMINISTIC GUARD: security issues NEVER go to Solomon — always return to coder
41
+ const categories = categorizeIssues(review.blocking_issues);
42
+ if (categories.security > 0) {
43
+ logger.info(`Reviewer found ${categories.security} security issue(s) — returning to coder (Solomon bypassed)`);
44
+ emitProgress(emitter, makeEvent("reviewer:security-block", { ...eventBase, stage: "reviewer" }, {
45
+ message: `${categories.security} security issue(s) detected — must be fixed before approval`,
46
+ detail: { securityCount: categories.security, blockingIssues: review.blocking_issues }
47
+ }));
48
+ return { review, solomonApproved: false };
49
+ }
50
+
40
51
  const logPrefix = repeatState.stalled
41
52
  ? `Reviewer stalled (${repeatCounts.reviewer} repeats)`
42
53
  : `Reviewer rejected (first rejection)`;
@@ -106,12 +117,38 @@ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState,
106
117
  return { review, stalled: true, stalledResult: { approved: false, sessionId: session.id, reason: "stalled" } };
107
118
  }
108
119
 
109
- 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 }) {
110
121
  repeatDetector.addIteration([], review.blocking_issues);
111
122
  const repeatState = repeatDetector.isStalled();
112
123
 
113
124
  const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
114
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
+
115
152
  // When Solomon is disabled, only act on stalls (legacy behavior)
116
153
  if (!solomonEnabled) {
117
154
  if (!repeatState.stalled) return null;
@@ -139,16 +176,16 @@ async function handleReviewerRejection({ review, repeatDetector, config, logger,
139
176
  }
140
177
 
141
178
  export async function fetchReviewDiff(session, logger) {
142
- if (session.becaria_pr_number) {
143
- const { getPrDiff } = await import("../../becaria/pr-diff.js");
144
- const diff = await getPrDiff(session.becaria_pr_number);
145
- logger.info(`Reviewer reading PR diff #${session.becaria_pr_number}`);
179
+ if (session.ci_pr_number) {
180
+ const { getPrDiff } = await import("../../ci/pr-diff.js");
181
+ const diff = await getPrDiff(session.ci_pr_number);
182
+ logger.info(`Reviewer reading PR diff #${session.ci_pr_number}`);
146
183
  return diff;
147
184
  }
148
185
  return generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
149
186
  }
150
187
 
151
- 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 }) {
152
189
  logger.setContext({ iteration, stage: "reviewer" });
153
190
  emitProgress(
154
191
  emitter,
@@ -315,13 +352,19 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
315
352
  });
316
353
  }
317
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
+
318
360
  await addCheckpoint(session, {
319
361
  stage: "reviewer",
320
362
  iteration,
321
363
  approved: review.approved,
322
364
  blocking_issues: review.blocking_issues.length,
323
365
  provider: reviewerRole.provider,
324
- model: reviewerRole.model || null
366
+ model: reviewerRole.model || null,
367
+ note: feedbackSignature
325
368
  });
326
369
 
327
370
  emitProgress(
@@ -342,7 +385,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
342
385
  );
343
386
 
344
387
  if (!review.approved) {
345
- 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 });
346
389
  if (rejectionResult) return rejectionResult;
347
390
  }
348
391
 
@@ -162,7 +162,6 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
162
162
  ? "SonarQube is running but no authentication token is configured. Fix: run 'kj init' to configure it, or set KJ_SONAR_TOKEN env var, or add sonarqube.token to ~/.karajan/kj.config.yml."
163
163
  : `Sonar scan failed: ${sonarResult.error}`;
164
164
 
165
- await markSessionStatus(session, "failed");
166
165
  emitProgress(
167
166
  emitter,
168
167
  makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
@@ -170,6 +169,30 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
170
169
  message: errorMessage
171
170
  })
172
171
  );
172
+
173
+ // Let Solomon decide: continue without sonar or stop
174
+ const solomonResult = await invokeSolomon({
175
+ config, logger, emitter, eventBase, stage: "sonar_error", askQuestion, session, iteration,
176
+ conflict: {
177
+ stage: "sonar_error",
178
+ task: session.task,
179
+ iterationCount: iteration,
180
+ maxIterations: config.max_iterations,
181
+ history: [{ agent: "sonar", feedback: errorMessage }]
182
+ }
183
+ });
184
+
185
+ if (solomonResult.action === "approve" || solomonResult.action === "continue") {
186
+ logger.info(`Solomon decided to continue without SonarQube: ${solomonResult.ruling?.result?.conditions?.join(", ") || "no conditions"}`);
187
+ return { action: "continue" };
188
+ }
189
+
190
+ if (solomonResult.action === "pause") {
191
+ return { action: "return", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "sonar_error" } };
192
+ }
193
+
194
+ // Solomon couldn't resolve — fail
195
+ await markSessionStatus(session, "failed");
173
196
  throw new Error(errorMessage);
174
197
  }
175
198
 
@@ -0,0 +1,146 @@
1
+ // Verification gate: checks whether the coder actually produced changes.
2
+ // Prevents advancing the pipeline when coder iterations produce 0 file changes.
3
+
4
+ import { execSync } from "node:child_process";
5
+
6
+ /**
7
+ * @typedef {Object} VerificationResult
8
+ * @property {boolean} passed - true if coder produced real changes
9
+ * @property {number} filesChanged - count of modified/added/deleted files
10
+ * @property {number} linesChanged - total lines added/deleted
11
+ * @property {string[]} files - list of changed file paths
12
+ * @property {string} reason - explanation when not passed
13
+ * @property {string} [retryStrategy] - suggested retry approach when not passed
14
+ */
15
+
16
+ /**
17
+ * Count files and lines changed since a base reference.
18
+ * Uses git diff --numstat to get both file and line counts.
19
+ */
20
+ export function countChangesSince(baseRef, projectDir = null) {
21
+ try {
22
+ const scope = projectDir ? `-- ${projectDir}` : "";
23
+ const output = execSync(
24
+ `git diff --numstat ${baseRef} ${scope}`,
25
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
26
+ ).trim();
27
+
28
+ if (!output) return { files: [], filesChanged: 0, linesAdded: 0, linesDeleted: 0 };
29
+
30
+ const lines = output.split("\n");
31
+ const files = [];
32
+ let linesAdded = 0;
33
+ let linesDeleted = 0;
34
+
35
+ for (const line of lines) {
36
+ const [added, deleted, file] = line.split("\t");
37
+ if (!file) continue;
38
+ files.push(file);
39
+ linesAdded += Number(added) || 0;
40
+ linesDeleted += Number(deleted) || 0;
41
+ }
42
+
43
+ return { files, filesChanged: files.length, linesAdded, linesDeleted };
44
+ } catch {
45
+ return { files: [], filesChanged: 0, linesAdded: 0, linesDeleted: 0 };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Also count untracked files (new files not yet added to git).
51
+ */
52
+ export function countUntrackedFiles(projectDir = null) {
53
+ try {
54
+ const scope = projectDir || ".";
55
+ const output = execSync(
56
+ `git ls-files --others --exclude-standard ${scope}`,
57
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
58
+ ).trim();
59
+
60
+ if (!output) return [];
61
+ return output.split("\n").filter(Boolean);
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Verify that the coder produced real changes.
69
+ * Counts both tracked changes and untracked files.
70
+ *
71
+ * @param {Object} params
72
+ * @param {string} params.baseRef - git reference to compare against
73
+ * @param {string} [params.projectDir] - directory scope
74
+ * @param {number} [params.minFiles=1] - minimum files that must change
75
+ * @param {number} [params.minLines=1] - minimum lines that must change
76
+ * @returns {VerificationResult}
77
+ */
78
+ export function verifyCoderOutput({ baseRef, projectDir = null, minFiles = 1, minLines = 1 }) {
79
+ const tracked = countChangesSince(baseRef, projectDir);
80
+ const untracked = countUntrackedFiles(projectDir);
81
+
82
+ const totalFiles = tracked.filesChanged + untracked.length;
83
+ const totalLines = tracked.linesAdded + tracked.linesDeleted;
84
+ const allFiles = [...tracked.files, ...untracked];
85
+
86
+ const passed = totalFiles >= minFiles && (untracked.length > 0 || totalLines >= minLines);
87
+
88
+ if (passed) {
89
+ return {
90
+ passed: true,
91
+ filesChanged: totalFiles,
92
+ linesChanged: totalLines,
93
+ files: allFiles,
94
+ reason: "",
95
+ retryStrategy: null
96
+ };
97
+ }
98
+
99
+ return {
100
+ passed: false,
101
+ filesChanged: totalFiles,
102
+ linesChanged: totalLines,
103
+ files: allFiles,
104
+ reason: totalFiles === 0
105
+ ? "Coder produced 0 file changes"
106
+ : `Only ${totalFiles} file(s) and ${totalLines} line(s) changed (below minimum)`,
107
+ retryStrategy: buildRetryStrategy(totalFiles, totalLines)
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Build a retry strategy based on the verification failure.
113
+ */
114
+ function buildRetryStrategy(filesChanged, linesChanged) {
115
+ if (filesChanged === 0) {
116
+ return "Rephrase the feedback with explicit file paths. List specific files to create/modify. Include example code snippets.";
117
+ }
118
+ return "Expand the prompt with more specific instructions. Break the task into smaller concrete steps.";
119
+ }
120
+
121
+ /**
122
+ * Track consecutive verification failures to detect stuck loops.
123
+ */
124
+ export class VerificationTracker {
125
+ constructor() {
126
+ this.consecutiveFailures = 0;
127
+ this.history = [];
128
+ }
129
+
130
+ record(result) {
131
+ this.history.push({ ...result, timestamp: Date.now() });
132
+ if (result.passed) {
133
+ this.consecutiveFailures = 0;
134
+ } else {
135
+ this.consecutiveFailures += 1;
136
+ }
137
+ }
138
+
139
+ isStuck(threshold = 2) {
140
+ return this.consecutiveFailures >= threshold;
141
+ }
142
+
143
+ getLastFailure() {
144
+ return [...this.history].reverse().find(h => !h.passed) || null;
145
+ }
146
+ }