karajan-code 2.19.2 → 2.19.3

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.3 released** — Patch. **HU Board now reads + writes plans from the canonical home dir** (KJC-BUG-0059). Reported by [@aitormf](https://github.com/aitormf): the top card showed `Directorio del proyecto — no detectado` even when the run had a valid `projectDir` and the coder was reading files from it. Root cause: five board call sites still hard-coded `~/.kj/plans/` as their plans root — leftover from the v2.19.0 home consolidation, which fixed `sync.js` but missed the rest. After the auto-migrator ran (or the user created new plans post-v2.19.0), plans landed under `~/.karajan/plans/<slug>/`; the board kept looking under `~/.kj/plans/<slug>/` and silently found nothing — so `GET /api/projects/:id/preflight` could not extract `projectDir` (the literal Aitor saw), `GET /api/projects/:id/plans-outcome` returned `plans: []` for every project, `DELETE /api/projects/:id` swept the wrong path leaving residue on disk, `DELETE /api/plans/:planId` failed silently, `plan-mutations.plansRoot` wrote new per-HU run logs to the legacy root splitting state across both, and `cleanup-zombies` never GC'd zombies under `~/.karajan/plans/`. PR #795 ships three new exports in `packages/hu-board/src/db.js`: `getHuBoardPlansDir()` (canonical, or `KJ_PLANS_DIR` override), `getHuBoardLegacyPlansDir()` (legacy, null when override set), `getHuBoardPlansDirs()` ordered `[canonical, legacy?]` for read callers. Single-write callers (`plan-mutations`) use the canonical root; read / delete / GC iterate both so users mid-migration with plans still under `~/.kj/` don't regress. 29 hu-board test files / 349 tests still green. **If your board shows "Directorio del proyecto — no detectado" on a project that clearly works in CLI, upgrade to v2.19.3.**
349
+ >
348
350
  > **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
351
  >
350
352
  > **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.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.19.2",
3
+ "version": "2.19.3",
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",
@@ -47,8 +47,12 @@ function isEphemeralId(id) {
47
47
  return EPHEMERAL_PREFIXES.some((p) => id.startsWith(p));
48
48
  }
49
49
 
50
- function plansRoot() {
51
- return process.env.KJ_PLANS_DIR || path.join(homedir(), '.kj', 'plans');
50
+ // KJC-BUG-0059 (v2.19.3): cleanup walks all known plan roots,
51
+ // canonical first then legacy. Pre-fix hard-coded `~/.kj/plans/`
52
+ // missed every plan post-v2.19.0 → zombies never garbage-collected.
53
+ import { getHuBoardPlansDirs } from './db.js';
54
+ function plansRoots() {
55
+ return getHuBoardPlansDirs();
52
56
  }
53
57
 
54
58
  function olderThanDays(isoStr, days) {
@@ -90,7 +94,7 @@ export function cleanupZombies(opts = {}) {
90
94
  const fsPaths = [
91
95
  ...storyIds.map((sid) => path.join(getKjHome(), 'hu-stories', sid)),
92
96
  ...sessionIds.map((sid) => path.join(getKjHome(), 'sessions', sid)),
93
- path.join(plansRoot(), proj.id),
97
+ ...plansRoots().map((root) => path.join(root, proj.id)),
94
98
  ];
95
99
  deleteProject(proj.id);
96
100
  addTombstone('project', proj.id, { source: 'auto-cleanup', fsPaths });
@@ -56,6 +56,51 @@ export function getHuBoardRunsDir() {
56
56
  return join(getKjHome(), 'hu-board-runs');
57
57
  }
58
58
 
59
+ /**
60
+ * Canonical path to the plans directory the board scans
61
+ * (`<karajan-home>/plans/`). Respects `KJ_PLANS_DIR` as an absolute
62
+ * override, mirroring sync.js. KJC-BUG-0059 (v2.19.3): preflight
63
+ * endpoint used a hard-coded `~/.kj/plans/` legacy fallback that
64
+ * survived the v2.19.0 consolidation, causing the board to report
65
+ * "directorio del proyecto: no detectado" even when the plan on disk
66
+ * had a valid `projectDir`.
67
+ *
68
+ * @returns {string}
69
+ */
70
+ export function getHuBoardPlansDir() {
71
+ return process.env.KJ_PLANS_DIR || join(getKjHome(), 'plans');
72
+ }
73
+
74
+ /**
75
+ * Legacy `~/.kj/plans/` directory — only returned when KJ_PLANS_DIR
76
+ * is unset (it would be the same path anyway when overridden).
77
+ * Callers iterate canonical + legacy so users who have not yet run
78
+ * the v2.19.0 auto-migrator still see their plans. KJC-BUG-0059.
79
+ *
80
+ * @returns {string|null}
81
+ */
82
+ export function getHuBoardLegacyPlansDir() {
83
+ if (process.env.KJ_PLANS_DIR) return null;
84
+ return join(process.env.HOME || '/root', '.kj', 'plans');
85
+ }
86
+
87
+ /**
88
+ * Ordered list of every plans root the board should scan: canonical
89
+ * first (`~/.karajan/plans/`), legacy second when applicable
90
+ * (`~/.kj/plans/`). Honours `KJ_PLANS_DIR` as a single override,
91
+ * matching `sync.js`'s fullScan / startWatcher precedence so the
92
+ * preflight, plans-outcome, delete-project and delete-plan endpoints
93
+ * see the same set of files the watcher sees. KJC-BUG-0059.
94
+ *
95
+ * @returns {string[]}
96
+ */
97
+ export function getHuBoardPlansDirs() {
98
+ const dirs = [getHuBoardPlansDir()];
99
+ const legacy = getHuBoardLegacyPlansDir();
100
+ if (legacy) dirs.push(legacy);
101
+ return dirs;
102
+ }
103
+
59
104
  // Bump when the schema gains a column / index / table that older
60
105
  // Karajan versions cannot understand. A DB written by a newer Karajan
61
106
  // refuses to open here so it does not get silently downgraded.
@@ -40,10 +40,13 @@ const KJ_CLI = join(REPO_ROOT, 'src', 'cli.js');
40
40
  * `KJ_PLANS_DIR` (tests) then `KJ_HOME/plans` (power users) then
41
41
  * `~/.kj/plans/` (default) — same precedence as `sync.js::fullScan`.
42
42
  */
43
+ // KJC-BUG-0059: write operations go to the canonical plans dir.
44
+ // Read operations should iterate getHuBoardPlansDirs() instead, to
45
+ // pick up plans the user has under the legacy ~/.kj/plans/ tree
46
+ // while the auto-migrator has not yet fired.
47
+ import { getHuBoardPlansDir } from './db.js';
43
48
  function plansRoot() {
44
- if (process.env.KJ_PLANS_DIR) return process.env.KJ_PLANS_DIR;
45
- if (process.env.KJ_HOME) return join(process.env.KJ_HOME, 'plans');
46
- return join(homedir(), '.kj', 'plans');
49
+ return getHuBoardPlansDir();
47
50
  }
48
51
 
49
52
  /**
@@ -30,6 +30,7 @@
30
30
  import { execSync } from 'node:child_process';
31
31
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
32
32
  import { join } from 'node:path';
33
+ import { getHuBoardPlansDirs } from './db.js';
33
34
 
34
35
  function runQuiet(cmd, opts = {}) {
35
36
  try {
@@ -221,13 +222,13 @@ function checkConfigYml() {
221
222
  }
222
223
 
223
224
  function checkPlans(projectId) {
224
- // Plans live at ~/.kj/plans/<projectId>/*.json (or
225
- // $KJ_HOME/plans/<projectId>/*.json under VITEST or custom config).
226
- // Mirror plan-mutations.js::plansRoot() so the path matches reality.
227
- const dir = process.env.KJ_HOME
228
- ? join(process.env.KJ_HOME, 'plans', projectId)
229
- : join(process.env.HOME || '', '.kj', 'plans', projectId);
230
- if (!existsSync(dir)) {
225
+ // KJC-BUG-0059 (v2.19.3): scan canonical + legacy roots, same as
226
+ // the preflight route. Pre-fix this hard-coded `~/.kj/plans/`
227
+ // missed every plan post-v2.19.0 → preflight reported "sin plans
228
+ // aún" even when the user had a valid plan on disk.
229
+ const dirs = getHuBoardPlansDirs().map((root) => join(root, projectId));
230
+ const dir = dirs.find((d) => existsSync(d));
231
+ if (!dir) {
231
232
  return {
232
233
  id: 'plans', label: 'Plans en disco', status: 'info',
233
234
  detail: 'sin plans aún',
@@ -15,6 +15,9 @@ import {
15
15
  deleteSession,
16
16
  getKjHome,
17
17
  getHuBoardRunsDir,
18
+ getHuBoardPlansDir,
19
+ getHuBoardLegacyPlansDir,
20
+ getHuBoardPlansDirs,
18
21
  getStoryRow,
19
22
  listPlanIdsForProject,
20
23
  updateStoryStatus,
@@ -192,22 +195,23 @@ router.get('/projects/:id/preflight', async (req, res) => {
192
195
  try {
193
196
  const projectId = req.params.id;
194
197
  let projectDir = null;
198
+ // KJC-BUG-0059 (v2.19.3): scan plans in BOTH the canonical
199
+ // ~/.karajan/plans/ AND the legacy ~/.kj/plans/ — the legacy
200
+ // fallback covers users who started the board before running any
201
+ // `kj` command on a fresh install (so the auto-migrator from
202
+ // v2.19.0 has not fired yet). Pre-fix, this endpoint hard-coded
203
+ // ~/.kj/plans/ as the default, missing every plan post-2.19.0
204
+ // → board card showed "directorio del proyecto: no detectado"
205
+ // even though the plan on disk had a valid projectDir.
195
206
  try {
196
- // Plans live at ~/.kj/plans/<slug>/ when KJ_HOME is unset, or
197
- // $KJ_HOME/plans/<slug>/ when set. db.js::getKjHome() returns
198
- // the .karajan root, NOT the .kj root — they are sister dirs.
199
- // Mirror plan-mutations.js::plansRoot() exactly.
200
- const plansBase = process.env.KJ_HOME
201
- ? path.join(process.env.KJ_HOME, 'plans')
202
- : path.join(process.env.HOME || '', '.kj', 'plans');
203
- const plansDir = path.join(plansBase, projectId);
204
- if (fs.existsSync(plansDir)) {
205
- const files = fs.readdirSync(plansDir).filter((f) => f.endsWith('.json'));
206
- for (const f of files) {
207
+ outer: for (const root of getHuBoardPlansDirs()) {
208
+ const plansDir = path.join(root, projectId);
209
+ if (!fs.existsSync(plansDir)) continue;
210
+ for (const f of fs.readdirSync(plansDir).filter((x) => x.endsWith('.json'))) {
207
211
  try {
208
212
  const plan = JSON.parse(fs.readFileSync(path.join(plansDir, f), 'utf8'));
209
- if (plan?.projectDir) { projectDir = plan.projectDir; break; }
210
- } catch { /* try next */ }
213
+ if (plan?.projectDir) { projectDir = plan.projectDir; break outer; }
214
+ } catch { /* try next file */ }
211
215
  }
212
216
  }
