karajan-code 1.59.0 → 2.0.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 (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 +34 -6
  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 +3 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/domains/domain-loader.js +85 -4
  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 +2 -2
  26. package/src/mcp/tools.js +3 -2
  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/post-loop-stages.js +20 -15
  35. package/src/orchestrator/preflight-checks.js +7 -3
  36. package/src/orchestrator/role-output-compressor.js +190 -0
  37. package/src/orchestrator/session-journal.js +344 -0
  38. package/src/orchestrator/solomon-escalation.js +38 -5
  39. package/src/orchestrator/solomon-rules.js +32 -33
  40. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  41. package/src/orchestrator/stages/sonar-stage.js +24 -1
  42. package/src/orchestrator/verification-gate.js +146 -0
  43. package/src/orchestrator.js +435 -661
  44. package/src/planning-game/client.js +2 -2
  45. package/src/planning-game/pipeline-adapter.js +1 -1
  46. package/src/prompts/coder.js +17 -3
  47. package/src/prompts/reviewer.js +2 -2
  48. package/src/prompts/rtk-snippet.js +3 -5
  49. package/src/review/diff-generator.js +66 -17
  50. package/src/review/snapshot-diff.js +122 -0
  51. package/src/review/tdd-policy.js +4 -2
  52. package/src/roles/agent-role.js +135 -0
  53. package/src/roles/architect-role.js +61 -108
  54. package/src/roles/audit-role.js +25 -71
  55. package/src/roles/coder-role.js +32 -57
  56. package/src/roles/discover-role.js +30 -71
  57. package/src/roles/hu-reviewer-role.js +51 -99
  58. package/src/roles/impeccable-role.js +34 -91
  59. package/src/roles/index.js +2 -0
  60. package/src/roles/karajan-brain-role.js +134 -0
  61. package/src/roles/planner-role.js +40 -75
  62. package/src/roles/refactorer-role.js +22 -64
  63. package/src/roles/researcher-role.js +52 -118
  64. package/src/roles/reviewer-role.js +69 -126
  65. package/src/roles/security-role.js +38 -102
  66. package/src/roles/solomon-role.js +71 -15
  67. package/src/roles/tester-role.js +121 -95
  68. package/src/roles/triage-role.js +50 -111
  69. package/src/session-store.js +1 -1
  70. package/src/skills/skill-detector.js +11 -0
  71. package/src/sonar/config-resolver.js +20 -1
  72. package/src/sonar/credentials.js +17 -0
  73. package/src/sonar/project-key.js +1 -1
  74. package/src/sonar/scanner.js +1 -1
  75. package/src/utils/banner.js +66 -0
  76. package/src/utils/display.js +35 -17
  77. package/src/utils/injection-guard.js +23 -8
  78. package/src/utils/logger.js +6 -1
  79. package/src/utils/process.js +3 -4
  80. package/src/utils/role-assigner.js +131 -0
  81. package/src/utils/update-check.js +5 -1
  82. package/src/utils/welcome.js +3 -8
  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 -198
  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;
@@ -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)`;
@@ -139,10 +150,10 @@ async function handleReviewerRejection({ review, repeatDetector, config, logger,
139
150
  }
140
151
 
141
152
  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}`);
153
+ if (session.ci_pr_number) {
154
+ const { getPrDiff } = await import("../../ci/pr-diff.js");
155
+ const diff = await getPrDiff(session.ci_pr_number);
156
+ logger.info(`Reviewer reading PR diff #${session.ci_pr_number}`);
146
157
  return diff;
147
158
  }
148
159
  return generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
@@ -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
+ }