karajan-code 1.34.4 → 1.36.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.
- package/README.md +3 -2
- package/docs/README.es.md +3 -2
- package/package.json +1 -1
- package/src/agents/aider-agent.js +16 -9
- package/src/agents/base-agent.js +15 -0
- package/src/agents/claude-agent.js +51 -6
- package/src/agents/codex-agent.js +35 -13
- package/src/agents/gemini-agent.js +17 -9
- package/src/agents/model-registry.js +8 -7
- package/src/agents/opencode-agent.js +17 -10
- package/src/bootstrap.js +235 -0
- package/src/commands/doctor.js +98 -0
- package/src/config.js +1 -1
- package/src/mcp/server-handlers.js +33 -2
- package/src/orchestrator/preflight-checks.js +54 -27
- package/src/orchestrator/solomon-escalation.js +11 -2
- package/src/orchestrator.js +9 -2
- package/src/roles/solomon-role.js +17 -1
- package/src/sonar/credentials.js +35 -0
- package/src/sonar/scanner.js +8 -7
- package/src/utils/budget.js +12 -8
- package/src/utils/model-selector.js +3 -3
- package/src/utils/stall-detector.js +5 -5
- package/templates/kj.config.yml +3 -0
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { runCommand } from "../utils/process.js";
|
|
5
7
|
import { exists } from "../utils/fs.js";
|
|
@@ -239,6 +241,101 @@ async function checkRuleFiles(config) {
|
|
|
239
241
|
];
|
|
240
242
|
}
|
|
241
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Detect duplicate TOML table headers (e.g. [mcp_servers."karajan-mcp"] appearing twice).
|
|
246
|
+
* Full TOML parsing would require a dependency — this catches the most common config error.
|
|
247
|
+
*/
|
|
248
|
+
function findDuplicateTomlKeys(content) {
|
|
249
|
+
const tableHeaders = [];
|
|
250
|
+
const duplicates = [];
|
|
251
|
+
for (const line of content.split("\n")) {
|
|
252
|
+
const match = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
253
|
+
if (match) {
|
|
254
|
+
const key = match[1].trim();
|
|
255
|
+
if (tableHeaders.includes(key)) {
|
|
256
|
+
duplicates.push(key);
|
|
257
|
+
} else {
|
|
258
|
+
tableHeaders.push(key);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return duplicates;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function checkAgentConfigs() {
|
|
266
|
+
const checks = [];
|
|
267
|
+
const home = os.homedir();
|
|
268
|
+
|
|
269
|
+
// Claude: ~/.claude.json
|
|
270
|
+
const claudeJsonPath = path.join(home, ".claude.json");
|
|
271
|
+
try {
|
|
272
|
+
const raw = await fs.readFile(claudeJsonPath, "utf8");
|
|
273
|
+
JSON.parse(raw);
|
|
274
|
+
checks.push({ name: "agent-config:claude", label: "Agent config: claude (~/.claude.json)", ok: true, detail: "Valid JSON", fix: null });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (err.code === "ENOENT") {
|
|
277
|
+
// File doesn't exist — not an error, Claude may not be configured
|
|
278
|
+
} else {
|
|
279
|
+
checks.push({
|
|
280
|
+
name: "agent-config:claude",
|
|
281
|
+
label: "Agent config: claude (~/.claude.json)",
|
|
282
|
+
ok: false,
|
|
283
|
+
detail: `Invalid JSON: ${err.message.split("\n")[0]}`,
|
|
284
|
+
fix: "Fix the JSON syntax in ~/.claude.json. Common issues: trailing commas, missing quotes."
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Codex: ~/.codex/config.toml
|
|
290
|
+
const codexTomlPath = path.join(home, ".codex", "config.toml");
|
|
291
|
+
try {
|
|
292
|
+
const raw = await fs.readFile(codexTomlPath, "utf8");
|
|
293
|
+
const duplicates = findDuplicateTomlKeys(raw);
|
|
294
|
+
if (duplicates.length > 0) {
|
|
295
|
+
checks.push({
|
|
296
|
+
name: "agent-config:codex",
|
|
297
|
+
label: "Agent config: codex (~/.codex/config.toml)",
|
|
298
|
+
ok: false,
|
|
299
|
+
detail: `Duplicate TOML keys: ${duplicates.join(", ")}`,
|
|
300
|
+
fix: `Remove duplicate entries in ~/.codex/config.toml: ${duplicates.join(", ")}`
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
checks.push({ name: "agent-config:codex", label: "Agent config: codex (~/.codex/config.toml)", ok: true, detail: "Valid TOML (no duplicate keys)", fix: null });
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err.code !== "ENOENT") {
|
|
307
|
+
checks.push({
|
|
308
|
+
name: "agent-config:codex",
|
|
309
|
+
label: "Agent config: codex (~/.codex/config.toml)",
|
|
310
|
+
ok: false,
|
|
311
|
+
detail: `Cannot read: ${err.message.split("\n")[0]}`,
|
|
312
|
+
fix: "Check file permissions on ~/.codex/config.toml"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// KJ config: ~/.karajan/kj.config.yml (validate YAML)
|
|
318
|
+
const kjConfigPath = getConfigPath();
|
|
319
|
+
try {
|
|
320
|
+
const raw = await fs.readFile(kjConfigPath, "utf8");
|
|
321
|
+
const yaml = await import("js-yaml");
|
|
322
|
+
yaml.default.load(raw);
|
|
323
|
+
checks.push({ name: "agent-config:karajan", label: "Agent config: karajan (kj.config.yml)", ok: true, detail: "Valid YAML", fix: null });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (err.code !== "ENOENT") {
|
|
326
|
+
checks.push({
|
|
327
|
+
name: "agent-config:karajan",
|
|
328
|
+
label: "Agent config: karajan (kj.config.yml)",
|
|
329
|
+
ok: false,
|
|
330
|
+
detail: `Invalid YAML: ${err.message.split("\n")[0]}`,
|
|
331
|
+
fix: `Fix YAML syntax in ${kjConfigPath}. Run 'kj init' to regenerate if needed.`
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return checks;
|
|
337
|
+
}
|
|
338
|
+
|
|
242
339
|
export async function runChecks({ config }) {
|
|
243
340
|
const checks = [];
|
|
244
341
|
|
|
@@ -260,6 +357,7 @@ export async function runChecks({ config }) {
|
|
|
260
357
|
checks.push(...await checkBecariaInfra(config));
|
|
261
358
|
}
|
|
262
359
|
|
|
360
|
+
checks.push(...await checkAgentConfigs());
|
|
263
361
|
checks.push(...await checkRuleFiles(config));
|
|
264
362
|
checks.push(await checkRtk());
|
|
265
363
|
|
package/src/config.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolveReviewProfile } from "../review/profiles.js";
|
|
|
24
24
|
import { createRunLog, readRunLog } from "../utils/run-log.js";
|
|
25
25
|
import { currentBranch } from "../utils/git.js";
|
|
26
26
|
import { isPreflightAcked, ackPreflight, getSessionOverrides } from "./preflight.js";
|
|
27
|
+
import { ensureBootstrap } from "../bootstrap.js";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Resolve the user's project directory.
|
|
@@ -118,6 +119,11 @@ const ERROR_CLASSIFIERS = [
|
|
|
118
119
|
test: (lower) => lower.includes("not a git repository"),
|
|
119
120
|
category: "git_error",
|
|
120
121
|
suggestion: "Current directory is not a git repository. Navigate to your project root or initialize git with 'git init'."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
test: (lower) => lower.includes("bootstrap failed"),
|
|
125
|
+
category: "bootstrap_error",
|
|
126
|
+
suggestion: "Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly."
|
|
121
127
|
}
|
|
122
128
|
];
|
|
123
129
|
|
|
@@ -149,6 +155,16 @@ export async function assertNotOnBaseBranch(config) {
|
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Run bootstrap gate: validate all environment prerequisites before execution.
|
|
160
|
+
* Throws if any prerequisite fails.
|
|
161
|
+
*/
|
|
162
|
+
async function runBootstrapGate(server, a) {
|
|
163
|
+
const projectDir = await resolveProjectDir(server, a.projectDir);
|
|
164
|
+
const { config } = await loadConfig(projectDir);
|
|
165
|
+
await ensureBootstrap(projectDir, config);
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
export function enrichedFailPayload(error, toolName) {
|
|
153
169
|
const msg = error?.message || String(error);
|
|
154
170
|
const { category, suggestion } = classifyError(error);
|
|
@@ -191,7 +207,7 @@ export function buildAskQuestion(server) {
|
|
|
191
207
|
|
|
192
208
|
const MAX_AUTO_RESUMES = 2;
|
|
193
209
|
const NON_RECOVERABLE_CATEGORIES = new Set([
|
|
194
|
-
"config_error", "auth_error", "agent_missing", "branch_error", "git_error"
|
|
210
|
+
"config_error", "auth_error", "agent_missing", "branch_error", "git_error", "bootstrap_error"
|
|
195
211
|
]);
|
|
196
212
|
|
|
197
213
|
async function attemptAutoResume({ err, config, logger, emitter, askQuestion, runLog }) {
|
|
@@ -783,6 +799,7 @@ async function handleResume(a, server, extra) {
|
|
|
783
799
|
if (!a.sessionId) {
|
|
784
800
|
return failPayload("Missing required field: sessionId");
|
|
785
801
|
}
|
|
802
|
+
await runBootstrapGate(server, a);
|
|
786
803
|
if (a.answer) {
|
|
787
804
|
const validation = validateResumeAnswer(a.answer);
|
|
788
805
|
if (!validation.valid) {
|
|
@@ -804,6 +821,7 @@ async function handleRun(a, server, extra) {
|
|
|
804
821
|
return failPayload(`Invalid taskType "${a.taskType}". Valid values: ${[...validTypes].join(", ")}`);
|
|
805
822
|
}
|
|
806
823
|
}
|
|
824
|
+
await runBootstrapGate(server, a);
|
|
807
825
|
if (!isPreflightAcked()) {
|
|
808
826
|
// Auto-acknowledge with defaults for autonomous operation
|
|
809
827
|
ackPreflight({});
|
|
@@ -818,6 +836,7 @@ async function handleCode(a, server, extra) {
|
|
|
818
836
|
if (!a.task) {
|
|
819
837
|
return failPayload("Missing required field: task");
|
|
820
838
|
}
|
|
839
|
+
await runBootstrapGate(server, a);
|
|
821
840
|
if (!isPreflightAcked()) {
|
|
822
841
|
// Auto-acknowledge with defaults for autonomous operation
|
|
823
842
|
ackPreflight({});
|
|
@@ -832,6 +851,7 @@ async function handleReview(a, server, extra) {
|
|
|
832
851
|
if (!a.task) {
|
|
833
852
|
return failPayload("Missing required field: task");
|
|
834
853
|
}
|
|
854
|
+
await runBootstrapGate(server, a);
|
|
835
855
|
return handleReviewDirect(a, server, extra);
|
|
836
856
|
}
|
|
837
857
|
|
|
@@ -839,6 +859,7 @@ async function handlePlan(a, server, extra) {
|
|
|
839
859
|
if (!a.task) {
|
|
840
860
|
return failPayload("Missing required field: task");
|
|
841
861
|
}
|
|
862
|
+
await runBootstrapGate(server, a);
|
|
842
863
|
return handlePlanDirect(a, server, extra);
|
|
843
864
|
}
|
|
844
865
|
|
|
@@ -850,6 +871,7 @@ async function handleDiscover(a, server, extra) {
|
|
|
850
871
|
if (a.mode && !validModes.has(a.mode)) {
|
|
851
872
|
return failPayload(`Invalid mode "${a.mode}". Valid values: ${[...validModes].join(", ")}`);
|
|
852
873
|
}
|
|
874
|
+
await runBootstrapGate(server, a);
|
|
853
875
|
return handleDiscoverDirect(a, server, extra);
|
|
854
876
|
}
|
|
855
877
|
|
|
@@ -857,6 +879,7 @@ async function handleTriage(a, server, extra) {
|
|
|
857
879
|
if (!a.task) {
|
|
858
880
|
return failPayload("Missing required field: task");
|
|
859
881
|
}
|
|
882
|
+
await runBootstrapGate(server, a);
|
|
860
883
|
return handleTriageDirect(a, server, extra);
|
|
861
884
|
}
|
|
862
885
|
|
|
@@ -864,6 +887,7 @@ async function handleResearcher(a, server, extra) {
|
|
|
864
887
|
if (!a.task) {
|
|
865
888
|
return failPayload("Missing required field: task");
|
|
866
889
|
}
|
|
890
|
+
await runBootstrapGate(server, a);
|
|
867
891
|
return handleResearcherDirect(a, server, extra);
|
|
868
892
|
}
|
|
869
893
|
|
|
@@ -871,10 +895,12 @@ async function handleArchitect(a, server, extra) {
|
|
|
871
895
|
if (!a.task) {
|
|
872
896
|
return failPayload("Missing required field: task");
|
|
873
897
|
}
|
|
898
|
+
await runBootstrapGate(server, a);
|
|
874
899
|
return handleArchitectDirect(a, server, extra);
|
|
875
900
|
}
|
|
876
901
|
|
|
877
902
|
async function handleAudit(a, server, extra) {
|
|
903
|
+
await runBootstrapGate(server, a);
|
|
878
904
|
return handleAuditDirect(a, server, extra);
|
|
879
905
|
}
|
|
880
906
|
|
|
@@ -892,6 +918,11 @@ async function handleBoard(a) {
|
|
|
892
918
|
}
|
|
893
919
|
}
|
|
894
920
|
|
|
921
|
+
async function handleScan(a, server) {
|
|
922
|
+
await runBootstrapGate(server, a);
|
|
923
|
+
return runKjCommand({ command: "scan", options: a });
|
|
924
|
+
}
|
|
925
|
+
|
|
895
926
|
/* ── Handler dispatch map ─────────────────────────────────────────── */
|
|
896
927
|
|
|
897
928
|
const toolHandlers = {
|
|
@@ -901,7 +932,7 @@ const toolHandlers = {
|
|
|
901
932
|
kj_agents: (a) => handleAgents(a),
|
|
902
933
|
kj_preflight: (a) => handlePreflight(a),
|
|
903
934
|
kj_config: (a) => runKjCommand({ command: "config", commandArgs: a.json ? ["--json"] : [], options: a }),
|
|
904
|
-
kj_scan: (a) =>
|
|
935
|
+
kj_scan: (a, server) => handleScan(a, server),
|
|
905
936
|
kj_roles: (a) => handleRoles(a),
|
|
906
937
|
kj_report: (a) => runKjCommand({ command: "report", commandArgs: buildReportArgs(a), options: a }),
|
|
907
938
|
kj_resume: (a, server, extra) => handleResume(a, server, extra),
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* Preflight environment checks for kj_run.
|
|
3
3
|
*
|
|
4
4
|
* Runs AFTER policy resolution (so we know which stages are active)
|
|
5
|
-
* and BEFORE session iteration loop (so we fail fast
|
|
5
|
+
* and BEFORE session iteration loop (so we fail fast).
|
|
6
6
|
*
|
|
7
|
-
* Design:
|
|
8
|
-
*
|
|
7
|
+
* Design: SonarQube checks are BLOCKING when enabled — if SonarQube is
|
|
8
|
+
* configured but not available, the pipeline STOPS with a clear error.
|
|
9
|
+
* Security agent checks remain graceful (warning, auto-disable).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { checkBinary } from "../utils/agent-detect.js";
|
|
12
13
|
import { isSonarReachable, sonarUp } from "../sonar/manager.js";
|
|
13
14
|
import { runCommand } from "../utils/process.js";
|
|
14
15
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
16
|
+
import { loadSonarCredentials } from "../sonar/credentials.js";
|
|
15
17
|
|
|
16
18
|
function normalizeApiHost(rawHost) {
|
|
17
19
|
return String(rawHost || "http://localhost:9000").replace(/host\.docker\.internal/g, "localhost");
|
|
@@ -75,14 +77,19 @@ async function checkSonarAuth(config) {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
// Try admin credentials
|
|
79
|
-
const
|
|
80
|
+
// Try admin credentials: env vars → config → ~/.karajan/sonar-credentials.json (NO default admin/admin)
|
|
81
|
+
const fileCreds = await loadSonarCredentials() || {};
|
|
82
|
+
const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube?.admin_user || fileCreds.user;
|
|
80
83
|
const candidates = [
|
|
81
84
|
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
82
85
|
config.sonarqube?.admin_password,
|
|
83
|
-
|
|
86
|
+
fileCreds.password
|
|
84
87
|
].filter(Boolean);
|
|
85
88
|
|
|
89
|
+
if (!adminUser || candidates.length === 0) {
|
|
90
|
+
return { name: "sonar-auth", ok: false, detail: "No Sonar token or admin credentials configured. Set KJ_SONAR_TOKEN, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json." };
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
for (const password of [...new Set(candidates)]) {
|
|
87
94
|
const validateRes = await runCommand("curl", [
|
|
88
95
|
"-sS", "-u", `${adminUser}:${password}`,
|
|
@@ -128,6 +135,10 @@ async function checkSecurityAgent(config) {
|
|
|
128
135
|
/**
|
|
129
136
|
* Run preflight environment checks.
|
|
130
137
|
*
|
|
138
|
+
* SonarQube checks are BLOCKING: if SonarQube is enabled but not available,
|
|
139
|
+
* ok will be false and errors[] will contain actionable fix instructions.
|
|
140
|
+
* Security agent checks remain graceful (auto-disable via configOverrides).
|
|
141
|
+
*
|
|
131
142
|
* @param {object} opts
|
|
132
143
|
* @param {object} opts.config - Karajan config
|
|
133
144
|
* @param {object} opts.logger - Logger instance
|
|
@@ -135,7 +146,7 @@ async function checkSecurityAgent(config) {
|
|
|
135
146
|
* @param {object} opts.eventBase - Base event data
|
|
136
147
|
* @param {object} opts.resolvedPolicies - Output from applyPolicies()
|
|
137
148
|
* @param {boolean} opts.securityEnabled - Whether security stage is enabled
|
|
138
|
-
* @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[] }}
|
|
149
|
+
* @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[], errors: object[] }}
|
|
139
150
|
*/
|
|
140
151
|
export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
|
|
141
152
|
const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
|
|
@@ -148,6 +159,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
148
159
|
remediations: [],
|
|
149
160
|
configOverrides: {},
|
|
150
161
|
warnings: [],
|
|
162
|
+
errors: [],
|
|
151
163
|
};
|
|
152
164
|
|
|
153
165
|
// Short-circuit: nothing to check
|
|
@@ -171,20 +183,24 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
171
183
|
result.checks.push(dockerCheck);
|
|
172
184
|
|
|
173
185
|
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
174
|
-
status: dockerCheck.ok ? "ok" : "
|
|
186
|
+
status: dockerCheck.ok ? "ok" : "fail",
|
|
175
187
|
message: `Docker: ${dockerCheck.detail}`,
|
|
176
188
|
detail: dockerCheck
|
|
177
189
|
}));
|
|
178
190
|
|
|
179
191
|
if (!dockerCheck.ok) {
|
|
180
|
-
result.
|
|
181
|
-
result.
|
|
182
|
-
|
|
192
|
+
result.ok = false;
|
|
193
|
+
result.errors.push({
|
|
194
|
+
check: "docker",
|
|
195
|
+
message: "Docker not available but SonarQube is enabled.",
|
|
196
|
+
fix: "Start Docker, or disable SonarQube: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
|
|
197
|
+
});
|
|
198
|
+
logger.error("Preflight: Docker not found — SonarQube requires Docker");
|
|
183
199
|
|
|
184
200
|
// Skip remaining sonar checks, continue to security
|
|
185
201
|
if (!securityEnabled) {
|
|
186
202
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
187
|
-
status: "
|
|
203
|
+
status: "fail", message: "Preflight FAILED — environment not ready", detail: result
|
|
188
204
|
}));
|
|
189
205
|
return result;
|
|
190
206
|
}
|
|
@@ -192,7 +208,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
// --- 2. SonarQube reachable ---
|
|
195
|
-
if (sonarEnabled &&
|
|
211
|
+
if (sonarEnabled && result.ok) {
|
|
196
212
|
const reachableCheck = await checkSonarReachable(sonarHost);
|
|
197
213
|
result.checks.push(reachableCheck);
|
|
198
214
|
|
|
@@ -201,25 +217,29 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
204
|
-
status: reachableCheck.ok ? "ok" : "
|
|
220
|
+
status: reachableCheck.ok ? "ok" : "fail",
|
|
205
221
|
message: `SonarQube reachability: ${reachableCheck.detail}`,
|
|
206
222
|
detail: reachableCheck
|
|
207
223
|
}));
|
|
208
224
|
|
|
209
225
|
if (!reachableCheck.ok) {
|
|
210
|
-
result.
|
|
211
|
-
result.
|
|
212
|
-
|
|
226
|
+
result.ok = false;
|
|
227
|
+
result.errors.push({
|
|
228
|
+
check: "sonar-reachable",
|
|
229
|
+
message: `SonarQube not reachable at ${sonarHost}.`,
|
|
230
|
+
fix: "Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
|
|
231
|
+
});
|
|
232
|
+
logger.error("Preflight: SonarQube not reachable after remediation attempt");
|
|
213
233
|
}
|
|
214
234
|
}
|
|
215
235
|
|
|
216
236
|
// --- 3. SonarQube auth/token ---
|
|
217
|
-
if (sonarEnabled &&
|
|
237
|
+
if (sonarEnabled && result.ok) {
|
|
218
238
|
const authCheck = await checkSonarAuth(config);
|
|
219
239
|
result.checks.push(authCheck);
|
|
220
240
|
|
|
221
241
|
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
222
|
-
status: authCheck.ok ? "ok" : "
|
|
242
|
+
status: authCheck.ok ? "ok" : "fail",
|
|
223
243
|
message: `SonarQube auth: ${authCheck.detail}`,
|
|
224
244
|
detail: { name: authCheck.name, ok: authCheck.ok, detail: authCheck.detail }
|
|
225
245
|
}));
|
|
@@ -229,13 +249,17 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
229
249
|
result.remediations.push("Sonar token resolved and cached in KJ_SONAR_TOKEN");
|
|
230
250
|
logger.info("Preflight: Sonar token resolved and cached");
|
|
231
251
|
} else if (!authCheck.ok) {
|
|
232
|
-
result.
|
|
233
|
-
result.
|
|
234
|
-
|
|
252
|
+
result.ok = false;
|
|
253
|
+
result.errors.push({
|
|
254
|
+
check: "sonar-auth",
|
|
255
|
+
message: "SonarQube authentication failed.",
|
|
256
|
+
fix: "Regenerate the SonarQube token and update it via kj_init or in kj.config.yml under sonarqube.token."
|
|
257
|
+
});
|
|
258
|
+
logger.error("Preflight: Sonar auth failed");
|
|
235
259
|
}
|
|
236
260
|
}
|
|
237
261
|
|
|
238
|
-
// --- 4. Security agent ---
|
|
262
|
+
// --- 4. Security agent (graceful — only warning, not blocking) ---
|
|
239
263
|
if (securityEnabled) {
|
|
240
264
|
const secCheck = await checkSecurityAgent(config);
|
|
241
265
|
result.checks.push(secCheck);
|
|
@@ -253,12 +277,15 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
253
277
|
}
|
|
254
278
|
}
|
|
255
279
|
|
|
280
|
+
const hasErrors = result.errors.length > 0;
|
|
256
281
|
const hasWarnings = result.warnings.length > 0;
|
|
257
282
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
258
|
-
status: hasWarnings ? "warn" : "ok",
|
|
259
|
-
message:
|
|
260
|
-
? `Preflight
|
|
261
|
-
:
|
|
283
|
+
status: hasErrors ? "fail" : hasWarnings ? "warn" : "ok",
|
|
284
|
+
message: hasErrors
|
|
285
|
+
? `Preflight FAILED — ${result.errors.length} blocking issue(s)`
|
|
286
|
+
: hasWarnings
|
|
287
|
+
? `Preflight completed with ${result.warnings.length} warning(s)`
|
|
288
|
+
: "Preflight passed — all checks OK",
|
|
262
289
|
detail: result
|
|
263
290
|
}));
|
|
264
291
|
|
|
@@ -30,10 +30,17 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const solomonError = ruling.result?.error;
|
|
34
|
+
if (!ruling.ok && solomonError) {
|
|
35
|
+
logger.warn(`Solomon execution failed: ${solomonError}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
emitProgress(
|
|
34
39
|
emitter,
|
|
35
40
|
makeEvent("solomon:end", { ...eventBase, stage: "solomon" }, {
|
|
36
|
-
message:
|
|
41
|
+
message: ruling.ok
|
|
42
|
+
? `Solomon ruling: ${ruling.result?.ruling || "unknown"}`
|
|
43
|
+
: `Solomon failed: ${(solomonError || ruling.summary || "unknown error").slice(0, 200)}`,
|
|
37
44
|
detail: ruling.result
|
|
38
45
|
})
|
|
39
46
|
);
|
|
@@ -43,13 +50,15 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
43
50
|
iteration,
|
|
44
51
|
ruling: ruling.result?.ruling,
|
|
45
52
|
escalate: ruling.result?.escalate,
|
|
53
|
+
error: solomonError ? solomonError.slice(0, 500) : undefined,
|
|
46
54
|
subtask: ruling.result?.subtask?.title || null
|
|
47
55
|
});
|
|
48
56
|
|
|
49
57
|
if (!ruling.ok) {
|
|
58
|
+
const reason = ruling.result?.escalate_reason || solomonError || ruling.summary;
|
|
50
59
|
return escalateToHuman({
|
|
51
60
|
askQuestion, session, emitter, eventBase, stage, iteration,
|
|
52
|
-
conflict: { ...conflict, solomonReason:
|
|
61
|
+
conflict: { ...conflict, solomonReason: reason }
|
|
53
62
|
});
|
|
54
63
|
}
|
|
55
64
|
|
package/src/orchestrator.js
CHANGED
|
@@ -886,9 +886,16 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
886
886
|
session.preflight = preflightResult;
|
|
887
887
|
await saveSession(session);
|
|
888
888
|
|
|
889
|
-
if (
|
|
890
|
-
|
|
889
|
+
// Hard fail if blocking checks failed (SonarQube enabled but not available)
|
|
890
|
+
if (!preflightResult.ok) {
|
|
891
|
+
const errorLines = (preflightResult.errors || [])
|
|
892
|
+
.map(e => ` - ${e.message}\n Fix: ${e.fix}`)
|
|
893
|
+
.join("\n");
|
|
894
|
+
throw new Error(
|
|
895
|
+
`Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
|
|
896
|
+
);
|
|
891
897
|
}
|
|
898
|
+
|
|
892
899
|
if (preflightResult.configOverrides.securityDisabled) {
|
|
893
900
|
pipelineFlags.securityEnabled = false;
|
|
894
901
|
}
|
|
@@ -62,12 +62,28 @@ function buildPrompt({ conflict, task, instructions }) {
|
|
|
62
62
|
const iterationCount = conflict?.iterationCount ?? "?";
|
|
63
63
|
const maxIterations = conflict?.maxIterations ?? "?";
|
|
64
64
|
|
|
65
|
+
const isFirstRejection = conflict?.isFirstRejection ?? false;
|
|
66
|
+
const isRepeat = conflict?.isRepeat ?? false;
|
|
67
|
+
|
|
65
68
|
sections.push(
|
|
66
69
|
`## Conflict context`,
|
|
67
70
|
`Stage: ${stage}`,
|
|
68
|
-
`Iterations exhausted: ${iterationCount}/${maxIterations}
|
|
71
|
+
`Iterations exhausted: ${iterationCount}/${maxIterations}`,
|
|
72
|
+
`isFirstRejection: ${isFirstRejection}`,
|
|
73
|
+
`isRepeat: ${isRepeat}`
|
|
69
74
|
);
|
|
70
75
|
|
|
76
|
+
if (conflict?.issueCategories) {
|
|
77
|
+
sections.push(`## Issue categories\n${JSON.stringify(conflict.issueCategories, null, 2)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (conflict?.blockingIssues?.length) {
|
|
81
|
+
const issueList = conflict.blockingIssues
|
|
82
|
+
.map((issue, i) => `${i + 1}. [${issue.severity || "unknown"}] ${issue.description || issue}`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
sections.push(`## Blocking issues\n${issueList}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
if (task) {
|
|
72
88
|
sections.push(`## Original task\n${task}`);
|
|
73
89
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load SonarQube admin credentials from ~/.karajan/sonar-credentials.json.
|
|
3
|
+
*
|
|
4
|
+
* File format:
|
|
5
|
+
* {
|
|
6
|
+
* "user": "admin",
|
|
7
|
+
* "password": "your-password"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* Returns { user, password } or { user: null, password: null } if file missing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
16
|
+
|
|
17
|
+
const CREDENTIALS_FILENAME = "sonar-credentials.json";
|
|
18
|
+
|
|
19
|
+
export async function loadSonarCredentials() {
|
|
20
|
+
try {
|
|
21
|
+
const filePath = path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
22
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
23
|
+
const data = JSON.parse(raw);
|
|
24
|
+
return {
|
|
25
|
+
user: data.user || null,
|
|
26
|
+
password: data.password || null
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return { user: null, password: null };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function credentialsPath() {
|
|
34
|
+
return path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
35
|
+
}
|
package/src/sonar/scanner.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { runCommand } from "../utils/process.js";
|
|
5
5
|
import { sonarUp } from "./manager.js";
|
|
6
6
|
import { resolveSonarProjectKey } from "./project-key.js";
|
|
7
|
+
import { loadSonarCredentials } from "./credentials.js";
|
|
7
8
|
|
|
8
9
|
export function buildScannerOpts(projectKey, scanner = {}) {
|
|
9
10
|
const opts = [`-Dsonar.projectKey=${projectKey}`];
|
|
@@ -136,18 +137,18 @@ async function resolveSonarToken(config, apiHost) {
|
|
|
136
137
|
const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube.token;
|
|
137
138
|
if (explicitToken) return explicitToken;
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
// Resolve admin credentials from: env vars → config → ~/.karajan/sonar-credentials.json
|
|
141
|
+
const fileCreds = await loadSonarCredentials() || {};
|
|
142
|
+
const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube.admin_user || fileCreds.user;
|
|
140
143
|
const candidates = [
|
|
141
144
|
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
142
145
|
config.sonarqube.admin_password,
|
|
143
|
-
|
|
146
|
+
fileCreds.password
|
|
144
147
|
].filter(Boolean);
|
|
145
148
|
|
|
149
|
+
if (!adminUser || candidates.length === 0) return null;
|
|
150
|
+
|
|
146
151
|
for (const password of new Set(candidates)) {
|
|
147
|
-
if (password === "admin") {
|
|
148
|
-
// eslint-disable-next-line no-console
|
|
149
|
-
console.warn("[karajan] WARNING: Using default admin/admin credentials for SonarQube. Set KJ_SONAR_TOKEN for production use.");
|
|
150
|
-
}
|
|
151
152
|
const valid = await validateAdminCredentials(apiHost, adminUser, password);
|
|
152
153
|
if (!valid) continue;
|
|
153
154
|
const token = await generateUserToken(apiHost, adminUser, password);
|
|
@@ -224,7 +225,7 @@ export async function runSonarScan(config, projectKey = null) {
|
|
|
224
225
|
ok: false,
|
|
225
226
|
stdout: "",
|
|
226
227
|
stderr:
|
|
227
|
-
"Unable to resolve Sonar token.
|
|
228
|
+
"Unable to resolve Sonar token. Set KJ_SONAR_TOKEN env var, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json.",
|
|
228
229
|
exitCode: 1
|
|
229
230
|
};
|
|
230
231
|
}
|
package/src/utils/budget.js
CHANGED
|
@@ -40,18 +40,22 @@ export function extractUsageMetrics(result, defaultModel = null) {
|
|
|
40
40
|
null;
|
|
41
41
|
|
|
42
42
|
// If no real token data AND no explicit cost, estimate from prompt/output sizes.
|
|
43
|
-
//
|
|
43
|
+
// Primary: uses result.promptSize when explicitly provided.
|
|
44
|
+
// Fallback: estimates from result.output or result.error text length.
|
|
44
45
|
let estimated = false;
|
|
45
46
|
let finalTokensIn = tokens_in;
|
|
46
47
|
let finalTokensOut = tokens_out;
|
|
47
48
|
const hasExplicitCost = cost_usd !== undefined && cost_usd !== null && cost_usd !== "";
|
|
48
|
-
if (!tokens_in && !tokens_out && !hasExplicitCost
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
if (!tokens_in && !tokens_out && !hasExplicitCost) {
|
|
50
|
+
const outputText = result?.output || result?.error || result?.summary || "";
|
|
51
|
+
const promptSize = result?.promptSize || 0;
|
|
52
|
+
const MIN_TEXT_FOR_ESTIMATION = 40;
|
|
53
|
+
if (promptSize > 0 || outputText.length >= MIN_TEXT_FOR_ESTIMATION) {
|
|
54
|
+
const est = estimateTokens(promptSize, outputText.length);
|
|
55
|
+
finalTokensIn = est.tokens_in;
|
|
56
|
+
finalTokensOut = est.tokens_out;
|
|
57
|
+
estimated = true;
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
return { tokens_in: finalTokensIn, tokens_out: finalTokensOut, cost_usd, model, estimated };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const DEFAULT_MODEL_TIERS = {
|
|
2
|
-
claude: { trivial: "
|
|
3
|
-
codex: { trivial: "
|
|
4
|
-
gemini: { trivial: "gemini
|
|
2
|
+
claude: { trivial: "haiku", simple: "haiku", medium: "sonnet", complex: "opus" },
|
|
3
|
+
codex: { trivial: "o4-mini", simple: "o4-mini", medium: "o4-mini", complex: "o3" },
|
|
4
|
+
gemini: { trivial: "gemini-2.0-flash", simple: "gemini-2.0-flash", medium: "gemini-2.5-pro", complex: "gemini-2.5-pro" },
|
|
5
5
|
aider: { trivial: null, simple: null, medium: null, complex: null }
|
|
6
6
|
};
|
|
7
7
|
|