karajan-code 1.48.0 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/agents/resolve-bin.js +1 -1
  3. package/src/audit/basal-cost.js +26 -23
  4. package/src/bootstrap.js +2 -2
  5. package/src/commands/agents.js +1 -1
  6. package/src/commands/board.js +3 -3
  7. package/src/commands/code.js +1 -1
  8. package/src/commands/doctor.js +4 -4
  9. package/src/commands/init.js +2 -2
  10. package/src/commands/report.js +1 -1
  11. package/src/commands/review.js +1 -1
  12. package/src/commands/roles.js +1 -1
  13. package/src/hu/store.js +9 -5
  14. package/src/mcp/orphan-guard.js +1 -1
  15. package/src/mcp/server-handlers.js +3 -3
  16. package/src/mcp/suggest-handler.js +2 -2
  17. package/src/orchestrator/hu-sub-pipeline.js +1 -1
  18. package/src/orchestrator/pre-loop-stages.js +2 -2
  19. package/src/orchestrator/preflight-checks.js +13 -19
  20. package/src/plugins/loader.js +1 -1
  21. package/src/prompts/architect.js +1 -1
  22. package/src/review/parser.js +3 -3
  23. package/src/roles/architect-role.js +1 -1
  24. package/src/roles/audit-role.js +3 -3
  25. package/src/roles/base-role.js +1 -1
  26. package/src/roles/discover-role.js +1 -1
  27. package/src/roles/hu-reviewer-role.js +1 -1
  28. package/src/roles/researcher-role.js +1 -1
  29. package/src/roles/triage-role.js +1 -1
  30. package/src/session-cleanup.js +1 -1
  31. package/src/session-store.js +2 -2
  32. package/src/skills/openskills-client.js +1 -1
  33. package/src/skills/skill-detector.js +2 -2
  34. package/src/sonar/api.js +4 -3
  35. package/src/sonar/config-resolver.js +69 -0
  36. package/src/sonar/credentials.js +1 -1
  37. package/src/sonar/project-key.js +1 -1
  38. package/src/sonar/scanner.js +13 -21
  39. package/src/utils/fs.js +1 -1
  40. package/src/utils/json-extract.js +1 -1
  41. package/src/utils/project-detect.js +1 -1
  42. package/src/utils/rtk-detect.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -26,7 +26,7 @@ function getNvmDirs() {
26
26
  const versionsDir = path.join(nvmDir, "versions", "node");
27
27
  try {
28
28
  return readdirSync(versionsDir).map(v => path.join(versionsDir, v, "bin"));
29
- } catch {
29
+ } catch { /* nvm versions dir may not exist */
30
30
  return [];
31
31
  }
32
32
  }
@@ -1,8 +1,11 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { execFile, execFileSync } from "node:child_process";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
4
5
  import { getKarajanHome } from "../utils/paths.js";
5
6
 