213
217
  } catch { /* projectDir stays null → preflight will report it */ }
@@ -229,17 +233,17 @@ router.get('/projects/:id/preflight', async (req, res) => {
229
233
  router.get('/projects/:id/plans-outcome', (req, res) => {
230
234
  try {
231
235
  const projectId = req.params.id;
232
- const plansBase = process.env.KJ_HOME
233
- ? path.join(process.env.KJ_HOME, 'plans')
234
- : path.join(process.env.HOME || '', '.kj', 'plans');
235
- const plansDir = path.join(plansBase, projectId);
236
- if (!fs.existsSync(plansDir)) return res.json({ projectId, plans: [] });
236
+ // KJC-BUG-0059 (v2.19.3): scan canonical + legacy roots so users
237
+ // who have not yet run the auto-migrator still see their plans.
237
238
  const out = [];
238
- for (const f of fs.readdirSync(plansDir).filter(n => n.endsWith('.json'))) {
239
- try {
240
- const plan = JSON.parse(fs.readFileSync(path.join(plansDir, f), 'utf8'));
241
- if (plan?.version !== 2) continue;
242
- out.push({
239
+ for (const root of getHuBoardPlansDirs()) {
240
+ const plansDir = path.join(root, projectId);
241
+ if (!fs.existsSync(plansDir)) continue;
242
+ for (const f of fs.readdirSync(plansDir).filter(n => n.endsWith('.json'))) {
243
+ try {
244
+ const plan = JSON.parse(fs.readFileSync(path.join(plansDir, f), 'utf8'));
245
+ if (plan?.version !== 2) continue;
246
+ out.push({
243
247
  planId: plan.planId,
244
248
  name: plan.name || (plan.task ? plan.task.slice(0, 80) : plan.planId),
245
249
  status: plan.status || 'draft',
@@ -248,6 +252,7 @@ router.get('/projects/:id/plans-outcome', (req, res) => {
248
252
  huCount: Array.isArray(plan.hus) ? plan.hus.length : 0,
249
253
  });
250
254
  } catch { /* skip unreadable plan */ }
255
+ }
251
256
  }
252
257
  res.json({ projectId, plans: out });
253
258
  } catch (err) {
@@ -356,12 +361,13 @@ router.delete('/projects/:id', (req, res) => {
356
361
  // ~/.kj/plans relative to KJ_HOME's parent, which broke when the
357
362
  // two homes were configured separately and left orphan plan dirs
358
363
  // on disk after a 🗑️ delete.
359
- const plansRoot = process.env.KJ_PLANS_DIR
360
- || path.join(os.homedir(), '.kj', 'plans');
364
+ // KJC-BUG-0059 (v2.19.3): when removing a deleted project's
365
+ // residual plan dirs, sweep BOTH the canonical and legacy roots
366
+ // so the cleanup is consistent for users mid-migration.
361
367
  const fsPaths = [
362
368
  ...storyIds.map((sid) => path.join(huStoriesDir(), sid)),
363
369
  ...sessionIds.map((sid) => path.join(getKjHome(), 'sessions', sid)),
364
- path.join(plansRoot, id),
370
+ ...getHuBoardPlansDirs().map((root) => path.join(root, id)),
365
371
  ];
366
372
 
367
373
  const existed = deleteProject(id);
@@ -464,13 +470,18 @@ router.delete('/sessions/:id', (req, res) => {
464
470
  router.delete('/plans/:planId', (req, res) => {
465
471
  try {
466
472
  const planId = req.params.planId;
467
- // Locate the plan file by scanning the plans dir for filename match.
468
- const plansRoot = process.env.KJ_PLANS_DIR || path.join(getKjHome(), '..', '.kj', 'plans');
473
+ // Locate the plan file by scanning both canonical and legacy plans
474
+ // roots — KJC-BUG-0059 (v2.19.3). Pre-fix this hard-coded
475
+ // `getKjHome()/../.kj/plans` which silently misses plans saved by
476
+ // post-2.19.0 callers under `~/.karajan/plans/`.
469
477
  let foundPath = null;
470
478
  try {
471
- for (const projSlug of fs.readdirSync(plansRoot)) {
472
- const candidate = path.join(plansRoot, projSlug, `plan-${planId}.json`);
473
- if (fs.existsSync(candidate)) { foundPath = candidate; break; }
479
+ outer: for (const plansRoot of getHuBoardPlansDirs()) {
480
+ if (!fs.existsSync(plansRoot)) continue;
481
+ for (const projSlug of fs.readdirSync(plansRoot)) {
482
+ const candidate = path.join(plansRoot, projSlug, `plan-${planId}.json`);
483
+ if (fs.existsSync(candidate)) { foundPath = candidate; break outer; }
484
+ }
474
485
  }
475
486
  } catch { /* plansRoot missing */ }
476
487
  addTombstone('plan', planId, { source: 'api', fsPaths: foundPath ? [foundPath] : [] });