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.
@@ -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
@@ -77,7 +77,7 @@ const DEFAULTS = {
77
77
  },
78
78
  token: null,
79
79
  project_key: null,
80
- admin_user: "admin",
80
+ admin_user: null,
81
81
  admin_password: null,
82
82
  coverage: {
83
83
  enabled: false,
@@ -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) => runKjCommand({ command: "scan", options: 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 or degrade gracefully).
5
+ * and BEFORE session iteration loop (so we fail fast).
6
6
  *
7
- * Design: always returns ok:true (graceful degradation, never hard-fail).
8
- * Disabled stages are auto-disabled via configOverrides instead of blocking.
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 to generate a token
79
- const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube?.admin_user || "admin";
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
- "admin"
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" : "warn",
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.configOverrides.sonarDisabled = true;
181
- result.warnings.push("Docker not available — SonarQube auto-disabled");
182
- logger.warn("Preflight: Docker not found, disabling SonarQube");
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: "warn", message: "Preflight completed with warnings", detail: result
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 && !result.configOverrides.sonarDisabled) {
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" : "warn",
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.configOverrides.sonarDisabled = true;
211
- result.warnings.push("SonarQube not reachable — auto-disabled");
212
- logger.warn("Preflight: SonarQube not reachable after remediation, disabling");
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 && !result.configOverrides.sonarDisabled) {
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" : "warn",
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.configOverrides.sonarDisabled = true;
233
- result.warnings.push("SonarQube auth failed — auto-disabled");
234
- logger.warn("Preflight: Sonar auth failed, disabling SonarQube");
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: hasWarnings
260
- ? `Preflight completed with ${result.warnings.length} warning(s)`
261
- : "Preflight passed — all checks OK",
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: `Solomon ruling: ${ruling.result?.ruling || "unknown"}`,
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: ruling.result?.escalate_reason }
61
+ conflict: { ...conflict, solomonReason: reason }
53
62
  });
54
63
  }
55
64
 
@@ -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 (preflightResult.configOverrides.sonarDisabled) {
890
- updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
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
+ }
@@ -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
- const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube.admin_user || "admin";
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
- "admin"
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. Tried configured token/password and fallback admin/admin.",
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
  }
@@ -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
- // Estimation is opt-in: only triggered when result.promptSize is explicitly provided.
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 && result?.promptSize > 0) {
49
- const promptSize = result.promptSize;
50
- const outputSize = (result?.output || result?.summary || "").length;
51
- const est = estimateTokens(promptSize, outputSize);
52
- finalTokensIn = est.tokens_in;
53
- finalTokensOut = est.tokens_out;
54
- estimated = true;
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: "claude/haiku", simple: "claude/haiku", medium: "claude/sonnet", complex: "claude/opus" },
3
- codex: { trivial: "codex/o4-mini", simple: "codex/o4-mini", medium: "codex/o4-mini", complex: "codex/o3" },
4
- gemini: { trivial: "gemini/flash", simple: "gemini/flash", medium: "gemini/pro", complex: "gemini/pro" },
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