karajan-code 2.19.1 → 2.19.2

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 CHANGED
@@ -345,6 +345,8 @@ Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
345
345
 
346
346
  ## Recent releases
347
347
 
348
+ > **v2.19.2 released** — Patch. **SonarQube 401 now triggers automatic token re-bootstrap** instead of failing the run (KJC-BUG-0057). Until v2.19.1, when the Sonar token was missing / stale / revoked / pointed at a recreated Sonar instance, `kj run` / `kj audit` threw `SonarQube authentication failed (HTTP 401)` with the hint "Regenerate with `kj init`" — putting the user in the loop for plumbing Karajan can do itself. PR #793 wires `src/sonar/api.js::sonarFetchOnce` to invoke a new `src/sonar/token-recovery.js::recoverSonarToken()` on the first 401, which reuses `bootstrapSonarToken()` (already shipped in v2.10.2) — probes admin/admin, rotates the default password if still in place, revokes the existing `karajan-cli` token, generates a fresh GLOBAL_ANALYSIS_TOKEN, mutates `config.sonarqube.token` in place AND mirrors it to `~/.karajan/sonar-credentials.json` so future processes pick it up via the normal resolver chain instead of triggering recovery again. The original request retries once with the new token; the user never sees the 401 when recovery succeeds. Per-process latch ensures N endpoints 401-ing trigger ONE bootstrap, not N. Programmatic, **zero LLM involvement** — exactly the kind of plumbing Karajan should never delegate to the agent. Reported by [@aitormf](https://github.com/aitormf).
349
+ >
348
350
  > **v2.19.1 released** — Patch. **APPLICATION BLOCKER fix** for the HU Board: every fresh `npm install -g karajan-code` since the HU Board feature shipped was producing tarballs without the `packages/hu-board/` directory (the `files` array in `package.json` did not include it), and even when the user copied that directory manually, the board crashed at startup with `Cannot find package 'helmet' imported from .../packages/hu-board/src/server.js` because its five dependencies (`helmet`, `chokidar`, `better-sqlite3`, `express`, `express-rate-limit`) were declared in `packages/hu-board/package.json` but missing from the root `dependencies`. PR #791 adds `packages/hu-board/{src,public,package.json}` to `files`, adds the five deps at root at the exact same versions so `npm dedupe` collapses to one copy resolvable by upward traversal, and regenerates `package-lock.json`. Verified end-to-end: `npm pack --dry-run` now ships 12 board files; `node packages/hu-board/src/server.js` boots cleanly. Also internal: 38 direct `os.homedir()` callers routed through the unified resolver (PR #790, `KARAJAN_HOME` now redirects every component) and 5 inline constructions of `~/.karajan/hu-board-runs/` unified under one helper (PR #789). Reported by [@aitormf](https://github.com/aitormf). **If you tried v2.19.0 and `kj board start` failed, upgrade to v2.19.1.**
349
351
  >
350
352
  > **v2.19.0 released** — Minor. Consolidates the HOME-level state of Karajan into a single root: **`~/.karajan/`**. Previously `~/.kj/` held plans, hibernated standby state, run-registry entries and worktrees, while `~/.karajan/` held sessions, hu-stories, config and the rest — no ADR justified the split and four divergent `getKjHome()` implementations had drifted. Three internal PRs land the change without breaking anyone: PR #781 unifies the resolver and introduces **`KARAJAN_HOME`** as the canonical env var (with `KJ_HOME` honoured + one-shot deprecation warning); PR #782 ships an idempotent **auto-migrator** that runs once on the next `kj` invocation — tarball backup at `~/.karajan/backup/kj-pre-migration-<ISO>.tar.gz` BEFORE moving, marker file `~/.karajan/.kj-migrated.json` prevents re-runs, cross-device safe (`rename` → `cp + rm` on `EXDEV`); PR #783 flips every default to `~/.karajan/` and adds a `legacy-kj-home` check to `kj doctor`. The HU Board reads BOTH locations until the migrator fires, so users who start the board first never see "missing plans". Restore is one `tar -xzf ~/.karajan/backup/kj-pre-migration-<ts>.tar.gz -C ~` away. Safe upgrade from 2.18.x.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.19.1",
3
+ "version": "2.19.2",
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",
package/src/sonar/api.js CHANGED
@@ -3,6 +3,7 @@ import { withRetry } from "../utils/retry.js";
3
3
  import { resolveSonarProjectKey } from "./project-key.js";
4
4
  import { resolveSonarToken } from "./config-resolver.js";
5
5
  import { filterFalsePositives } from "./issue-filter.js";
6
+ import { recoverSonarToken } from "./token-recovery.js";
6
7
 