7
+ const execFileAsync = promisify(execFile);
8
+
6
9
  const SOURCE_EXTENSIONS = new Set([
7
10
  ".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx", ".jsx",
8
11
  ".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift", ".c", ".cpp", ".h",
@@ -18,11 +21,11 @@ function slugify(projectDir) {
18
21
  return path.basename(projectDir).replace(/[^a-zA-Z0-9_-]/g, "_");
19
22
  }
20
23
 
21
- function walkSourceFiles(dir) {
24
+ async function walkSourceFiles(dir) {
22
25
  // Use git ls-files for speed (instant vs recursive walk)
23
26
  try {
24
- const output = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: dir, encoding: "utf8", timeout: 5000 });
25
- return output.trim().split("\n")
27
+ const { stdout } = await execFileAsync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: dir, encoding: "utf8", timeout: 5000 });
28
+ return stdout.trim().split("\n")
26
29
  .filter(f => f && SOURCE_EXTENSIONS.has(path.extname(f)) && !EXCLUDE_DIRS.has(f.split("/")[0]))
27
30
  .map(f => path.join(dir, f));
28
31
  } catch {
@@ -30,15 +33,15 @@ function walkSourceFiles(dir) {
30
33
  const files = [];
31
34
  let entries;
32
35
  try {
33
- entries = fs.readdirSync(dir, { withFileTypes: true });
34
- } catch {
36
+ entries = await fs.readdir(dir, { withFileTypes: true });
37
+ } catch { /* directory not readable */
35
38
  return files;
36
39
  }
37
40
  for (const entry of entries) {
38
41
  if (EXCLUDE_DIRS.has(entry.name)) continue;
39
42
  const full = path.join(dir, entry.name);
40
43
  if (entry.isDirectory()) {
41
- files.push(...walkSourceFiles(full));
44
+ files.push(...await walkSourceFiles(full));
42
45
  } else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
43
46
  files.push(full);
44
47
  }
@@ -47,19 +50,19 @@ function walkSourceFiles(dir) {
47
50
  }
48
51
  }
49
52
 
50
- function countLines(filePath) {
53
+ async function countLines(filePath) {
51
54
  try {
52
- const content = fs.readFileSync(filePath, "utf8");
55
+ const content = await fs.readFile(filePath, "utf8");
53
56
  return content.split("\n").length;
54
57
  } catch {
55
58
  return 0;
56
59
  }
57
60
  }
58
61
 
59
- function countDependencies(projectDir) {
62
+ async function countDependencies(projectDir) {
60
63
  const pkgPath = path.join(projectDir, "package.json");
61
64
  try {
62
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
65
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
63
66
  const deps = Object.keys(pkg.dependencies || {});
64
67
  const devDeps = Object.keys(pkg.devDependencies || {});
65
68
  return { dependencies: deps.length, devDependencies: devDeps.length, total: deps.length + devDeps.length };
@@ -89,7 +92,7 @@ function runDepcheck(projectDir) {
89
92
  });
90
93
  }
91
94
 
92
- function findDeadExports(sourceFiles) {
95
+ async function findDeadExports(sourceFiles) {
93
96
  const exportMap = new Map(); // file -> [exportName]
94
97
  const importedNames = new Set();
95
98
 
@@ -101,7 +104,7 @@ function findDeadExports(sourceFiles) {
101
104
  for (const file of sourceFiles) {
102
105
  let content;
103
106
  try {
104
- content = fs.readFileSync(file, "utf8");
107
+ content = await fs.readFile(file, "utf8");
105
108
  } catch {
106
109
  continue;
107
110
  }
@@ -139,15 +142,15 @@ function findDeadExports(sourceFiles) {
139
142
  }
140
143
 
141
144
  export async function measureBasalCost(projectDir) {
142
- const sourceFiles = walkSourceFiles(projectDir);
145
+ const sourceFiles = await walkSourceFiles(projectDir);
143
146
  let totalLines = 0;
144
147
  for (const f of sourceFiles) {
145
- totalLines += countLines(f);
148
+ totalLines += await countLines(f);
146
149
  }
147
150
 
148
- const depInfo = countDependencies(projectDir);
151
+ const depInfo = await countDependencies(projectDir);
149
152
  const depcheck = await runDepcheck(projectDir);
150
- const deadExports = findDeadExports(sourceFiles);
153
+ const deadExports = await findDeadExports(sourceFiles);
151
154
 
152
155
  return {
153
156
  totalLines,
@@ -162,20 +165,20 @@ function auditDir() {
162
165
  return path.join(getKarajanHome(), "audits");
163
166
  }
164
167
 
165
- export function loadPreviousAudit(projectDir) {
168
+ export async function loadPreviousAudit(projectDir) {
166
169
  const file = path.join(auditDir(), slugify(projectDir), "latest.json");
167
170
  try {
168
- return JSON.parse(fs.readFileSync(file, "utf8"));
171
+ return JSON.parse(await fs.readFile(file, "utf8"));
169
172
  } catch {
170
173
  return null;
171
174
  }
172
175
  }
173
176
 
174
- export function saveAuditSnapshot(projectDir, metrics) {
177
+ export async function saveAuditSnapshot(projectDir, metrics) {
175
178
  const dir = path.join(auditDir(), slugify(projectDir));
176
- fs.mkdirSync(dir, { recursive: true });
179
+ await fs.mkdir(dir, { recursive: true });
177
180
  const snapshot = { ...metrics, timestamp: new Date().toISOString() };
178
- fs.writeFileSync(path.join(dir, "latest.json"), JSON.stringify(snapshot, null, 2));
181
+ await fs.writeFile(path.join(dir, "latest.json"), JSON.stringify(snapshot, null, 2));
179
182
  return snapshot;
180
183
  }
181
184
 
package/src/bootstrap.js CHANGED
@@ -38,7 +38,7 @@ async function checkGitRepo() {
38
38
  let ok = false;
39
39
  try {
40
40
  ok = await ensureGitRepo();
41
- } catch {
41
+ } catch { /* git may not be installed */
42
42
  ok = false;
43
43
  }
44
44
  return {
@@ -156,7 +156,7 @@ async function readBootstrapFile(projectDir) {
156
156
  try {
157
157
  const raw = await fs.readFile(bootstrapPath(projectDir), "utf8");
158
158
  return JSON.parse(raw);
159
- } catch {
159
+ } catch { /* bootstrap file may not exist or contain invalid JSON */
160
160
  return null;
161
161
  }
162
162
  }
@@ -51,7 +51,7 @@ export async function setAgent(role, provider, { global: isGlobal = false } = {}
51
51
  const { setSessionOverride } = await import("../mcp/preflight.js");
52
52
  setSessionOverride(role, provider);
53
53
  return { role, provider, scope: "session" };
54
- } catch {
54
+ } catch { /* preflight module not available in CLI mode */
55
55
  // preflight module not available (CLI mode) — write to project config
56
56
  const projectConfigPath = getProjectConfigPath();
57
57
  const projectConfig = (await loadProjectConfig()) || {};
@@ -10,7 +10,7 @@ function readPid() {
10
10
  try {
11
11
  const pid = Number.parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
12
12
  return Number.isFinite(pid) ? pid : null;
13
- } catch {
13
+ } catch { /* PID file does not exist */
14
14
  return null;
15
15
  }
16
16
  }
@@ -19,7 +19,7 @@ function isProcessAlive(pid) {
19
19
  try {
20
20
  process.kill(pid, 0);
21
21
  return true;
22
- } catch {
22
+ } catch { /* process does not exist */
23
23
  return false;
24
24
  }
25
25
  }
@@ -55,7 +55,7 @@ export async function stopBoard() {
55
55
 
56
56
  try {
57
57
  process.kill(pid, "SIGTERM");
58
- } catch {
58
+ } catch { /* process already dead */
59
59
  // already dead
60
60
  }
61
61
  try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
@@ -13,7 +13,7 @@ export async function codeCommand({ task, config, logger }) {
13
13
  if (config.coder_rules) {
14
14
  try {
15
15
  coderRules = await fs.readFile(config.coder_rules, "utf8");
16
- } catch {
16
+ } catch { /* configured coder_rules path not found — try fallback */
17
17
  try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
18
18
  }
19
19
  }
@@ -44,7 +44,7 @@ async function checkGitRepo() {
44
44
  let gitOk = false;
45
45
  try {
46
46
  gitOk = await ensureGitRepo();
47
- } catch {
47
+ } catch { /* git check failed */
48
48
  gitOk = false;
49
49
  }
50
50
  return {
@@ -79,7 +79,7 @@ async function checkSonarQube(config) {
79
79
  if (config.sonarqube?.enabled !== false) {
80
80
  try {
81
81
  sonarOk = await isSonarReachable(sonarHost);
82
- } catch {
82
+ } catch { /* sonar check failed */
83
83
  sonarOk = false;
84
84
  }
85
85
  }
@@ -130,7 +130,7 @@ async function checkSerena() {
130
130
  try {
131
131
  const serenaCheck = await runCommand("serena", ["--version"]);
132
132
  serenaOk = serenaCheck.exitCode === 0;
133
- } catch {
133
+ } catch { /* serena not installed */
134
134
  serenaOk = false;
135
135
  }
136
136
  return {
@@ -181,7 +181,7 @@ async function checkBecariaSecrets() {
181
181
  detail: secretsOk ? "BECARIA_APP_ID + BECARIA_APP_PRIVATE_KEY found" : `Missing: ${missing}`,
182
182
  fix: secretsOk ? null : "Add BECARIA_APP_ID and BECARIA_APP_PRIVATE_KEY as GitHub repository secrets"
183
183
  };
184
- } catch {
184
+ } catch { /* GitHub API error */
185
185
  return null;
186
186
  }
187
187
  }
@@ -163,7 +163,7 @@ async function ensureCoderRules(coderRulesPath, logger) {
163
163
  let content;
164
164
  try {
165
165
  content = await fs.readFile(templatePath, "utf8");
166
- } catch {
166
+ } catch { /* template not found, use inline default */
167
167
  content = [
168
168
  "# Coder Rules",
169
169
  "",
@@ -333,7 +333,7 @@ export async function initCommand({ logger, flags = {} }) {
333
333
  try {
334
334
  const rtkRes = await runCommand("rtk", ["--version"]);
335
335
  hasRtk = rtkRes.exitCode === 0;
336
- } catch {
336
+ } catch { /* rtk not installed */
337
337
  hasRtk = false;
338
338
  }
339
339
  if (!hasRtk) {
@@ -312,7 +312,7 @@ async function findSessionsByPgTask(dir, pgTask) {
312
312
  if (session.pg_task_id === pgTask) {
313
313
  matches.push(entry);
314
314
  }
315
- } catch {
315
+ } catch { /* skip malformed session files */
316
316
  // skip malformed sessions
317
317
  }
318
318
  }
@@ -53,7 +53,7 @@ export async function reviewCommand({ task, config, logger, baseRef }) {
53
53
  let review;
54
54
  try {
55
55
  review = JSON.parse(result.output);
56
- } catch {
56
+ } catch { /* output is not JSON */
57
57
  review = { approved: true, summary: result.output };
58
58
  }
59
59
 
@@ -60,7 +60,7 @@ export async function showRole(roleName, config) {
60
60
  try {
61
61
  await fs.readFile(candidates[1], "utf8");
62
62
  source = "user";
63
- } catch {
63
+ } catch { /* user role file not found */
64
64
  source = "built-in";
65
65
  }
66
66
  }
package/src/hu/store.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { execSync } from "node:child_process";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
4
7
  import { getKarajanHome } from "../utils/paths.js";
5
8
 
6
9
  // FUTURE: hu-storage adapter for PG/Trello/etc — currently local files only
@@ -237,13 +240,14 @@ export function addSplittingMetadata(batch, huId, metadata) {
237
240
  /**
238
241
  * Detect project info from the given directory.
239
242
  * @param {string} cwd - Directory to detect project from.
240
- * @returns {{ name: string, remoteUrl: string|null }}
243
+ * @returns {Promise<{ name: string, remoteUrl: string|null }>}
241
244
  */
242
- export function detectProject(cwd) {
245
+ export async function detectProject(cwd) {
243
246
  const name = path.basename(cwd);
244
247
  let remoteUrl = null;
245
248
  try {
246
- remoteUrl = execSync("git remote get-url origin", { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
249
+ const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
250
+ remoteUrl = stdout.trim();
247
251
  } catch { /* not a git repo or no remote */ }
248
252
  return { name, remoteUrl };
249
253
  }
@@ -276,7 +280,7 @@ async function loadProjectBatch(projectDir) {
276
280
  try {
277
281
  const raw = await fs.readFile(filePath, "utf8");
278
282
  return JSON.parse(raw);
279
- } catch {
283
+ } catch { /* no saved batch for this project */
280
284
  return {
281
285
  project: projectSlug(projectDir),
282
286
  created_at: new Date().toISOString(),
@@ -8,7 +8,7 @@ export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = ()
8
8
  const timer = setInterval(() => {
9
9
  try {
10
10
  process.kill(parentPid, 0);
11
- } catch {
11
+ } catch { /* parent process is gone */
12
12
  clearInterval(timer);
13
13
  exitFn();
14
14
  }
@@ -201,7 +201,7 @@ export function buildAskQuestion(server) {
201
201
  }
202
202
  });
203
203
  return result.action === "accept" ? result.content?.answer || null : null;
204
- } catch {
204
+ } catch { /* elicitInput not supported by client */
205
205
  return null;
206
206
  }
207
207
  };
@@ -221,7 +221,7 @@ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, ru
221
221
  let session;
222
222
  try {
223
223
  session = await loadMostRecentSession();
224
- } catch {
224
+ } catch { /* no session store or read error */
225
225
  return null;
226
226
  }
227
227
  if (!session || !["failed", "stopped"].includes(session.status)) return null;
@@ -484,7 +484,7 @@ export async function handleCodeDirect(a, server, extra) {
484
484
  if (config.coder_rules) {
485
485
  try {
486
486
  coderRules = await fs.readFile(config.coder_rules, "utf8");
487
- } catch {
487
+ } catch { /* configured coder_rules path not found */
488
488
  try { coderRules = await fs.readFile("coder-rules.md", "utf8"); } catch { /* no coder rules file */ }
489
489
  }
490
490
  }
@@ -16,7 +16,7 @@ async function findActiveSession(projectDir) {
16
16
  let entries;
17
17
  try {
18
18
  entries = await fs.readdir(sessionRoot, { withFileTypes: true });
19
- } catch {
19
+ } catch { /* session root dir may not exist */
20
20
  return null;
21
21
  }
22
22
 
@@ -29,7 +29,7 @@ async function findActiveSession(projectDir) {
29
29
  if (session.status !== "running") continue;
30
30
  if (projectDir && session.projectDir && session.projectDir !== projectDir) continue;
31
31
  return session;
32
- } catch {
32
+ } catch { /* session.json missing or malformed */
33
33
  continue;
34
34
  }
35
35
  }
@@ -205,7 +205,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
205
205
  let orderedIds;
206
206
  try {
207
207
  orderedIds = topologicalSort(certifiedStories);
208
- } catch {
208
+ } catch { /* cyclic dependency */
209
209
  orderedIds = certifiedStories.map(s => s.id);
210
210
  }
211
211
 
@@ -677,7 +677,7 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
677
677
  let batch;
678
678
  try {
679
679
  batch = await loadHuBatch(batchSessionId);
680
- } catch {
680
+ } catch { /* no existing batch */
681
681
  batch = await createHuBatch(batchSessionId, stories);
682
682
  }
683
683
 
@@ -881,7 +881,7 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
881
881
  let orderedIds;
882
882
  try {
883
883
  orderedIds = topologicalSort(certifiedStories);
884
- } catch {
884
+ } catch { /* cyclic dependency */
885
885
  orderedIds = certifiedStories.map(s => s.id);
886
886
  }
887
887
 
@@ -13,16 +13,16 @@ import { checkBinary } from "../utils/agent-detect.js";
13
13
  import { isSonarReachable, sonarUp } from "../sonar/manager.js";
14
14
  import { runCommand } from "../utils/process.js";
15
15
  import { emitProgress, makeEvent } from "../utils/events.js";
16
- import { loadSonarCredentials } from "../sonar/credentials.js";
17
-
18
- function normalizeApiHost(rawHost) {
19
- return String(rawHost || "http://localhost:9000").replace(/host\.docker\.internal/g, "localhost");
20
- }
16
+ import {
17
+ resolveSonarHost,
18
+ resolveSonarToken,
19
+ resolveSonarCredentials,
20
+ } from "../sonar/config-resolver.js";
21
21
 
22
22
  function parseJsonSafe(text) {
23
23
  try {
24
24
  return JSON.parse(text);
25
- } catch {
25
+ } catch { /* invalid JSON */
26
26
  return null;
27
27
  }
28
28
  }
@@ -60,10 +60,10 @@ async function checkSonarReachable(host) {
60
60
  }
61
61
 
62
62
  async function checkSonarAuth(config) {
63
- const host = normalizeApiHost(config.sonarqube?.host);
63
+ const host = resolveSonarHost(config.sonarqube?.host);
64
64
 
65
65
  // Check explicit token first
66
- const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube?.token;
66
+ const explicitToken = resolveSonarToken(config);
67
67
  if (explicitToken) {
68
68
  // Validate the token works
69
69
  const res = await runCommand("curl", [
@@ -77,20 +77,14 @@ async function checkSonarAuth(config) {
77
77
  }
78
78
  }
79
79
 
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;
83
- const candidates = [
84
- process.env.KJ_SONAR_ADMIN_PASSWORD,
85
- config.sonarqube?.admin_password,
86
- fileCreds.password
87
- ].filter(Boolean);
80
+ // Try admin credentials via centralized resolver
81
+ const { user: adminUser, passwords } = await resolveSonarCredentials(config);
88
82
 
89
- if (!adminUser || candidates.length === 0) {
83
+ if (!adminUser || passwords.length === 0) {
90
84
  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
85
  }
92
86
 
93
- for (const password of [...new Set(candidates)]) {
87
+ for (const password of passwords) {
94
88
  const validateRes = await runCommand("curl", [
95
89
  "-sS", "-u", `${adminUser}:${password}`,
96
90
  `${host}/api/authentication/validate`
@@ -151,7 +145,7 @@ async function checkSecurityAgent(config) {
151
145
  export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
152
146
  const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
153
147
  const isExternalSonar = Boolean(config.sonarqube?.external);
154
- const sonarHost = normalizeApiHost(config.sonarqube?.host);
148
+ const sonarHost = resolveSonarHost(config.sonarqube?.host);
155
149
 
156
150
  const result = {
157
151
  ok: true,
@@ -21,7 +21,7 @@ async function listPluginFiles(dir) {
21
21
  return entries
22
22
  .filter((e) => e.isFile() && e.name.endsWith(".js"))
23
23
  .map((e) => path.join(dir, e.name));
24
- } catch {
24
+ } catch { /* plugin dir does not exist */
25
25
  return [];
26
26
  }
27
27
  }
@@ -69,7 +69,7 @@ export function parseArchitectOutput(raw) {
69
69
  let parsed;
70
70
  try {
71
71
  parsed = JSON.parse(jsonMatch[0]);
72
- } catch {
72
+ } catch { /* architect output is not valid JSON */
73
73
  return null;
74
74
  }
75
75
 
@@ -47,7 +47,7 @@ export function parseJsonOutput(raw) {
47
47
  try {
48
48
  const parsed = JSON.parse(cleaned);
49
49
  return normalizeReviewPayload(parsed);
50
- } catch {
50
+ } catch { /* not a single JSON blob */
51
51
  const lines = cleaned
52
52
  .split("\n")
53
53
  .map((x) => x.trim())
@@ -57,7 +57,7 @@ export function parseJsonOutput(raw) {
57
57
  for (const line of lines) {
58
58
  try {
59
59
  parsedLines.push(JSON.parse(line));
60
- } catch {
60
+ } catch { /* line is not JSON */
61
61
  continue;
62
62
  }
63
63
  }
@@ -71,7 +71,7 @@ export function parseJsonOutput(raw) {
71
71
  try {
72
72
  const parsed = JSON.parse(lines[i]);
73
73
  return normalizeReviewPayload(parsed);
74
- } catch {
74
+ } catch { /* line is not JSON */
75
75
  continue;
76
76
  }
77
77
  }
@@ -104,7 +104,7 @@ export class ArchitectRole extends BaseRole {
104
104
  summary: buildSummary(parsed),
105
105
  usage: result.usage
106
106
  };
107
- } catch {
107
+ } catch { /* agent output is not structured JSON */
108
108
  return {
109
109
  ok: true,
110
110
  result: {
@@ -63,7 +63,7 @@ export class AuditRole extends BaseRole {
63
63
  let growthDelta = null;
64
64
  try {
65
65
  basalCost = await measureBasalCost(projectDir);
66
- const previous = loadPreviousAudit(projectDir);
66
+ const previous = await loadPreviousAudit(projectDir);
67
67
  growthDelta = computeGrowthDelta(basalCost, previous);
68
68
  } catch {
69
69
  // basal cost is best-effort, don't fail the audit
@@ -95,7 +95,7 @@ export class AuditRole extends BaseRole {
95
95
  }
96
96
 
97
97
  if (basalCost) {
98
- try { saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ }
98
+ try { await saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ }
99
99
  }
100
100
 
101
101
  return {
@@ -111,7 +111,7 @@ export class AuditRole extends BaseRole {
111
111
  summary: buildSummary(parsed),
112
112
  usage: result.usage
113
113
  };
114
- } catch {
114
+ } catch { /* agent output is not structured JSON */
115
115
  return {
116
116
  ok: true,
117
117
  result: { raw: result.output, provider },
@@ -35,7 +35,7 @@ async function loadFirstExisting(paths) {
35
35
  for (const p of paths) {
36
36
  try {
37
37
  return await fs.readFile(p, "utf8");
38
- } catch {
38
+ } catch { /* file not found */
39
39
  continue;
40
40
  }
41
41
  }
@@ -115,7 +115,7 @@ export class DiscoverRole extends BaseRole {
115
115
  summary: buildSummary(parsed, mode),
116
116
  usage: result.usage
117
117
  };
118
- } catch {
118
+ } catch { /* agent output is not structured JSON */
119
119
  return buildUnstructuredResult(result.output, mode, provider, result.usage);
120
120
  }
121
121
  }
@@ -95,7 +95,7 @@ export class HuReviewerRole extends BaseRole {
95
95
  summary: buildSummary(parsed),
96
96
  usage: result.usage
97
97
  };
98
- } catch {
98
+ } catch { /* agent output is not structured JSON */
99
99
  return {
100
100
  ok: true,
101
101
  result: {
@@ -116,7 +116,7 @@ export class ResearcherRole extends BaseRole {
116
116
  },
117
117
  summary: buildSummary(parsed)
118
118
  };
119
- } catch {
119
+ } catch { /* agent output is not structured JSON */
120
120
  return {
121
121
  ok: true,
122
122
  result: {
@@ -112,7 +112,7 @@ export class TriageRole extends BaseRole {
112
112
  summary: `Triage: ${level} (${roles.length} role${roles.length === 1 ? "" : "s"})${decomposeNote}`,
113
113
  usage: result.usage
114
114
  };
115
- } catch {
115
+ } catch { /* agent output is not structured JSON */
116
116
  return {
117
117
  ok: true,
118
118
  result: {
@@ -71,7 +71,7 @@ export async function cleanupExpiredSessions({ logger } = {}) {
71
71
  let entries;
72
72
  try {
73
73
  entries = await fs.readdir(sessionRoot, { withFileTypes: true });
74
- } catch {
74
+ } catch { /* session root does not exist */
75
75
  return { removed: 0, errors: [] };
76
76
  }
77
77
 
@@ -67,14 +67,14 @@ export async function loadMostRecentSession() {
67
67
  let entries;
68
68
  try {
69
69
  entries = await fs.readdir(SESSION_ROOT, { withFileTypes: true });
70
- } catch {
70
+ } catch { /* session root does not exist yet */
71
71
  return null;
72
72
  }
73
73
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
74
74
  for (let i = dirs.length - 1; i >= 0; i--) {
75
75
  try {
76
76
  return await loadSession(dirs[i]);
77
- } catch {
77
+ } catch { /* session.json missing or malformed */
78
78
  continue;
79
79
  }
80
80
  }
@@ -22,7 +22,7 @@ export async function isOpenSkillsAvailable() {
22
22
  timeout: 15_000
23
23
  });
24
24
  return result.exitCode === 0;
25
- } catch {
25
+ } catch { /* openskills CLI not available */
26
26
  return false;
27
27
  }
28
28
  }
@@ -149,7 +149,7 @@ export async function autoInstallSkills(neededSkills, projectDir) {
149
149
  } else {
150
150
  result.failed.push(skillName);
151
151
  }
152
- } catch {
152
+ } catch { /* skill install threw */
153
153
  result.failed.push(skillName);
154
154
  }
155
155
  }
@@ -178,7 +178,7 @@ export async function cleanupAutoInstalledSkills(skillNames, projectDir) {
178
178
  } else {
179
179
  result.failed.push(name);
180
180
  }
181
- } catch {
181
+ } catch { /* skill removal threw */
182
182
  result.failed.push(name);
183
183
  }
184
184
  }
package/src/sonar/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { runCommand } from "../utils/process.js";
2
2
  import { withRetry } from "../utils/retry.js";
3
3
  import { resolveSonarProjectKey } from "./project-key.js";
4
+ import { resolveSonarToken } from "./config-resolver.js";
4
5
 
5
6
  export class SonarApiError extends Error {
6
7
  constructor(message, { url, httpStatus, hint } = {}) {
@@ -13,7 +14,7 @@ export class SonarApiError extends Error {
13
14
  }
14
15
 
15
16
  function tokenFromConfig(config) {
16
- return process.env.KJ_SONAR_TOKEN || config.sonarqube.token || "";
17
+ return resolveSonarToken(config) || "";
17
18
  }
18
19
 
19
20
  function parseHttpResponse(stdout) {
@@ -73,7 +74,7 @@ export async function getQualityGateStatus(config, projectKey = null) {
73
74
  try {
74
75
  const parsed = JSON.parse(body);
75
76
  return { ok: true, status: parsed.projectStatus?.status || "ERROR", conditions: parsed.projectStatus?.conditions || [], raw: parsed };
76
- } catch {
77
+ } catch { /* SonarQube response is not valid JSON */
77
78
  return { ok: false, status: "ERROR", conditions: [], raw: body };
78
79
  }
79
80
  }
@@ -85,7 +86,7 @@ export async function getOpenIssues(config, projectKey = null) {
85
86
  try {
86
87
  const parsed = JSON.parse(body);
87
88
  return { total: parsed.total || 0, issues: parsed.issues || [], raw: parsed };
88
- } catch {
89
+ } catch { /* SonarQube response is not valid JSON */
89
90
  return { total: 0, issues: [], raw: body };
90
91
  }
91
92
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Centralized SonarQube configuration resolution.
3
+ *
4
+ * Single source of truth for host URL, token, and admin credentials.
5
+ * Consumers: scanner.js, preflight-checks.js, api.js.
6
+ */
7
+
8
+ import { loadSonarCredentials } from "./credentials.js";
9
+
10
+ const DEFAULT_HOST = "http://localhost:9000";
11
+
12
+ /**
13
+ * Normalize SonarQube host URL for API calls (replace Docker-internal hostname).
14
+ * @param {string|undefined} rawHost - Raw host from config or env
15
+ * @returns {string} Normalized host URL without trailing slash
16
+ */
17
+ export function resolveSonarHost(rawHost) {
18
+ const host = String(rawHost || DEFAULT_HOST)
19
+ .replace(/host\.docker\.internal/g, "localhost")
20
+ .replace(/\/+$/, "");
21
+ return host;
22
+ }
23
+
24
+ /**
25
+ * Resolve SonarQube auth token from (in priority order):
26
+ * 1. KJ_SONAR_TOKEN env var
27
+ * 2. config.sonarqube.token
28
+ * 3. SONAR_TOKEN env var
29
+ *
30
+ * @param {object} [config={}] - Karajan config object
31
+ * @returns {string|null} Token string or null if none found
32
+ */
33
+ export function resolveSonarToken(config = {}) {
34
+ const token =
35
+ process.env.KJ_SONAR_TOKEN ||
36
+ config.sonarqube?.token ||
37
+ process.env.SONAR_TOKEN ||
38
+ null;
39
+ return token || null;
40
+ }
41
+
42
+ /**
43
+ * Resolve SonarQube admin credentials from (in priority order):
44
+ * 1. Env vars: KJ_SONAR_ADMIN_USER / KJ_SONAR_ADMIN_PASSWORD
45
+ * 2. Config: sonarqube.admin_user / sonarqube.admin_password
46
+ * 3. Credentials file: ~/.karajan/sonar-credentials.json
47
+ *
48
+ * Returns all candidate passwords (de-duped) so callers can try each.
49
+ *
50
+ * @param {object} [config={}] - Karajan config object
51
+ * @returns {Promise<{ user: string|null, passwords: string[] }>}
52
+ */
53
+ export async function resolveSonarCredentials(config = {}) {
54
+ const fileCreds = (await loadSonarCredentials()) || {};
55
+
56
+ const user =
57
+ process.env.KJ_SONAR_ADMIN_USER ||
58
+ config.sonarqube?.admin_user ||
59
+ fileCreds.user ||
60
+ null;
61
+
62
+ const candidates = [
63
+ process.env.KJ_SONAR_ADMIN_PASSWORD,
64
+ config.sonarqube?.admin_password,
65
+ fileCreds.password,
66
+ ].filter(Boolean);
67
+
68
+ return { user, passwords: [...new Set(candidates)] };
69
+ }
@@ -25,7 +25,7 @@ export async function loadSonarCredentials() {
25
25
  user: data.user || null,
26
26
  password: data.password || null
27
27
  };
28
- } catch {
28
+ } catch { /* credentials file may not exist */
29
29
  return { user: null, password: null };
30
30
  }
31
31
  }
@@ -30,7 +30,7 @@ function parseUrlLikeRemote(remoteUrl) {
30
30
  try {
31
31
  const parsed = new URL(String(remoteUrl || "").trim());
32
32
  return { host: parsed.hostname, path: parsed.pathname.replace(/^\/+/, "") };
33
- } catch {
33
+ } catch { /* not a valid URL */
34
34
  return null;
35
35
  }
36
36
  }
@@ -4,7 +4,11 @@ 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
+ import {
8
+ resolveSonarHost,
9
+ resolveSonarToken as resolveToken,
10
+ resolveSonarCredentials,
11
+ } from "./config-resolver.js";
8
12
 
9
13
  export function buildScannerOpts(projectKey, scanner = {}) {
10
14
  const opts = [`-Dsonar.projectKey=${projectKey}`];
@@ -45,15 +49,11 @@ function normalizeScannerConfig(scanner = {}) {
45
49
  function parseJsonSafe(text) {
46
50
  try {
47
51
  return JSON.parse(text);
48
- } catch {
52
+ } catch { /* invalid JSON */
49
53
  return null;
50
54
  }
51
55
  }
52
56
 
53
- function normalizeApiHost(rawHost) {
54
- return String(rawHost || "http://localhost:9000").replaceAll("host.docker.internal", "localhost");
55
- }
56
-
57
57
  async function validateAdminCredentials(host, user, password) {
58
58
  const res = await runCommand("curl", [
59
59
  "-sS",
@@ -133,22 +133,14 @@ async function maybeRunCoverage(config) {
133
133
  return checkLcovExists(lcovPath, blockOnFailure, run.stdout || "");
134
134
  }
135
135
 
136
- async function resolveSonarToken(config, apiHost) {
137
- const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube.token;
136
+ async function resolveSonarTokenWithFallback(config, apiHost) {
137
+ const explicitToken = resolveToken(config);
138
138
  if (explicitToken) return explicitToken;
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;
143
- const candidates = [
144
- process.env.KJ_SONAR_ADMIN_PASSWORD,
145
- config.sonarqube.admin_password,
146
- fileCreds.password
147
- ].filter(Boolean);
148
-
149
- if (!adminUser || candidates.length === 0) return null;
140
+ const { user: adminUser, passwords } = await resolveSonarCredentials(config);
141
+ if (!adminUser || passwords.length === 0) return null;
150
142
 
151
- for (const password of new Set(candidates)) {
143
+ for (const password of passwords) {
152
144
  const valid = await validateAdminCredentials(apiHost, adminUser, password);
153
145
  if (!valid) continue;
154
146
  const token = await generateUserToken(apiHost, adminUser, password);
@@ -205,7 +197,7 @@ export async function runSonarScan(config, projectKey = null) {
205
197
  ? Number(sonarConfig.timeouts.scanner_ms)
206
198
  : 15 * 60 * 1000;
207
199
  const sonarNetwork = sonarConfig.network || "karajan_sonar_net";
208
- const apiHost = normalizeApiHost(rawHost);
200
+ const apiHost = resolveSonarHost(rawHost);
209
201
  const isLocalHost = /localhost|127\.0\.0\.1/.test(rawHost);
210
202
  const host = isLocalHost ? rawHost.replaceAll(/localhost|127\.0\.0\.1/g, "host.docker.internal") : rawHost;
211
203
 
@@ -219,7 +211,7 @@ export async function runSonarScan(config, projectKey = null) {
219
211
  };
220
212
  }
221
213
  await ensureSonarProjectProperties();
222
- const token = await resolveSonarToken(config, apiHost);
214
+ const token = await resolveSonarTokenWithFallback(config, apiHost);
223
215
  if (!token) {
224
216
  return {
225
217
  ok: false,
package/src/utils/fs.js CHANGED
@@ -9,7 +9,7 @@ export async function exists(filePath) {
9
9
  try {
10
10
  await fs.access(filePath);
11
11
  return true;
12
- } catch {
12
+ } catch { /* file does not exist */
13
13
  return false;
14
14
  }
15
15
  }
@@ -54,7 +54,7 @@ export function extractFirstJson(raw) {
54
54
  if (depth === 0) {
55
55
  try {
56
56
  return JSON.parse(str.substring(start, i + 1));
57
- } catch {
57
+ } catch { /* matched braces but not valid JSON */
58
58
  return null;
59
59
  }
60
60
  }
@@ -120,7 +120,7 @@ export async function detectSonarConfig(cwd = process.cwd()) {
120
120
  try {
121
121
  await fs.access(path.join(cwd, "sonar-project.properties"));
122
122
  return { configured: true };
123
- } catch {
123
+ } catch { /* sonar-project.properties does not exist */
124
124
  return { configured: false };
125
125
  }
126
126
  }
@@ -12,7 +12,7 @@ export async function detectRtk() {
12
12
  return { available: true, version };
13
13
  }
14
14
  return { available: false, version: null };
15
- } catch {
15
+ } catch { /* rtk binary not found */
16
16
  return { available: false, version: null };
17
17
  }
18
18
  }