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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +1 -4
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/board.js +8 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +17 -52
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/pipeline-context.js +3 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/coder-stage.js +49 -4
- package/src/orchestrator/stages/reviewer-stage.js +52 -9
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +481 -672
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +33 -88
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/display.js +33 -14
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -201
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /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:
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
143
|
-
const { getPrDiff } = await import("../../
|
|
144
|
-
const diff = await getPrDiff(session.
|
|
145
|
-
logger.info(`Reviewer reading PR diff #${session.
|
|
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
|
+
}
|