7
8
  class SonarApiError extends Error {
8
9
  constructor(message, { url, httpStatus, hint } = {}) {
@@ -25,7 +26,7 @@ function parseHttpResponse(stdout) {
25
26
  return { httpCode, body };
26
27
  }
27
28
 
28
- async function sonarFetchOnce(config, urlPath) {
29
+ async function sonarFetchOnce(config, urlPath, { _retriedAfterRecovery = false, logger = null } = {}) {
29
30
  const token = tokenFromConfig(config);
30
31
  const url = `${config.sonarqube.host}${urlPath}`;
31
32
  const res = await runCommand("curl", ["-s", "-w", "\n%{http_code}", "-u", `${token}:`, url]);
@@ -42,9 +43,22 @@ async function sonarFetchOnce(config, urlPath) {
42
43
  const { httpCode, body } = parseHttpResponse(res.stdout);
43
44
 
44
45
  if (httpCode === 401) {
46
+ // KJC-BUG-0057 (v2.19.2): try to bootstrap a fresh token via the Sonar
47
+ // REST API and retry ONCE. If recovery still fails — or this was already
48
+ // the retry — surface the actionable error like before.
49
+ if (!_retriedAfterRecovery) {
50
+ const recovery = await recoverSonarToken(config, logger);
51
+ if (recovery.ok) {
52
+ return sonarFetchOnce(config, urlPath, { _retriedAfterRecovery: true, logger });
53
+ }
54
+ throw new SonarApiError(
55
+ `SonarQube authentication failed (HTTP 401) and auto-recovery failed: ${recovery.reason}. Regenerate the token manually with 'kj init' or save admin user/password in ~/.karajan/sonar-credentials.json.`,
56
+ { url, httpStatus: 401, hint: "Run 'kj init' to regenerate the SonarQube token, or save admin credentials in ~/.karajan/sonar-credentials.json so Karajan can re-bootstrap automatically." }
57
+ );
58
+ }
45
59
  throw new SonarApiError(
46
- `SonarQube authentication failed (HTTP 401). Token may be invalid or expired. Regenerate with 'kj init'.`,
47
- { url, httpStatus: 401, hint: "Run 'kj init' to regenerate the SonarQube token." }
60
+ `SonarQube authentication failed (HTTP 401) even after auto-recovery. The newly generated token is also being rejected — the Sonar instance may be in an inconsistent state.`,
61
+ { url, httpStatus: 401, hint: "Inspect the Sonar UI directly; consider 'kj sonar restart'." }
48
62
  );
49
63
  }
50
64
 
@@ -0,0 +1,67 @@
1
+ // Sonar 401 auto-recovery — KJC-BUG-0057 / v2.19.2.
2
+ //
3
+ // When the Sonar API returns 401 (token missing / invalid / expired / revoked)
4
+ // and we still have admin/admin (or a user/pass we saved at `kj init` time),
5
+ // regenerate the analysis token VIA THE REST API and persist it. The caller
6
+ // retries the original request once with the new token.
7
+ //
8
+ // Programmatic, no LLM involvement. The whole point of the user feedback was
9
+ // that this is plumbing, not a decision: Karajan has the credentials to
10
+ // re-bootstrap and should do it without asking.
11
+
12
+ import { bootstrapSonarToken } from "./token-bootstrap.js";
13
+ import { saveSonarToken } from "./credentials.js";
14
+
15
+ // Per-process latch: a single Sonar run that 401s on every endpoint must
16
+ // only trigger ONE bootstrap attempt — not one per endpoint. The latch
17
+ // resets when the process exits.
18
+ let _recoveryAttempted = false;
19
+
20
+ /**
21
+ * @internal Reset the latch between tests (mirrors __resetKjHomeWarningForTests).
22
+ */
23
+ export function __resetSonarRecoveryLatchForTests() {
24
+ _recoveryAttempted = false;
25
+ }
26
+
27
+ /**
28
+ * Attempt to re-bootstrap the Sonar token after a 401. Mutates `config`
29
+ * in place when successful so the immediate retry picks up the new token.
30
+ *
31
+ * @param {object} config Karajan config (will be mutated on success).
32
+ * @param {object} [logger]
33
+ * @returns {Promise<{ ok: boolean, reason?: string, newToken?: string, savedTo?: string, passwordRotated?: boolean }>}
34
+ */
35
+ export async function recoverSonarToken(config, logger = null) {
36
+ if (_recoveryAttempted) {
37
+ return { ok: false, reason: "recovery already attempted in this process" };
38
+ }
39
+ _recoveryAttempted = true;
40
+
41
+ const host = config?.sonarqube?.host || "http://localhost:9000";
42
+ logger?.warn?.(`[sonar-recovery] 401 from ${host} — attempting token re-bootstrap`);
43
+
44
+ const result = await bootstrapSonarToken({ host });
45
+
46
+ if (!result.ok) {
47
+ logger?.warn?.(`[sonar-recovery] bootstrap failed: ${result.reason}`);
48
+ return { ok: false, reason: result.reason };
49
+ }
50
+
51
+ // Mutate the in-memory config so the caller's retry sees the new token.
52
+ config.sonarqube = config.sonarqube || {};
53
+ config.sonarqube.token = result.token;
54
+
55
+ // Also persist to ~/.karajan/sonar-credentials.json so future processes
56
+ // pick it up via the normal config-resolver chain instead of triggering
57
+ // recovery again. token-bootstrap.js already wrote sonar.token; this
58
+ // mirrors it into the structured credentials file the resolver reads.
59
+ try {
60
+ await saveSonarToken(result.token);
61
+ } catch (err) {
62
+ logger?.warn?.(`[sonar-recovery] persist to sonar-credentials.json failed (non-blocking): ${err.message}`);
63
+ }
64
+
65
+ logger?.info?.(`[sonar-recovery] new token persisted to ${result.savedTo}${result.passwordRotated ? " (admin password rotated)" : ""} — retrying original request`);
66
+ return { ok: true, newToken: result.token, savedTo: result.savedTo, passwordRotated: result.passwordRotated };
67
+ }