karajan-code 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,36 +23,6 @@
23
23
 
24
24
  ---
25
25
 
26
- > **v2.18.0 released** — Minor. Closes the **resilience audit** triggered by the public launch: 15 PRs across 5 phases hardening Karajan against the silent-failure family of bugs — *the problem is not that something fails, the problem is failing without telling the user why.* **Phase 1 — hibernation end to end** (#756–#759): a quota cap is classified as a recoverable quota class (incl. Claude Code's `"You've hit your session limit · resets 10:10pm"`); `withBrainRecovery` persists a standby JSON; the orchestrator consumes `action:"hibernate"`, seals the session `hibernated` (resumable) instead of `failed`; the last line printed is the exact resume command (`kj standby resume <id>`). **Phase 2 — don't lie** (#761–#763): `runCommand` surfaces ENOENT so a missing agent CLI gets an actionable error (not an empty one); `silenceTimeoutMs` is forwarded to every role so a hung agent is killed, not waited on forever; all 6 state-file writers go through `writeJsonAtomic` (write-temp + rename) — interrupted writes can no longer truncate plans, sessions, standby snapshots. **Phase 3 — don't lose or block** (#764–#767): a corrupt plan JSON is renamed aside with a loud warn (was silently dropped from `kj plan list`); `kj.config.yml` parse errors throw `Invalid YAML in <path>` (used to brick every kj command including `kj doctor`); `injectLoadedPlan` reconciles HUs left in `coding`/`reviewing`/`running` by a killed `kj run` (cross-checked against the run-registry); board SQLite gets `busy_timeout`, a `user_version` schema gate and corruption recovery. **Phase 4 — don't degrade silently** (#768–#769): `TriageRole` warns loudly when LLM output is unparseable (used to skip researcher/architect/security/tester with a chirpy "fallback defaults" summary); `verifyCoderOutput` distinguishes a git failure from "the coder did nothing" (`gitError` + `retryStrategy: null`) — no more iterations blaming the agent for infra. **Phase 5 — safety net** (#770): a `tests/resilience/` suite indexes every silent-failure mode and pins each one with a test, plus an end-to-end tripwire for the whole quota → hibernate → resume flow. Plus CI runs the `packages/hu-board` suite on every PR (#755). 4 959/4 959 tests passing across 416 files. Safe upgrade from 2.17.2.
27
- >
28
- > **v2.17.2** — Patch. Wires quota-exhaustion **hibernation end to end**. A `kj run` / `kj plan` that hits a provider session or usage cap used to abort the task with an opaque `UNKNOWN_FATAL`; now it suspends and tells you how to resume. **Session-limit classification (#756)**: `"You've hit your session limit · resets 10:10pm"` matched no rate-limit pattern → `session limit`/`weekly limit` added, and `parseCooldown` learns the 12-hour `resets 10:10pm` clock. **Hibernation persists (#757)**: `withBrainRecovery` only wrote `~/.kj/standby/<id>.json` with a `sessionState`, which no caller passed — new `buildStandbyState()` assembles it with an allowlisted env subset (never the full `process.env`). **Orchestrator consumes `action:"hibernate"` (#758)**: no code checked for it, so a hibernation sealed the HU `failed`; the coder/refactorer stages now stop cleanly and the session is sealed `hibernated` (resumable). **Resume hint (#759)**: a stopped run's last line is now the exact command — `kj standby resume <id>` for a hibernation, `kj resume <id>` otherwise. Plus CI now runs the `packages/hu-board` suite (#755). 4 931/4 931 tests passing across 410 files. Safe upgrade from 2.17.1.
29
- >
30
- > **v2.17.1** — Patch. Fixes **KJC-BUG-0055**: a project deleted from the HU Board (🗑️) no longer resurrects on the next `kj plan` or board restart. Four independent leaks closed: (1) **`sync.js` temporal gate** — the unconditional `removeTombstone` from KJC-BUG-0050 becomes a `plan.updatedAt > tombstone.deleted_at` comparison, so a tombstoned project revives only when the plan is genuinely newer than the delete; (2) **`ephemeral-cleaner.js`** now writes a tombstone and `rm -rf`'s `hu-stories/`, `sessions/` and `~/.kj/plans/` dirs when wiping ephemeral projects at boot — previously the orphan directories revived the project on the next scan; (3) **`fullScan` boot GC** sweeps orphan tombstoned dirs (the manual DB-wipe case); (4) **`DELETE /api/projects/:id`** honours `KJ_PLANS_DIR` instead of the hardcoded path. New `getTombstone()` helper in `db.js`. **Also fixes a silent `kj board start` failure (#753)**: the daemon's entry-point guard compared `import.meta.url` to a hand-built `file://` path, which wrongly returned false on Windows, symlinked/global installs and paths with spaces — the board exited 0 with an empty log. `server.js` now trusts a `KJ_BOARD_DAEMON` launcher flag, adds `uncaughtException`/`unhandledRejection` handlers and an actionable `better-sqlite3` load error; `board.js` detects an early daemon exit instead of reporting a phantom PID. 4 909/4 909 tests passing across 408 files. Safe upgrade from 2.17.0.
31
- >
32
- > **v2.17.0** — Minor. `kj audit` gains two new deterministic structural collectors and the v2.16 Sonar false-positive filter is generalised to apply across every collector. **Knip dead-exports collector** (codeQuality dim): reports unused exports/types (MINOR) and unused files (MAJOR), stack-aware (JS/TS only, needs package.json), subprocess via `--reporter json`, 120s timeout. **Madge circular-import collector** (architecture dim): detects import cycles, severity by chain length (≥4 files = MAJOR), honours tsconfig.json path aliases, 60s timeout. **Generalised FP filter**: every collector (sonar, knip, madge, osv, semgrep) now uses the same `config.audit.false_positives` shape with `tool` field plus inline marker `// karajan-audit-ignore: <tool>:<ruleId>`; legacy `config.sonar.false_positives` and `// karajan-sonar-ignore:` keep working. **Built-in catalogue** ships 4 entries by default (knip:unused-files in tests/fixtures + examples, knip:unused-exports on barrel files, madge cycles in node_modules). **BREAKING(engines)**: Node `>=20.10` → `>=20.19` (knip 6.x requirement; same flavour as the v2.8 18→20.10 bump). 4 872/4 872 tests passing across 402 files. Safe upgrade from 2.16.0 if you're on Node ≥ 20.19.
33
- >
34
- > **v2.16.0** — Minor. Quality-focused release: **deterministic Sonar false-positive filter** (KJC-TSK-0416), wire universal de Brain Recovery completado en `semantic-detector` (TSK-0413 step D), codemod `replace(/regex/g, …)` → `replaceAll(/regex/g, …)` en 41 sitios, y limpieza de hallazgos del propio `kj audit` v2.15.0. The Sonar filter combines two mechanisms: (1) **static rules** `{ rule, filePattern, reason }` from a built-in catalog (covering common false positives like `javascript:S2699` on `tests/architecture/`) plus extensible via `config.sonar.false_positives`; (2) **inline ignores** with `// karajan-sonar-ignore: <ruleId>` on the issue line. Result: the coder stops burning tokens "fixing" non-broken assertions. Brain Recovery now wraps **every** AI call in the pipeline — `semantic-detector` was the last legacy caller. 4 846/4 846 tests passing across 401 files. Safe upgrade from 2.15.0.
35
- >
36
- > **v2.15.0** — Minor. Three epics, 30+ commits, ~4 000 LOC. (1) **Brain Recovery** (KJC-PCS-0044): universal error classifier with 7 rich classes (rate_limit_short, quota_daily, quota_monthly, api_down, auth_failed, network_timeout, silenced, unknown_fatal), `withBrainRecovery` wrapper wired into ALL agent invocations (no silent failures), persistent hibernation in `~/.kj/standby/<id>.json` with event-driven scheduler (no polling), `kj standby list` + `kj standby resume`, board reconcile at startup, **fallback chain** when quota exhausted with `retryAfter > 12h` (Claude → Codex → OpenCode → Aider, configurable per role via `kj init`). (2) **Model Routing + Undo** (KJC-PCS-0043): each HU gets `coder_model` + `reviewer_model` with cross-provider review by default (claude↔codex), per-HU override from board modal, OpenCode + Aider as first-class providers, ⏪ Undo button restores files via git snapshots. (3) **Self-Healing Plan** (KJC-PCS-0042): structural integrity pass breaks cycles + cleans orphan refs, smart convergence guard for self-fix loop, `kj plan fix [planId] [--prompt]` for iterating without regenerating, skip Sonar for SPIKE/DOC/RESEARCH HUs, eliminate Failed kanban column. 4 835/4 835 tests passing across 400 files. Safe upgrade from 2.14.3.
37
- >
38
- > **v2.14.2** — Patch. More dogfooding fallout from GRETA Plan 2 v2.14.1: the **▶ Run button** on the HU Board appeared on every `pending` card regardless of `blocked_by` (you could launch a HU whose deps don't exist yet — the user caught it instantly), and titles on the board lost their **`[EPICA]` prefix** at some point in v2.14.x evolution, making it impossible to tell at a glance which area of the plan a card belonged to. Fixes: `canRunHu` now requires `blockedBy.length === 0` before showing ▶; the planner prompt now demands `description: "[EPICA] one-sentence description"` with INFRA/SHARED fallbacks. Plus a new doc `docs/task-templates/spec-conventions.md` collecting the 6 SPEC conventions the planner v2.14.x understands (épicas, scope exclusions, transversal deps, reuse, async observers, explicit deps), so users don't have to rediscover them by dogfooding. 2 PRs (#687, #688). 4584/4584 tests passing. Safe upgrade from 2.14.1.
39
- >
40
- > **v2.14.1** — Patch. Two more planner pathologies surfaced by dogfooding v2.14.0 against GRETA Plan 2: the **self-fix loop could regress** (iter 1 would drop 15→10 issues, iter 2 then deleted HUs the first iter had added, reaching 17 — worse than before iter 2 started) and the **planner declared `blocked_by` on async observers** (HUs that an async guardrail or cron *reacts to* were marked as depending on the guardrail, breaking the GRETA "AVISA-no-BLOQUEA" principle). Fixes: P5 snapshots the plan before each self-fix iter and reverts if `newCount > currentCount`; P6 lists six async-observer patterns in the planner prompt with a "consume vs react" heuristic. After P5+P6, regenerating Plan 2 GRETA returns to the baseline-iter-1 quality (9 findings) instead of v2.14.0's 17. 2 PRs (#684, #685). 4580/4580 tests passing. Safe upgrade from 2.14.0.
41
- >
42
- > **v2.14.0** — Quality pass. 16 PRs absorbing bug blockers (Solomon no longer rubber-stamps security issues misclassified as "style", coder filesystem-leak detection gains a second layer that catches `cd <abs> && pnpm init` even when the dir pre-existed, Sonar admin password rotation now surfaces silent failures), the four planner pathologies surfaced by the GRETA Plan 2 dogfooding (scope respect, transversal one-to-many deps, explicit `reuse` marker, and a brand-new **self-fix loop** where the plan-reviewer re-invokes the planner with structured feedback until zero issues remain), HU Board polish (zombie-TTL for crashed-runner prompts, less aggressive rate-limit with SSE exempt), and the first wave of `tests/` reorg (issue #368): ~93 files moved from root to mirror-subfolders. **4577/4577 tests passing**, **0 regressions** across all 16 PRs. Safe upgrade from 2.13.0.
43
- >
44
- > **v2.13.0** — HU Board hardening. Five PRs make the board resilient and self-healing after a dogfooding session uncovered four pathologies: a "Karajan needs an answer" prompt modal from days earlier blocking the entire UI, 18 zombi projects reappearing after every `kj board start`, the browser serving stale HTML/JS after a server restart, and the prompt modal showing a transparent background. Now: **tombstones** persist deletes across `fullScan` (`KJC-TSK-0380`), new endpoints `DELETE /api/prompts/:id`, `DELETE /api/plans/:planId`, `GET /api/tombstones`, `POST /api/tombstones/:type/:id/restore`; new command **`kj board cleanup`** sweeps ephemeral projects, orphan prompts and dangling session dirs in one pass; **`Cache-Control: no-store`** + `/api/version` polling auto-reloads the client when the server boot time changes; new **🧹 button** as manual escape hatch; CSS `--bg-secondary` finally declared so modals are opaque. 5 PRs (#654, #655, #656, #657, #658). 4522/4522 tests passing.
45
- >
46
- > **v2.12.0** — Quality-measurement release. Two new features land together: every `kj run` against a known plan now scores how faithfully the coder followed it (deterministic 0–100 **plan adherence** metric, four weighted components, rendered in `summary.md`), and a small **golden-tasks** regression suite (`todo-rest-api`, `npm-package-cli`, `react-counter-component`) catches output-quality drops between Karajan versions before npm publish. Plus the shrink-budget CI gate now exempts human-facing docs from its 200-LOC ceiling while keeping AI-rule files (CLAUDE.md, AGENTS.md, role prompts) capped. 3 PRs for plan adherence (#645–#647), 4 for golden tasks (#648, #650–#652), 1 for the CI policy (#649). 4522/4522 tests passing. Safe upgrade from 2.11.0.
47
- >
48
- > **v2.11.0** — Dogfooding pass release. Two-day pass through a 10-level test plan surfaced and fixed a long tail of UX papercuts and three latent bugs that only show up on fresh `/tmp` repos: the `SonarStage` no longer loops on remoteless projects (was burning iterations until `max_iterations`-fallback-approval), the post-loop `commitAll` now tolerates the locale-specific "nothing to commit" race, the HU sub-pipeline branches off `master`/`HEAD` when the configured `main` doesn't exist, and `runFlow` now seals `session.status` at the boundary so `kj status` never shows zombi `running` runs again. Plus `hu-board` gains automatic ephemeral-project cleanup and an in-UI help modal for the five views. 14 PRs (#624–#637), 4452/4452 tests passing. Safe upgrade from 2.10.2.
49
- >
50
- > **v2.10.2** — Patch release. `kj init` wizard expanded from 9 prompts to a full setup: per-role provider selection (10 roles, "inherit / pick CLI / disable"), automatic SonarQube token generation via REST API (no more web UI walkthrough), git automation flags (`auto_commit/push/pr`) and HU Board security (bind host + port). +16 new tests. Safe upgrade from 2.10.1.
51
- >
52
- > **v2.10.1** — Patch release. One-line fix for a stdout contamination bug in `kj audit --agent-readiness --json` (the `[info]` banner was breaking downstream `jq` pipes), plus polish in the asciinema demo scripts. Safe upgrade from 2.10.0.
53
- >
54
- > **v2.10.0** — Agent-readiness release. Karajan is now the first orchestrator with a full agent-readability surface: an `llms.txt` index at the root, a `SKILL.md` per CLI command under `docs/agents/`, and a static auditor that scores any third-party repo against the same shape. Highlights: (1) **`kj audit --agent-readiness`** scores any repo 0–100 across seven checks (llms.txt, robots AI-bot allowlist, page token budgets ≤ 32 KB, heading hierarchy, agents/README, SKILL.md coverage). LLM-free, deterministic, JSON-able. Karajan-on-Karajan: **100/100**. (2) **Six new SKILL.md files** (`kj doctor / init / board / review / resume / clean`) under `docs/agents/`, all with the same `What it does · Inputs · Outputs · Side effects · Failure modes · Example` contract; CI guards every link in `llms.txt` resolves. (3) **Webperf quality gate inside the iteration loop** (`pipeline.perf.enabled`): PASS continues, FAIL pushes blocking-metric feedback to the coder, scanner-missing skips best-effort. (4) **HU Board hardening**: binds 127.0.0.1 by default, opt-in `--bind 0.0.0.0` enforces an auto-generated token, helmet headers, rate limiting at 300 req/min — "safe by default on a coffee-shop WiFi". (5) **a11y skills auto-route**: tasks mentioning `accessibility / WCAG / ARIA / screen reader / keyboard nav` automatically pull the `frontend-ui-engineering` skill. (6) **Asciinema demo scripts** under `docs/demos/` so the recordings re-record per release instead of rotting. 5 PRs merged (#605–#609 + #610), 4358/4358 tests passing. See [CHANGELOG.md](./CHANGELOG.md) for the full punch list.
55
-
56
26
  You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
57
27
 
58
28
  ## What is Karajan?
@@ -246,10 +216,12 @@ $ claude $ kj-tail
246
216
  ## The pipeline
247
217
 
248
218
  ```
249
- hu-reviewer? → triage → domain-curator? → discover? → architect? → planner? → coder → sonar? → impeccable? → reviewer → tester? → security? → solomon → commiter?
219
+ pre-loop: intent → hu-reviewer? → triage? → domain-curator? → discover? → skills? → researcher? → architect? → planner? → acceptance?
220
+ iteration: coder → refactorer? → guard(output) → guard(perf) → sonar? → tdd → reviewer → solomon? → brain? (loops 1..N)
221
+ post-loop: tester? → security? → perf? → impeccable? → audit?
250
222
  ```
251
223
 
252
- **16 roles**, each executed by the AI agent you choose:
224
+ **24 stages** across three phases: 18 AI-agent-backed roles (table below), 6 deterministic stages without an LLM call (`intent`, `skills`, `acceptance`, `guard(output)`, `guard(perf)`, `tdd`). Two extra classes (`commiter`, `repairer`) are post-approval / internal helpers, not standalone pipeline stages. Each AI role is executed by the agent you choose:
253
225
 
254
226
  | Role | What it does | Default |
255
227
  |------|-------------|---------|
@@ -268,8 +240,17 @@ hu-reviewer? → triage → domain-curator? → discover? → architect? → pla
268
240
  | **security** | OWASP security audit | **On** |
269
241
  | **solomon** | Pipeline boss: evaluates every rejection, overrides style-only blocks | **On** |
270
242
  | **commiter** | Git commit, push, and PR automation after approval | Off |
243
+ | **researcher** | Investigates the codebase before planning (file map + signatures + related tests) | Off |
244
+ | **perf** | WebPerf quality gate — Core Web Vitals (LCP, CLS, INP) via Lighthouse | Off |
245
+ | **brain** | Central AI orchestrator: routing, feedback enrichment, output compression | **On** |
271
246
  | **audit** | Read-only codebase health analysis (5 dimensions, A-F scores) | Standalone |
272
247
 
248
+ > **Deterministic stages (no class):** `intent` (task-type classifier — `sw` / `infra` / `doc` / `add-tests` / `refactor` / `audit`), `skills` (slash-command surface), `acceptance` (executable acceptance tests), `guard(output)` (destructive ops + credential leaks), `guard(perf)` (frontend anti-patterns), `tdd` (test-coverage check).
249
+ >
250
+ > **Internal helpers (have a class, not a standalone stage):** `commiter` (git automation post-approval), `repairer` (repairs broken acceptance tests at runtime, invoked by `acceptance` / `tdd`).
251
+ >
252
+ > Full per-stage reference: [Pipeline roles](https://karajan-code.web.app/docs/handbook/pipeline-roles/) (handbook).
253
+
273
254
  ## 5 AI agents supported
274
255
 
275
256
  | Agent | CLI | Install |
@@ -362,6 +343,44 @@ No code, task descriptions, or personal data is ever sent.
362
343
 
363
344
  Opt out: set `telemetry: false` in `~/.karajan/kj.config.yml`
364
345
 
346
+ ## Recent releases
347
+
348
+ > **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.
349
+ >
350
+ > **v2.18.1 released** — Patch. Six user-feedback follow-ups to v2.18.0: **`kj-tail` after `kj resume`** (#772, was silent because `resume.js` skipped `withCliRunLog`); **standby waits in-process instead of exiting on a short cooldown** (#773, kj stays alive for waits ≤ 12 h and retries on its own; Ctrl+C during the wait prints `kj standby resume <id>`); **closed KJC-BUG-0040 — SEA linux binaries** (#774, was a race between `gh release create` and softprops, not `better-sqlite3` as the memory said — 60 s poll fix + `make_latest:false`); **stack bias — Python repos no longer get vitest** (#775 + #776 + #777, `detectProjectStack` finally cabled into the coder prompt, the HU auto-generator templates by language, and the synthesizer + `auto-hu-batch` take the stack from the filesystem). 4 971/4 971 tests passing across 416 files. Safe upgrade from 2.18.0.
351
+ >
352
+ > **v2.18.0** — Minor. Closes the **resilience audit** triggered by the public launch: 15 PRs across 5 phases hardening Karajan against the silent-failure family of bugs — *the problem is not that something fails, the problem is failing without telling the user why.* **Phase 1 — hibernation end to end** (#756–#759): a quota cap is classified as a recoverable quota class (incl. Claude Code's `"You've hit your session limit · resets 10:10pm"`); `withBrainRecovery` persists a standby JSON; the orchestrator consumes `action:"hibernate"`, seals the session `hibernated` (resumable) instead of `failed`; the last line printed is the exact resume command (`kj standby resume <id>`). **Phase 2 — don't lie** (#761–#763): `runCommand` surfaces ENOENT so a missing agent CLI gets an actionable error (not an empty one); `silenceTimeoutMs` is forwarded to every role so a hung agent is killed, not waited on forever; all 6 state-file writers go through `writeJsonAtomic` (write-temp + rename) — interrupted writes can no longer truncate plans, sessions, standby snapshots. **Phase 3 — don't lose or block** (#764–#767): a corrupt plan JSON is renamed aside with a loud warn (was silently dropped from `kj plan list`); `kj.config.yml` parse errors throw `Invalid YAML in <path>` (used to brick every kj command including `kj doctor`); `injectLoadedPlan` reconciles HUs left in `coding`/`reviewing`/`running` by a killed `kj run` (cross-checked against the run-registry); board SQLite gets `busy_timeout`, a `user_version` schema gate and corruption recovery. **Phase 4 — don't degrade silently** (#768–#769): `TriageRole` warns loudly when LLM output is unparseable (used to skip researcher/architect/security/tester with a chirpy "fallback defaults" summary); `verifyCoderOutput` distinguishes a git failure from "the coder did nothing" (`gitError` + `retryStrategy: null`) — no more iterations blaming the agent for infra. **Phase 5 — safety net** (#770): a `tests/resilience/` suite indexes every silent-failure mode and pins each one with a test, plus an end-to-end tripwire for the whole quota → hibernate → resume flow. Plus CI runs the `packages/hu-board` suite on every PR (#755). 4 959/4 959 tests passing across 416 files. Safe upgrade from 2.17.2.
353
+ >
354
+ > **v2.17.2** — Patch. Wires quota-exhaustion **hibernation end to end**. A `kj run` / `kj plan` that hits a provider session or usage cap used to abort the task with an opaque `UNKNOWN_FATAL`; now it suspends and tells you how to resume. **Session-limit classification (#756)**: `"You've hit your session limit · resets 10:10pm"` matched no rate-limit pattern → `session limit`/`weekly limit` added, and `parseCooldown` learns the 12-hour `resets 10:10pm` clock. **Hibernation persists (#757)**: `withBrainRecovery` only wrote `~/.kj/standby/<id>.json` with a `sessionState`, which no caller passed — new `buildStandbyState()` assembles it with an allowlisted env subset (never the full `process.env`). **Orchestrator consumes `action:"hibernate"` (#758)**: no code checked for it, so a hibernation sealed the HU `failed`; the coder/refactorer stages now stop cleanly and the session is sealed `hibernated` (resumable). **Resume hint (#759)**: a stopped run's last line is now the exact command — `kj standby resume <id>` for a hibernation, `kj resume <id>` otherwise. Plus CI now runs the `packages/hu-board` suite (#755). 4 931/4 931 tests passing across 410 files. Safe upgrade from 2.17.1.
355
+ >
356
+ > **v2.17.1** — Patch. Fixes **KJC-BUG-0055**: a project deleted from the HU Board (🗑️) no longer resurrects on the next `kj plan` or board restart. Four independent leaks closed: (1) **`sync.js` temporal gate** — the unconditional `removeTombstone` from KJC-BUG-0050 becomes a `plan.updatedAt > tombstone.deleted_at` comparison, so a tombstoned project revives only when the plan is genuinely newer than the delete; (2) **`ephemeral-cleaner.js`** now writes a tombstone and `rm -rf`'s `hu-stories/`, `sessions/` and `~/.kj/plans/` dirs when wiping ephemeral projects at boot — previously the orphan directories revived the project on the next scan; (3) **`fullScan` boot GC** sweeps orphan tombstoned dirs (the manual DB-wipe case); (4) **`DELETE /api/projects/:id`** honours `KJ_PLANS_DIR` instead of the hardcoded path. New `getTombstone()` helper in `db.js`. **Also fixes a silent `kj board start` failure (#753)**: the daemon's entry-point guard compared `import.meta.url` to a hand-built `file://` path, which wrongly returned false on Windows, symlinked/global installs and paths with spaces — the board exited 0 with an empty log. `server.js` now trusts a `KJ_BOARD_DAEMON` launcher flag, adds `uncaughtException`/`unhandledRejection` handlers and an actionable `better-sqlite3` load error; `board.js` detects an early daemon exit instead of reporting a phantom PID. 4 909/4 909 tests passing across 408 files. Safe upgrade from 2.17.0.
357
+ >
358
+ > **v2.17.0** — Minor. `kj audit` gains two new deterministic structural collectors and the v2.16 Sonar false-positive filter is generalised to apply across every collector. **Knip dead-exports collector** (codeQuality dim): reports unused exports/types (MINOR) and unused files (MAJOR), stack-aware (JS/TS only, needs package.json), subprocess via `--reporter json`, 120s timeout. **Madge circular-import collector** (architecture dim): detects import cycles, severity by chain length (≥4 files = MAJOR), honours tsconfig.json path aliases, 60s timeout. **Generalised FP filter**: every collector (sonar, knip, madge, osv, semgrep) now uses the same `config.audit.false_positives` shape with `tool` field plus inline marker `// karajan-audit-ignore: <tool>:<ruleId>`; legacy `config.sonar.false_positives` and `// karajan-sonar-ignore:` keep working. **Built-in catalogue** ships 4 entries by default (knip:unused-files in tests/fixtures + examples, knip:unused-exports on barrel files, madge cycles in node_modules). **BREAKING(engines)**: Node `>=20.10` → `>=20.19` (knip 6.x requirement; same flavour as the v2.8 18→20.10 bump). 4 872/4 872 tests passing across 402 files. Safe upgrade from 2.16.0 if you're on Node ≥ 20.19.
359
+ >
360
+ > **v2.16.0** — Minor. Quality-focused release: **deterministic Sonar false-positive filter** (KJC-TSK-0416), wire universal de Brain Recovery completado en `semantic-detector` (TSK-0413 step D), codemod `replace(/regex/g, …)` → `replaceAll(/regex/g, …)` en 41 sitios, y limpieza de hallazgos del propio `kj audit` v2.15.0. The Sonar filter combines two mechanisms: (1) **static rules** `{ rule, filePattern, reason }` from a built-in catalog (covering common false positives like `javascript:S2699` on `tests/architecture/`) plus extensible via `config.sonar.false_positives`; (2) **inline ignores** with `// karajan-sonar-ignore: <ruleId>` on the issue line. Result: the coder stops burning tokens "fixing" non-broken assertions. Brain Recovery now wraps **every** AI call in the pipeline — `semantic-detector` was the last legacy caller. 4 846/4 846 tests passing across 401 files. Safe upgrade from 2.15.0.
361
+ >
362
+ > **v2.15.0** — Minor. Three epics, 30+ commits, ~4 000 LOC. (1) **Brain Recovery** (KJC-PCS-0044): universal error classifier with 7 rich classes (rate_limit_short, quota_daily, quota_monthly, api_down, auth_failed, network_timeout, silenced, unknown_fatal), `withBrainRecovery` wrapper wired into ALL agent invocations (no silent failures), persistent hibernation in `~/.kj/standby/<id>.json` with event-driven scheduler (no polling), `kj standby list` + `kj standby resume`, board reconcile at startup, **fallback chain** when quota exhausted with `retryAfter > 12h` (Claude → Codex → OpenCode → Aider, configurable per role via `kj init`). (2) **Model Routing + Undo** (KJC-PCS-0043): each HU gets `coder_model` + `reviewer_model` with cross-provider review by default (claude↔codex), per-HU override from board modal, OpenCode + Aider as first-class providers, ⏪ Undo button restores files via git snapshots. (3) **Self-Healing Plan** (KJC-PCS-0042): structural integrity pass breaks cycles + cleans orphan refs, smart convergence guard for self-fix loop, `kj plan fix [planId] [--prompt]` for iterating without regenerating, skip Sonar for SPIKE/DOC/RESEARCH HUs, eliminate Failed kanban column. 4 835/4 835 tests passing across 400 files. Safe upgrade from 2.14.3.
363
+ >
364
+ > **v2.14.2** — Patch. More dogfooding fallout from GRETA Plan 2 v2.14.1: the **▶ Run button** on the HU Board appeared on every `pending` card regardless of `blocked_by` (you could launch a HU whose deps don't exist yet — the user caught it instantly), and titles on the board lost their **`[EPICA]` prefix** at some point in v2.14.x evolution, making it impossible to tell at a glance which area of the plan a card belonged to. Fixes: `canRunHu` now requires `blockedBy.length === 0` before showing ▶; the planner prompt now demands `description: "[EPICA] one-sentence description"` with INFRA/SHARED fallbacks. Plus a new doc `docs/task-templates/spec-conventions.md` collecting the 6 SPEC conventions the planner v2.14.x understands (épicas, scope exclusions, transversal deps, reuse, async observers, explicit deps), so users don't have to rediscover them by dogfooding. 2 PRs (#687, #688). 4584/4584 tests passing. Safe upgrade from 2.14.1.
365
+ >
366
+ > **v2.14.1** — Patch. Two more planner pathologies surfaced by dogfooding v2.14.0 against GRETA Plan 2: the **self-fix loop could regress** (iter 1 would drop 15→10 issues, iter 2 then deleted HUs the first iter had added, reaching 17 — worse than before iter 2 started) and the **planner declared `blocked_by` on async observers** (HUs that an async guardrail or cron *reacts to* were marked as depending on the guardrail, breaking the GRETA "AVISA-no-BLOQUEA" principle). Fixes: P5 snapshots the plan before each self-fix iter and reverts if `newCount > currentCount`; P6 lists six async-observer patterns in the planner prompt with a "consume vs react" heuristic. After P5+P6, regenerating Plan 2 GRETA returns to the baseline-iter-1 quality (9 findings) instead of v2.14.0's 17. 2 PRs (#684, #685). 4580/4580 tests passing. Safe upgrade from 2.14.0.
367
+ >
368
+ > **v2.14.0** — Quality pass. 16 PRs absorbing bug blockers (Solomon no longer rubber-stamps security issues misclassified as "style", coder filesystem-leak detection gains a second layer that catches `cd <abs> && pnpm init` even when the dir pre-existed, Sonar admin password rotation now surfaces silent failures), the four planner pathologies surfaced by the GRETA Plan 2 dogfooding (scope respect, transversal one-to-many deps, explicit `reuse` marker, and a brand-new **self-fix loop** where the plan-reviewer re-invokes the planner with structured feedback until zero issues remain), HU Board polish (zombie-TTL for crashed-runner prompts, less aggressive rate-limit with SSE exempt), and the first wave of `tests/` reorg (issue #368): ~93 files moved from root to mirror-subfolders. **4577/4577 tests passing**, **0 regressions** across all 16 PRs. Safe upgrade from 2.13.0.
369
+ >
370
+ > **v2.13.0** — HU Board hardening. Five PRs make the board resilient and self-healing after a dogfooding session uncovered four pathologies: a "Karajan needs an answer" prompt modal from days earlier blocking the entire UI, 18 zombi projects reappearing after every `kj board start`, the browser serving stale HTML/JS after a server restart, and the prompt modal showing a transparent background. Now: **tombstones** persist deletes across `fullScan` (`KJC-TSK-0380`), new endpoints `DELETE /api/prompts/:id`, `DELETE /api/plans/:planId`, `GET /api/tombstones`, `POST /api/tombstones/:type/:id/restore`; new command **`kj board cleanup`** sweeps ephemeral projects, orphan prompts and dangling session dirs in one pass; **`Cache-Control: no-store`** + `/api/version` polling auto-reloads the client when the server boot time changes; new **🧹 button** as manual escape hatch; CSS `--bg-secondary` finally declared so modals are opaque. 5 PRs (#654, #655, #656, #657, #658). 4522/4522 tests passing.
371
+ >
372
+ > **v2.12.0** — Quality-measurement release. Two new features land together: every `kj run` against a known plan now scores how faithfully the coder followed it (deterministic 0–100 **plan adherence** metric, four weighted components, rendered in `summary.md`), and a small **golden-tasks** regression suite (`todo-rest-api`, `npm-package-cli`, `react-counter-component`) catches output-quality drops between Karajan versions before npm publish. Plus the shrink-budget CI gate now exempts human-facing docs from its 200-LOC ceiling while keeping AI-rule files (CLAUDE.md, AGENTS.md, role prompts) capped. 3 PRs for plan adherence (#645–#647), 4 for golden tasks (#648, #650–#652), 1 for the CI policy (#649). 4522/4522 tests passing. Safe upgrade from 2.11.0.
373
+ >
374
+ > **v2.11.0** — Dogfooding pass release. Two-day pass through a 10-level test plan surfaced and fixed a long tail of UX papercuts and three latent bugs that only show up on fresh `/tmp` repos: the `SonarStage` no longer loops on remoteless projects (was burning iterations until `max_iterations`-fallback-approval), the post-loop `commitAll` now tolerates the locale-specific "nothing to commit" race, the HU sub-pipeline branches off `master`/`HEAD` when the configured `main` doesn't exist, and `runFlow` now seals `session.status` at the boundary so `kj status` never shows zombi `running` runs again. Plus `hu-board` gains automatic ephemeral-project cleanup and an in-UI help modal for the five views. 14 PRs (#624–#637), 4452/4452 tests passing. Safe upgrade from 2.10.2.
375
+ >
376
+ > **v2.10.2** — Patch release. `kj init` wizard expanded from 9 prompts to a full setup: per-role provider selection (10 roles, "inherit / pick CLI / disable"), automatic SonarQube token generation via REST API (no more web UI walkthrough), git automation flags (`auto_commit/push/pr`) and HU Board security (bind host + port). +16 new tests. Safe upgrade from 2.10.1.
377
+ >
378
+ > **v2.10.1** — Patch release. One-line fix for a stdout contamination bug in `kj audit --agent-readiness --json` (the `[info]` banner was breaking downstream `jq` pipes), plus polish in the asciinema demo scripts. Safe upgrade from 2.10.0.
379
+ >
380
+ > **v2.10.0** — Agent-readiness release. Karajan is now the first orchestrator with a full agent-readability surface: an `llms.txt` index at the root, a `SKILL.md` per CLI command under `docs/agents/`, and a static auditor that scores any third-party repo against the same shape. Highlights: (1) **`kj audit --agent-readiness`** scores any repo 0–100 across seven checks (llms.txt, robots AI-bot allowlist, page token budgets ≤ 32 KB, heading hierarchy, agents/README, SKILL.md coverage). LLM-free, deterministic, JSON-able. Karajan-on-Karajan: **100/100**. (2) **Six new SKILL.md files** (`kj doctor / init / board / review / resume / clean`) under `docs/agents/`, all with the same `What it does · Inputs · Outputs · Side effects · Failure modes · Example` contract; CI guards every link in `llms.txt` resolves. (3) **Webperf quality gate inside the iteration loop** (`pipeline.perf.enabled`): PASS continues, FAIL pushes blocking-metric feedback to the coder, scanner-missing skips best-effort. (4) **HU Board hardening**: binds 127.0.0.1 by default, opt-in `--bind 0.0.0.0` enforces an auto-generated token, helmet headers, rate limiting at 300 req/min — "safe by default on a coffee-shop WiFi". (5) **a11y skills auto-route**: tasks mentioning `accessibility / WCAG / ARIA / screen reader / keyboard nav` automatically pull the `frontend-ui-engineering` skill. (6) **Asciinema demo scripts** under `docs/demos/` so the recordings re-record per release instead of rotting. 5 PRs merged (#605–#609 + #610), 4358/4358 tests passing. See [CHANGELOG.md](./CHANGELOG.md) for the full punch list.
381
+
382
+ See [CHANGELOG.md](./CHANGELOG.md) for the full version history.
383
+
365
384
  ## Links
366
385
 
367
386
  - [Website](https://karajancode.com) (also [kj-code.com](https://kj-code.com))
package/docs/README.es.md CHANGED
@@ -141,10 +141,12 @@ kj-tail --help # Todas las opciones
141
141
  ## El pipeline
142
142
 
143
143
  ```
144
- hu-reviewer? → triage → domain-curator? → discover? → architect? → planner? → coder → sonar? → impeccable? → reviewer → tester? → security? → solomon → commiter?
144
+ pre-loop: intent → hu-reviewer? → triage? → domain-curator? → discover? → skills? → researcher? → architect? → planner? → acceptance?
145
+ iteration: coder → refactorer? → guard(output) → guard(perf) → sonar? → tdd → reviewer → solomon? → brain? (bucle 1..N)
146
+ post-loop: tester? → security? → perf? → impeccable? → audit?
145
147
  ```
146
148
 
147
- **16 roles**, cada uno ejecutado por el agente de IA que elijas:
149
+ **24 etapas** repartidas en tres fases: 18 roles respaldados por agente IA (tabla abajo), 6 etapas deterministas sin llamada al LLM (`intent`, `skills`, `acceptance`, `guard(output)`, `guard(perf)`, `tdd`). Dos clases extra (`commiter`, `repairer`) son post-aprobación / auxiliares internas, no etapas independientes del pipeline. Cada rol IA es ejecutado por el agente que tú elijas:
148
150
 
149
151
  | Rol | Que hace | Por defecto |
150
152
  |-----|----------|-------------|
@@ -163,8 +165,17 @@ hu-reviewer? → triage → domain-curator? → discover? → architect? → pla
163
165
  | **security** | Auditoria de seguridad OWASP | **On** |
164
166
  | **solomon** | Jefe del pipeline: evalua cada rechazo, anula bloqueos solo de estilo | **On** |
165
167
  | **commiter** | Automatizacion de git commit, push y PR tras aprobacion | Off |
168
+ | **researcher** | Investiga el codebase antes de planificar (mapa de ficheros + signatures + tests relacionados) | Off |
169
+ | **perf** | Quality gate WebPerf — Core Web Vitals (LCP, CLS, INP) via Lighthouse | Off |
170
+ | **brain** | Orquestador IA central: routing, enriquecimiento de feedback, compresión de salidas | **On** |
166
171
  | **audit** | Analisis de salud del codebase solo-lectura (5 dimensiones, scores A-F) | Standalone |
167
172
 
173
+ > **Etapas deterministas (sin clase):** `intent` (clasificador de tipo de tarea — `sw` / `infra` / `doc` / `add-tests` / `refactor` / `audit`), `skills` (superficie de slash-commands), `acceptance` (tests de aceptación ejecutables), `guard(output)` (operaciones destructivas + fugas de credenciales), `guard(perf)` (anti-patrones frontend), `tdd` (verificación de cobertura).
174
+ >
175
+ > **Auxiliares internas (tienen clase, no etapa independiente):** `commiter` (automatización git post-aprobación), `repairer` (repara tests de aceptación rotos en runtime, invocado por `acceptance` / `tdd`).
176
+ >
177
+ > Referencia completa por etapa: [Roles del pipeline](https://karajan-code.web.app/docs/es/handbook/pipeline-roles/) (handbook).
178
+
168
179
  ## 5 agentes de IA soportados
169
180
 
170
181
  | Agente | CLI | Instalacion |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.18.0",
3
+ "version": "2.19.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",
@@ -14,15 +14,17 @@
14
14
 
15
15
  import fs from "node:fs";
16
16
  import path from "node:path";
17
- import os from "node:os";
18
17
  import { writeJsonAtomicSync } from "../utils/atomic-write.js";
18
+ import { resolveHome } from "../utils/paths.js";
19
19
 
20
20
  const FILENAME_RE = /^[a-zA-Z0-9._-]+\.json$/;
21
21
 
22
+ // Delegates to the unified resolver. PR 3 of KJC-PCS-0047 switched
23
+ // the default to `.karajan` — hibernated sessions now persist under
24
+ // `~/.karajan/standby/`, migrated automatically from the legacy
25
+ // `~/.kj/standby/` on the next `kj` invocation.
22
26
  function getKjHome() {
23
- if (process.env.KJ_HOME) return process.env.KJ_HOME;
24
- if (process.env.VITEST) return path.join(os.tmpdir(), `kj-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`, ".kj");
25
- return path.join(os.homedir(), ".kj");
27
+ return resolveHome({ defaultSegment: ".karajan" });
26
28
  }
27
29
 
28
30
  export function standbyDir() {
@@ -7,7 +7,7 @@
7
7
  // puede ignorarlo (fallback a long standby capped) hasta que llegue 0414.
8
8
 
9
9
  import { classifyAgentError, ERROR_CLASS } from "./agent-error-classifier.js";
10
- import { persistStandby } from "./standby-store.js";
10
+ import { persistStandby, markStandbyDone } from "./standby-store.js";
11
11
 
12
12
  const ONE_MIN = 60 * 1000;
13
13
 
@@ -20,6 +20,13 @@ export const DEFAULT_RECOVERY_POLICY = Object.freeze({
20
20
  // KJC-TSK-0415: threshold de fallback. Si retryAfter > esto Y el role
21
21
  // tiene fallback configurado → switch provider en vez de hibernar.
22
22
  fallbackWaitHoursDefault: DEFAULT_FALLBACK_WAIT_HOURS,
23
+ // Standby-in-process: when the cooldown is short enough we keep the
24
+ // process alive and `await sleepFn(retryAfter)`, then retry the
25
+ // agent. The user sees nothing terminal — kj just pauses. A SIGINT /
26
+ // SIGTERM during the wait prints `kj standby resume <id>` and exits
27
+ // cleanly. For waits longer than this the process exits straight
28
+ // away (a multi-day weekly cap is not worth keeping kj running).
29
+ standbyWaitHoursMax: DEFAULT_FALLBACK_WAIT_HOURS,
23
30
  classes: {
24
31
  [ERROR_CLASS.RATE_LIMIT_SHORT]: { mode: "standby", maxRetries: 3 },
25
32
  [ERROR_CLASS.QUOTA_EXHAUSTED_DAILY]: { mode: "hibernate", maxRetries: 1, fallbackEligible: true },
@@ -132,10 +139,15 @@ export async function withBrainRecovery({
132
139
  }
133
140
  }
134
141
 
135
- // HIBERNATE: persistir + signal action=hibernate al caller.
136
- // Si el caller pasó `sessionState` (TSK-0414 PR2), persistimos el JSON
137
- // automáticamente en ~/.kj/standby/<sessionId>.json. Caller decide qué
138
- // hacer con action=hibernate (típicamente exit 0 para liberar memoria).
142
+ // HIBERNATE: the cooldown is too long for a short standby. Persist
143
+ // the snapshot first so a SIGINT / SIGTERM during the wait leaves a
144
+ // recoverable session on disk. Then either:
145
+ // (a) wait in-process until cooldownUntil and retry (the common
146
+ // case — daily quotas reset in a few hours), or
147
+ // (b) when the wait would exceed `standbyWaitHoursMax`, return
148
+ // action="hibernate" so the caller exits and the user
149
+ // resumes later with `kj standby resume <id>` (weekly /
150
+ // monthly caps).
139
151
  if (classPolicy.mode === "hibernate" && cls.retryAfter && cls.retryAfter > policy.hibernateThresholdMs) {
140
152
  let standbyFile = null;
141
153
  if (sessionState && sessionState.sessionId) {
@@ -147,13 +159,58 @@ export async function withBrainRecovery({
147
159
  role, provider: effectiveProvider,
148
160
  classifierMessage: cls.message,
149
161
  });
150
- logger?.info?.(`[brain] hibernated session ${sessionState.sessionId} → ${standbyFile} (resume at ${cls.retryUntil})`);
151
162
  } catch (err) {
152
163
  logger?.warn?.(`[brain] failed to persist standby: ${err.message}`);
153
164
  }
154
165
  }
155
- emit(emitter, "brain:hibernate-request", eventBase, { role, class: cls.class, retryUntil: cls.retryUntil, retryAfter: cls.retryAfter, standbyFile });
156
- return { ok: false, action: "hibernate", recovery: cls, standbyFile };
166
+
167
+ const maxWaitMs = (policy.standbyWaitHoursMax ?? DEFAULT_FALLBACK_WAIT_HOURS) * ONE_HOUR;
168
+ const tooLongToWait = cls.retryAfter > maxWaitMs;
169
+
170
+ if (tooLongToWait) {
171
+ logger?.info?.(`[brain] hibernated session ${sessionState?.sessionId ?? "(no id)"} → ${standbyFile} (resume at ${cls.retryUntil}; wait > ${Math.round(maxWaitMs/ONE_HOUR)}h, exiting)`);
172
+ emit(emitter, "brain:hibernate-request", eventBase, { role, class: cls.class, retryUntil: cls.retryUntil, retryAfter: cls.retryAfter, standbyFile });
173
+ return { ok: false, action: "hibernate", recovery: cls, standbyFile };
174
+ }
175
+
176
+ // Standby-in-process: keep kj alive, sleep until the cooldown,
177
+ // then retry. A SIGINT / SIGTERM during the wait prints the
178
+ // resume command and exits — the standby JSON is already on
179
+ // disk so the user can pick it up later.
180
+ const sid = sessionState?.sessionId ?? null;
181
+ const onSignal = (signal) => {
182
+ // eslint-disable-next-line no-console
183
+ console.log(`\n⏸ Standby interrupted (${signal}). Resume with:`);
184
+ // eslint-disable-next-line no-console
185
+ if (sid) console.log(` kj standby resume ${sid}`);
186
+ // eslint-disable-next-line no-console
187
+ else console.log(` kj standby list (no session id was assigned)`);
188
+ process.exit(signal === "SIGINT" ? 130 : 143);
189
+ };
190
+ const detach = () => {
191
+ process.removeListener("SIGINT", onSignal);
192
+ process.removeListener("SIGTERM", onSignal);
193
+ };
194
+ process.once("SIGINT", onSignal);
195
+ process.once("SIGTERM", onSignal);
196
+
197
+ const hours = Math.round((cls.retryAfter / ONE_HOUR) * 10) / 10;
198
+ logger?.info?.(`[brain] ${role}: standby for ~${hours}h until ${cls.retryUntil} (Ctrl+C to detach; will resume with 'kj standby resume ${sid}').`);
199
+ emit(emitter, "brain:standby-wait", eventBase, { role, class: cls.class, waitMs: cls.retryAfter, retryUntil: cls.retryUntil, standbyFile });
200
+
201
+ try {
202
+ await sleepFn(cls.retryAfter);
203
+ } finally {
204
+ detach();
205
+ }
206
+
207
+ // Wait finished without an interrupt — mark the standby JSON
208
+ // done so it does not auto-resume from disk later.
209
+ if (sid) {
210
+ try { markStandbyDone(sid); } catch { /* ignore */ }
211
+ }
212
+ emit(emitter, "brain:standby-resumed", eventBase, { role, class: cls.class });
213
+ continue;
157
214
  }
158
215
 
159
216
  // STANDBY: espera hasta cooldownUntil (capado por longStandbyCapMs).
@@ -62,6 +62,40 @@ export function createKarajanDirsCheck() {
62
62
  };
63
63
  }
64
64
 
65
+ /**
66
+ * Detect a populated legacy `~/.kj/` left over from before
67
+ * KJC-PCS-0047 PR 3. The auto-migrator (src/utils/home-migration.js)
68
+ * moves its contents to `~/.karajan/` on the next `kj` invocation,
69
+ * so the fix is simply "run any kj command". Severity is `warn`
70
+ * (informational) because the legacy dir does not break anything —
71
+ * it just won't get read by the board until the migration runs.
72
+ * @internal Exported for dynamic import from tests.
73
+ */
74
+ export function createLegacyKjHomeCheck() {
75
+ return {
76
+ name: "legacy-kj-home",
77
+ label: "legacy ~/.kj/ directory",
78
+ strategy: STRATEGY.MANUAL,
79
+ describe: "Detect unmigrated ~/.kj/ left over from pre-2.19 layout",
80
+ async detect() {
81
+ const legacy = path.join(os.homedir(), ".kj");
82
+ try {
83
+ const entries = await fs.readdir(legacy);
84
+ if (entries.length === 0) return { ok: true, severity: "info", detail: "Empty legacy ~/.kj/" };
85
+ const marker = path.join(os.homedir(), ".karajan", ".kj-migrated.json");
86
+ try { await fs.access(marker); return { ok: true, severity: "info", detail: "Already migrated" }; }
87
+ catch { /* no marker, still pending */ }
88
+ return {
89
+ ok: false,
90
+ severity: "warn",
91
+ detail: `~/.kj/ still has ${entries.length} entries; will be migrated on the next kj invocation`,
92
+ fix: "Run any kj command (e.g. `kj doctor`) — the migrator runs automatically",
93
+ };
94
+ } catch { return { ok: true, severity: "info", detail: "No legacy ~/.kj/" }; }
95
+ },
96
+ };
97
+ }
98
+
65
99
  export function getDirSetupChecks() {
66
- return [createKarajanDirsCheck()];
100
+ return [createKarajanDirsCheck(), createLegacyKjHomeCheck()];
67
101
  }
package/src/cli.js CHANGED
@@ -12,6 +12,7 @@ import { registerSonar } from "./cli/register-sonar.js";
12
12
  import { registerStandby } from "./cli/register-standby.js";
13
13
  import { printUpdateNotice } from "./utils/update-check.js";
14
14
  import { printWelcomeScreen } from "./utils/welcome.js";
15
+ import { migrateKjToKarajan } from "./utils/home-migration.js";
15
16
 
16
17
  const PKG_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
17
18
  const PKG_VERSION = JSON.parse(readFileSync(PKG_PATH, "utf8")).version;
@@ -107,6 +108,18 @@ program.action(async (_opts, command) => {
107
108
  printWelcomeScreen({ version: PKG_VERSION, config });
108
109
  });
109
110
 
111
+ // One-shot ~/.kj/ → ~/.karajan/ consolidation. Idempotent via marker
112
+ // file; failures non-blocking. See src/utils/home-migration.js.
113
+ // Static import — the migrator runs on every kj invocation, no
114
+ // lazy-load benefit + the architectural dynamic-imports budget would
115
+ // otherwise grow by one for a permanent caller.
116
+ try {
117
+ await migrateKjToKarajan();
118
+ } catch (err) {
119
+ // eslint-disable-next-line no-console
120
+ console.warn(`\x1b[33m[warn]\x1b[0m home migration skipped: ${err.message}`);
121
+ }
122
+
110
123
  try {
111
124
  await program.parseAsync();
112
125
  } catch (error) {
@@ -3,6 +3,8 @@ import readline from "node:readline";
3
3
  import { resumeFlow } from "../orchestrator.js";
4
4
  import { createActivityLog } from "../activity-log.js";
5
5
  import { printEvent } from "../utils/display/event-handlers.js";
6
+ import { withCliRunLog } from "../utils/cli-run-log.js";
7
+ import { registerRun, unregisterRun } from "../utils/run-registry.js";
6
8
 
7
9
  function createCliAskQuestion(opts = {}) {
8
10
  const { sessionId = null } = opts;
@@ -51,36 +53,55 @@ export async function resumeCommand({ sessionId, answer, config, logger, flags }
51
53
  const jsonMode = flags?.json;
52
54
  const quietMode = config.output?.quiet !== false;
53
55
 
54
- const emitter = new EventEmitter();
55
- let activityLog = null;
56
+ // Same wrapper as every other CLI command — without it `.kj/run.log`
57
+ // is never opened during the resume and `kj-tail` stays silent
58
+ // (every line of the resumed run goes only to the per-session
59
+ // activity log + the local console). Also registers the run so the
60
+ // HU Board shows it as live and can offer its Stop button.
61
+ return withCliRunLog("resume", { projectDir: config?.projectDir, logger }, async ({ forwardProgress }) => {
62
+ const runId = registerRun({
63
+ pid: process.pid,
64
+ planId: flags?.plan || null,
65
+ huIds: null,
66
+ projectDir: config?.projectDir || process.cwd(),
67
+ source: "cli-resume",
68
+ });
69
+ const cleanupRegistry = () => { if (runId) unregisterRun(runId); };
70
+ process.once("exit", cleanupRegistry);
71
+ process.once("SIGTERM", () => { cleanupRegistry(); process.exit(143); });
72
+ process.once("SIGINT", () => { cleanupRegistry(); process.exit(130); });
56
73
 
57
- emitter.on("progress", (event) => {
58
- if (!activityLog && event.sessionId) {
59
- activityLog = createActivityLog(event.sessionId);
60
- logger.onLog((entry) => activityLog.write(entry));
61
- }
74
+ const emitter = new EventEmitter();
75
+ let activityLog = null;
76
+ forwardProgress(emitter);
62
77
 
63
- if (activityLog) {
64
- activityLog.writeEvent(event);
65
- }
78
+ emitter.on("progress", (event) => {
79
+ if (!activityLog && event.sessionId) {
80
+ activityLog = createActivityLog(event.sessionId);
81
+ logger.onLog((entry) => activityLog.write(entry));
82
+ }
83
+ if (activityLog) {
84
+ activityLog.writeEvent(event);
85
+ }
86
+ if (!jsonMode) {
87
+ printEvent(event, { quiet: quietMode });
88
+ }
89
+ });
66
90
 
67
- if (!jsonMode) {
68
- printEvent(event, { quiet: quietMode });
69
- }
70
- });
91
+ const askQuestion = createCliAskQuestion();
92
+ const result = await resumeFlow({
93
+ sessionId,
94
+ answer: answer || null,
95
+ config,
96
+ logger,
97
+ flags: flags || {},
98
+ emitter,
99
+ askQuestion
100
+ });
71
101
 
72
- const askQuestion = createCliAskQuestion();
73
- const result = await resumeFlow({
74
- sessionId,
75
- answer: answer || null,
76
- config,
77
- logger,
78
- flags: flags || {},
79
- emitter,
80
- askQuestion
102
+ if (jsonMode || !answer) {
103
+ console.log(JSON.stringify(result, null, 2));
104
+ }
105
+ return result;
81
106
  });
82
-
83
- if (jsonMode || !answer) {
84
- console.log(JSON.stringify(result, null, 2));
85
- }
86
107
  }
@@ -66,64 +66,191 @@ export function needsSetupHu({ isNewProject = false, stackHints = [], subtasks =
66
66
  return subtasks.some(s => setupKeywords.test(s));
67
67
  }
68
68
 
69
+ // Stack-hint keyword sets. Used by filterConflictingHints (drop hints
70
+ // from other ecosystems once one is detected) and by
71
+ // resolveLanguageFromHints (pick the canonical language for setup +
72
+ // acceptance_tests templates).
73
+ const HINT_NODE = new Set(["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "nestjs", "monorepo", "workspaces"]);
74
+ const HINT_PYTHON = new Set(["pytest", "flask", "fastapi", "django", "numpy", "pandas"]);
75
+ const HINT_GO = new Set(["gin", "fiber", "go"]);
76
+ const HINT_RUST = new Set(["cargo", "rust"]);
77
+
69
78
  /**
70
- * Filter conflicting stack hints. When Node.js ecosystem keywords are present,
71
- * remove Go/Rust/Python keywords that were detected from gitignore patterns
72
- * but aren't actually part of the task.
79
+ * Drop hints from OTHER ecosystems when one is clearly the project's.
80
+ * Symmetric: previously only node-vs-go was filtered, so a Python
81
+ * project that also had "vitest" in a stale gitignore would still
82
+ * receive vitest in its setup HU.
73
83
  */
74
84
  function filterConflictingHints(hints) {
75
85
  if (!hints || hints.length === 0) return hints;
76
- const nodeEcosystem = new Set(["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "nestjs", "monorepo", "workspaces"]);
77
- const goKeywords = new Set(["gin", "fiber", "go"]);
78
- const hasNode = hints.some(h => nodeEcosystem.has(h));
79
- if (!hasNode) return hints;
80
- return hints.filter(h => !goKeywords.has(h));
86
+ const setForEachLang = [
87
+ ["python", HINT_PYTHON],
88
+ ["go", HINT_GO],
89
+ ["rust", HINT_RUST],
90
+ ["node", HINT_NODE],
91
+ ];
92
+ const present = setForEachLang.filter(([, set]) => hints.some((h) => set.has(h)));
93
+ if (present.length <= 1) return hints;
94
+ // First ecosystem found in the original hints order wins. We keep
95
+ // ambiguous keywords (those not in any set) and the winner's own
96
+ // keywords; we drop the explicit losers from competing ecosystems.
97
+ const winnerSet = present[0][1];
98
+ const losers = present.slice(1).flatMap(([, s]) => [...s]);
99
+ return hints.filter((h) => winnerSet.has(h) || !losers.includes(h));
100
+ }
101
+
102
+ /**
103
+ * Pick the canonical language for setup + acceptance_test templates
104
+ * based on the (already filtered) stack hints. Falls back to
105
+ * javascript so existing Node-centric runs keep their previous shape.
106
+ * @param {string[]} hints
107
+ * @returns {"python"|"go"|"rust"|"javascript"}
108
+ */
109
+ export function resolveLanguageFromHints(hints) {
110
+ if (!hints || hints.length === 0) return "javascript";
111
+ if (hints.some((h) => HINT_PYTHON.has(h))) return "python";
112
+ if (hints.some((h) => HINT_GO.has(h))) return "go";
113
+ if (hints.some((h) => HINT_RUST.has(h))) return "rust";
114
+ return "javascript";
115
+ }
116
+
117
+ // HU templates per language. The JS template is the legacy default;
118
+ // the others were missing — every autogenerated HU got vitest /
119
+ // npm-install hardcoded acceptance_tests regardless of stack.
120
+ const HU_TEMPLATES = {
121
+ javascript: {
122
+ pkgManager: "npm",
123
+ setupBullets: [
124
+ "- Create package.json (with workspaces if monorepo detected from stack hints)",
125
+ "- Install all runtime + dev dependencies listed in stack hints",
126
+ "- Install test framework WITH coverage reporter (e.g. vitest + @vitest/coverage-v8)",
127
+ "- Configure vitest.config.js with coverage.enabled = true",
128
+ "- Create .env.example with placeholder variables",
129
+ ],
130
+ setupCriteria: [
131
+ "npm install succeeds without errors",
132
+ "npm test runs without error",
133
+ "npm run test:coverage runs without error",
134
+ ".env.example exists",
135
+ ],
136
+ setupTests: [
137
+ "npm install --ignore-scripts 2>&1 && echo PASS || echo FAIL",
138
+ "npx vitest run 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
139
+ "npx vitest run --coverage 2>&1 | grep -q 'All files\\|% Stmts' && echo PASS || echo FAIL",
140
+ "test -f .env.example && echo PASS || echo FAIL",
141
+ ],
142
+ taskTests: [
143
+ "npx vitest run 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
144
+ "npx vitest run --coverage 2>&1 | grep -q 'All files\\|% Stmts' && echo PASS || echo FAIL",
145
+ ],
146
+ },
147
+ python: {
148
+ pkgManager: "pip",
149
+ setupBullets: [
150
+ "- Create pyproject.toml (preferred) or requirements.txt",
151
+ "- Create a virtual env: python -m venv .venv",
152
+ "- Install runtime + dev dependencies (pip install -e . or pip install -r requirements.txt)",
153
+ "- Install pytest with coverage (pip install pytest pytest-cov)",
154
+ "- Create .env.example with placeholder variables",
155
+ ],
156
+ setupCriteria: [
157
+ "Project metadata file exists (pyproject.toml or requirements.txt)",
158
+ "pytest runs without error",
159
+ "pytest --cov reports a total",
160
+ ".env.example exists",
161
+ ],
162
+ setupTests: [
163
+ "(test -f pyproject.toml || test -f requirements.txt) && echo PASS || echo FAIL",
164
+ "python -m pytest 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
165
+ "python -m pytest --cov 2>&1 | grep -q 'TOTAL\\|coverage' && echo PASS || echo FAIL",
166
+ "test -f .env.example && echo PASS || echo FAIL",
167
+ ],
168
+ taskTests: [
169
+ "python -m pytest 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
170
+ "python -m pytest --cov 2>&1 | grep -q 'TOTAL\\|coverage' && echo PASS || echo FAIL",
171
+ ],
172
+ },
173
+ go: {
174
+ pkgManager: "go",
175
+ setupBullets: [
176
+ "- Create go.mod with `go mod init <module>`",
177
+ "- Install dependencies with `go get` per import",
178
+ "- Create .env.example with placeholder variables",
179
+ ],
180
+ setupCriteria: [
181
+ "go.mod exists",
182
+ "go test ./... runs without error",
183
+ "go test -cover reports coverage",
184
+ ".env.example exists",
185
+ ],
186
+ setupTests: [
187
+ "test -f go.mod && echo PASS || echo FAIL",
188
+ "go test ./... 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
189
+ "go test -cover ./... 2>&1 | grep -q 'coverage:' && echo PASS || echo FAIL",
190
+ "test -f .env.example && echo PASS || echo FAIL",
191
+ ],
192
+ taskTests: [
193
+ "go test ./... 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
194
+ ],
195
+ },
196
+ rust: {
197
+ pkgManager: "cargo",
198
+ setupBullets: [
199
+ "- Initialise the crate with `cargo init`",
200
+ "- Add dependencies with `cargo add`",
201
+ "- Create .env.example with placeholder variables",
202
+ ],
203
+ setupCriteria: [
204
+ "Cargo.toml exists",
205
+ "cargo test runs without error",
206
+ ".env.example exists",
207
+ ],
208
+ setupTests: [
209
+ "test -f Cargo.toml && echo PASS || echo FAIL",
210
+ "cargo test 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
211
+ "test -f .env.example && echo PASS || echo FAIL",
212
+ ],
213
+ taskTests: [
214
+ "cargo test 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
215
+ ],
216
+ },
217
+ };
218
+
219
+ function templateFor(language) {
220
+ return HU_TEMPLATES[language] || HU_TEMPLATES.javascript;
81
221
  }
82
222
 
83
223
  /**
84
224
  * Build a MINIMAL setup HU — project structure + deps only.
85
225
  * NEVER includes the full original task. The coder must only do setup.
86
226
  */
87
- function buildSetupHu({ stackHints }) {
227
+ function buildSetupHu({ stackHints, language = "javascript" }) {
228
+ const tpl = templateFor(language);
88
229
  const deps = stackHints.length > 0
89
230
  ? stackHints.map(h => `- ${h}`).join("\n")
90
231
  : "- (auto-detect from subsequent HUs)";
91
232
  const certifiedText = [
92
- "**Setup: initialize project structure and install dependencies.**",
233
+ `**Setup: initialise the ${language} project + install dependencies (package manager: ${tpl.pkgManager}).**`,
93
234
  "",
94
235
  "SCOPE (do ONLY this, nothing else):",
95
- "- Create package.json (with workspaces if monorepo detected from stack hints)",
96
- "- Install all runtime + dev dependencies listed in stack hints",
97
- "- Install test framework WITH coverage reporter (e.g. vitest + @vitest/coverage-v8)",
98
- "- Configure vitest.config.js with coverage.enabled = true",
99
- "- Create .env.example with placeholder variables",
236
+ ...tpl.setupBullets,
100
237
  "- Verify by running each acceptance_test command below",
101
238
  "",
102
239
  "DO NOT implement any business logic, API routes, components, or features.",
103
240
  "This HU is ONLY project scaffolding.",
104
241
  "",
105
242
  "Stack hints:",
106
- deps
243
+ deps,
107
244
  ].join("\n");
108
245
  return {
109
246
  id: "HU-01",
110
- title: "Setup: project structure + dependencies",
247
+ title: `Setup: ${language} project structure + dependencies`,
111
248
  task_type: "infra",
112
249
  status: "certified",
113
250
  blocked_by: [],
114
251
  certified: { text: certifiedText },
115
- acceptance_criteria: [
116
- "npm install succeeds without errors",
117
- "npm test runs without error",
118
- "npm run test:coverage runs without error",
119
- ".env.example exists"
120
- ],
121
- acceptance_tests: [
122
- "npm install --ignore-scripts 2>&1 && echo PASS || echo FAIL",
123
- "npx vitest run 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
124
- "npx vitest run --coverage 2>&1 | grep -q 'All files\\|% Stmts' && echo PASS || echo FAIL",
125
- "test -f .env.example && echo PASS || echo FAIL"
126
- ]
252
+ acceptance_criteria: tpl.setupCriteria,
253
+ acceptance_tests: tpl.setupTests,
127
254
  };
128
255
  }
129
256
 
@@ -131,8 +258,9 @@ function buildSetupHu({ stackHints }) {
131
258
  * Build a MINIMAL task HU — one specific, focused piece of work.
132
259
  * Includes a short goal reference (max 80 chars) NOT the full task.
133
260
  */
134
- function buildTaskHu({ id, subtask, projectName, blockedBy }) {
261
+ function buildTaskHu({ id, subtask, projectName, blockedBy, language = "javascript" }) {
135
262
  const taskType = classifyTaskType(subtask);
263
+ const tpl = templateFor(language);
136
264
  const certifiedText = [
137
265
  `**${subtask}**`,
138
266
  "",
@@ -143,7 +271,7 @@ function buildTaskHu({ id, subtask, projectName, blockedBy }) {
143
271
  "- Add unit tests for the new code",
144
272
  "- Run ALL acceptance_tests listed below and ensure they pass",
145
273
  "- Do NOT touch code outside this subtask's scope",
146
- "- Target: <200 lines changed (like an atomic PR)"
274
+ "- Target: <200 lines changed (like an atomic PR)",
147
275
  ].join("\n");
148
276
  return {
149
277
  id,
@@ -155,12 +283,9 @@ function buildTaskHu({ id, subtask, projectName, blockedBy }) {
155
283
  acceptance_criteria: [
156
284
  `${subtask} is implemented and working`,
157
285
  "Unit tests cover the new code",
158
- "All acceptance_tests pass"
286
+ "All acceptance_tests pass",
159
287
  ],
160
- acceptance_tests: [
161
- "npx vitest run 2>&1; test $? -eq 0 && echo PASS || echo FAIL",
162
- "npx vitest run --coverage 2>&1 | grep -q 'All files\\|% Stmts' && echo PASS || echo FAIL"
163
- ]
288
+ acceptance_tests: tpl.taskTests,
164
289
  };
165
290
  }
166
291
 
@@ -182,7 +307,8 @@ export function generateHuBatch({
182
307
  stackHints = [],
183
308
  isNewProject = false,
184
309
  researcherContext = null,
185
- architectContext = null
310
+ architectContext = null,
311
+ language = null,
186
312
  }) {
187
313
  if (!originalTask || typeof originalTask !== "string") {
188
314
  throw new Error("generateHuBatch: originalTask is required");
@@ -194,12 +320,17 @@ export function generateHuBatch({
194
320
  const stories = [];
195
321
  const filteredHints = filterConflictingHints(stackHints);
196
322
  const needsSetup = needsSetupHu({ isNewProject, stackHints: filteredHints, subtasks });
323
+ // Caller may pass a language explicitly (when it has a real
324
+ // detectProjectStack result from the filesystem); otherwise fall
325
+ // back to inferring from the hint keywords. Default 'javascript'
326
+ // matches the previous behaviour for Node-centric runs.
327
+ const resolvedLanguage = language || resolveLanguageFromHints(filteredHints);
197
328
  let nextId = 1;
198
329
 
199
330
  const projectName = deriveProjectName(originalTask);
200
331
 
201
332
  if (needsSetup) {
202
- stories.push(buildSetupHu({ stackHints: filteredHints }));
333
+ stories.push(buildSetupHu({ stackHints: filteredHints, language: resolvedLanguage }));
203
334
  nextId = 2;
204
335
  }
205
336
 
@@ -211,7 +342,7 @@ export function generateHuBatch({
211
342
  const blockedBy = [];
212
343
  if (setupId) blockedBy.push(setupId);
213
344
  if (previousId && previousId !== setupId) blockedBy.push(previousId);
214
- stories.push(buildTaskHu({ id, subtask, projectName, blockedBy }));
345
+ stories.push(buildTaskHu({ id, subtask, projectName, blockedBy, language: resolvedLanguage }));
215
346
  previousId = id;
216
347
  nextId += 1;
217
348
  }
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  import { emitProgress, makeEvent } from "#utils/events.js";
19
+ import { detectProjectStack } from "#utils/stack-detect.js";
19
20
 
20
21
  export async function maybeGenerateAutoHuBatch({
21
22
  flags, stageResults, task, logger, emitter, eventBase, projectDir, session,
@@ -40,19 +41,38 @@ export async function maybeGenerateAutoHuBatch({
40
41
  isNewProject = relevant.length === 0;
41
42
  } catch { /* ignore */ }
42
43
 
43
- // Extract stack hints from planner + architect output
44
+ // Stack hints from planner + architect text. Now multi-ecosystem
45
+ // (was 13/16 JS keywords — Python/Go/Rust barely existed). Combined
46
+ // with `detectProjectStack(projectDir)` below to override the text
47
+ // heuristic with the actual filesystem reality when available.
44
48
  const stackHints = [];
45
49
  const combined = `${stageResults.planner?.plan || ""} ${stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : ""} ${task}`.toLowerCase();
46
- const stackKeywords = ["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "fastapi", "django", "spring", "gin", "nestjs", "monorepo", "workspaces"];
50
+ const stackKeywords = [
51
+ "express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "nestjs", "monorepo", "workspaces",
52
+ "pytest", "flask", "fastapi", "django", "numpy", "pandas",
53
+ "gin", "fiber", "go",
54
+ "cargo", "rust",
55
+ "spring",
56
+ ];
47
57
  for (const kw of stackKeywords) {
48
58
  if (combined.includes(kw)) stackHints.push(kw);
49
59
  }
50
60
 
61
+ // Filesystem trumps text. detectProjectStack inspects package.json /
62
+ // pyproject.toml / go.mod / Cargo.toml, so a pure-python repo gets
63
+ // `language: "python"` regardless of what the task text says.
64
+ let detectedLanguage = null;
65
+ try {
66
+ const fsStack = await detectProjectStack(projectDir);
67
+ if (fsStack?.language) detectedLanguage = fsStack.language;
68
+ } catch { /* best-effort */ }
69
+
51
70
  const batch = generateHuBatch({
52
71
  originalTask: task,
53
72
  subtasks,
54
73
  stackHints,
55
74
  isNewProject,
75
+ language: detectedLanguage,
56
76
  researcherContext: stageResults.researcher?.summary || null,
57
77
  architectContext: stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : null
58
78
  });
@@ -6,8 +6,8 @@
6
6
 
7
7
  import fs from "node:fs/promises";
8
8
  import path from "node:path";
9
- import os from "node:os";
10
9
  import { writeJsonAtomic } from "../utils/atomic-write.js";
10
+ import { resolveHome } from "../utils/paths.js";
11
11
  import { generatePlanId, normaliseAlias } from "./plan-id.js";
12
12
 
13
13
  /**
@@ -38,33 +38,13 @@ async function handleCorruptPlanFile(filePath, error) {
38
38
  }
39
39
  import { isPlanV2, migratePlanV1toV2 } from "./plan-schema.js";
40
40
 
41
- // Per-process tmp dir for VITEST runs that forget to set KJ_HOME. We
42
- // memoise across calls so every helper in the same test process sees
43
- // the same root (mirrors what setting KJ_HOME explicitly would do)
44
- // without racing. The trailing `.kj` segment keeps semantic parity
45
- // with non-VITEST defaults so existing path assertions don't break.
46
- let _vitestKjHome = null;
47
- function vitestTmpKjHome() {
48
- if (_vitestKjHome) return _vitestKjHome;
49
- _vitestKjHome = path.join(
50
- os.tmpdir(),
51
- `kj-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`,
52
- ".kj"
53
- );
54
- return _vitestKjHome;
55
- }
56
-
41
+ // Delegates to the unified resolver in src/utils/paths.js. PR 3 of
42
+ // KJC-PCS-0047 flipped the default from `.kj` to `.karajan` — plans
43
+ // now live under `~/.karajan/plans/`. The migrator (src/utils/
44
+ // home-migration.js) auto-moves legacy `~/.kj/plans/` content on the
45
+ // next `kj` invocation, so existing users are not stranded.
57
46
  export function getKjHome() {
58
- if (process.env.KJ_HOME) return process.env.KJ_HOME;
59
- // VITEST guard — without this, a test that calls savePlan() drops
60
- // fixture files into the developer's real `~/.kj/plans/` and the
61
- // running HU Board's chokidar watcher ingests them as "real" plans
62
- // (observed: 49 fake "Add auth" plans contaminating the user's
63
- // board). Auto-isolate to a per-process tmp dir so accidental
64
- // writes can't leak. Tests that NEED to assert against a known
65
- // path should still set KJ_HOME explicitly.
66
- if (process.env.VITEST) return vitestTmpKjHome();
67
- return path.join(os.homedir(), ".kj");
47
+ return resolveHome({ defaultSegment: ".karajan" });
68
48
  }
69
49
 
70
50
  /**
@@ -33,7 +33,7 @@ import { withBrainRecovery } from "../brain/with-brain-recovery.js";
33
33
  * @param {Array<{ id: string, title?: string, scope?: string }>} args.husNeedingTests
34
34
  * @returns {string}
35
35
  */
36
- export function buildSynthesizerPrompt({ task, husNeedingTests }) {
36
+ export function buildSynthesizerPrompt({ task, husNeedingTests, stack = null, testFramework = null }) {
37
37
  const lines = [
38
38
  "You previously generated an implementation plan for the task below, but some HUs were left without acceptance_tests.",
39
39
  "Your ONLY job in this call is to fill that gap.",
@@ -59,12 +59,22 @@ export function buildSynthesizerPrompt({ task, husNeedingTests }) {
59
59
  "- Tests must be writable BEFORE the HU is implemented (they should fail until done).",
60
60
  "- Return ONLY the JSON — no prose, no markdown fences around it.",
61
61
  "",
62
- "## Task",
63
- task,
64
- "",
65
- "## HUs that need acceptance_tests",
66
- "",
67
62
  ];
63
+ // Stack-aware hint: tell the LLM what test framework / language to
64
+ // emit shell commands for. Without this it defaulted to vitest /
65
+ // npm even on Python / Go / Rust projects (sesgo-de-stack audit).
66
+ if (stack || testFramework) {
67
+ lines.push("## Project Stack — generate shell tests for THIS stack, not generic JS");
68
+ if (stack?.language) lines.push(`- Language: **${stack.language}**`);
69
+ if (stack?.frameworks?.length) lines.push(`- Frameworks: ${stack.frameworks.join(", ")}`);
70
+ if (testFramework?.hasTests && testFramework?.framework) {
71
+ lines.push(`- Test framework already in use: **${testFramework.framework}** — use it for the shell commands.`);
72
+ } else if (stack?.language && stack.language !== "javascript" && stack.language !== "typescript") {
73
+ lines.push(`- Use the canonical test runner for ${stack.language} (pytest / go test / cargo test / rspec). Do NOT emit vitest, jest, or npm commands.`);
74
+ }
75
+ lines.push("");
76
+ }
77
+ lines.push("## Task", task, "", "## HUs that need acceptance_tests", "");
68
78
  for (const hu of husNeedingTests) {
69
79
  lines.push(`### ${hu.id}: ${hu.title || ""}`);
70
80
  lines.push(hu.scope ? `Scope: ${hu.scope}` : "(no scope)");
@@ -158,7 +158,28 @@ function renderReviewerFindingsSection(findings, huId) {
158
158
  ].join("\n");
159
159
  }
160
160
 
161
- export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en", provider = null, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null }) {
161
+ // KJC sesgo-de-stack: Karajan used to default every prompt to JS/TS
162
+ // (vitest, npm install, console.log, JSDoc, httpOnly cookies). When
163
+ // the project is Python / Go / Rust / Ruby, that produced absurd
164
+ // outputs (e.g. installing vitest in a pure-python repo to satisfy a
165
+ // hardcoded `npx vitest run` acceptance test). When we know the
166
+ // stack we tell the coder explicitly so it stops assuming.
167
+ function buildStackSection({ stack, testFramework }) {
168
+ if (!stack && !testFramework) return null;
169
+ const lines = ["## Project Stack — match this, don't assume JavaScript"];
170
+ if (stack?.language) lines.push(`- Language: **${stack.language}**`);
171
+ if (stack?.frameworks?.length) lines.push(`- Frameworks: ${stack.frameworks.join(", ")}`);
172
+ if (testFramework?.hasTests && testFramework?.framework) {
173
+ lines.push(`- Test framework already in use: **${testFramework.framework}** (${testFramework.language || stack?.language || "unknown"}). Use it for new tests; do NOT add a JS test framework on top.`);
174
+ } else if (stack?.language && stack.language !== "javascript" && stack.language !== "typescript") {
175
+ lines.push(`- No test framework detected yet. Pick the canonical one for ${stack.language} (e.g. pytest for python, \`go test\` for go, \`cargo test\` for rust, rspec for ruby). Do NOT use vitest / jest / \`npm install\` in a non-JS project.`);
176
+ }
177
+ lines.push("");
178
+ lines.push("If an acceptance_test command on this HU points at a foreign framework (e.g. `npx vitest run` on a python project), REPLACE the command with the equivalent for the project's actual stack and use that to verify your work.");
179
+ return lines.join("\n");
180
+ }
181
+
182
+ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en", provider = null, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null, stack = null, testFramework = null }) {
162
183
  const langInstruction = getLanguageInstruction(language);
163
184
  // KJC-BUG-0032 (PR-I): hard rule about file paths. Real bug: a HU
164
185
  // titled "Initialize project skeleton: create assistant/ directory…"
@@ -190,10 +211,12 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
190
211
  "Keep changes minimal and production-ready.",
191
212
  "Follow SOLID principles. Write small, focused functions (< 30 lines).",
192
213
  "Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.",
193
- "Security: use httpOnly cookies for auth tokens, validate all input, parameterize queries, never expose secrets.",
194
- "No console.log — use a structured logger. No 'any' types — use JSDoc annotations.",
214
+ "Security: validate all input, parameterize queries, never expose secrets. Use the auth pattern that matches the stack (e.g. httpOnly cookies for web sessions).",
215
+ "Use the project's standard logger and type system (no console.log when a structured logger exists; use the project's type idiom — TS types, JSDoc, Python type hints, Go interfaces…).",
195
216
  SUBPROCESS_CONSTRAINTS
196
217
  ];
218
+ const stackRule = buildStackSection({ stack, testFramework });
219
+ if (stackRule) sections.push(stackRule);
197
220
  if (projectDirRule) sections.push(projectDirRule);
198
221
 
199
222
  if (serenaEnabled) {
@@ -268,7 +291,7 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
268
291
  "1. Find the relevant file(s) in the project",
269
292
  "2. Make the specific code change needed to resolve the issue",
270
293
  "3. If tests are missing, write them",
271
- "4. If dependencies are needed, install them (npm install)",
294
+ "4. If dependencies are needed, install them with the project's package manager (npm install / pip install / go get / cargo add — pick the one that matches the stack above)",
272
295
  "5. Do NOT skip any issue — all must be resolved"
273
296
  );
274
297
  }
@@ -2,6 +2,8 @@ import { AgentRole } from "./agent-role.js";
2
2
  import { buildCoderPrompt } from "../prompts/coder.js";
3
3
  import { isHostAgent } from "../utils/agent-detect.js";
4
4
  import { HostAgent } from "../agents/host-agent.js";
5
+ import { detectProjectStack } from "../utils/stack-detect.js";
6
+ import { detectTestFramework } from "../utils/project-detect.js";
5
7
 
6
8
  export class CoderRole extends AgentRole {
7
9
  constructor(opts) {
@@ -54,6 +56,16 @@ export class CoderRole extends AgentRole {
54
56
  }
55
57
 
56
58
  async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId }) {
59
+ const projectDir = this.config?.projectDir || null;
60
+ // KJC sesgo-de-stack: pasar stack/testFramework al prompt para que
61
+ // el coder no asuma JS/vitest en proyectos Python / Go / Rust /
62
+ // Ruby. audit-role.js y tester-role.js ya lo hacen así.
63
+ let stack = null;
64
+ let testFramework = null;
65
+ if (projectDir) {
66
+ try { stack = await detectProjectStack(projectDir); } catch { /* best-effort */ }
67
+ try { testFramework = await detectTestFramework(projectDir); } catch { /* best-effort */ }
68
+ }
57
69
  const prompt = await buildCoderPrompt({
58
70
  task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests,
59
71
  adrs, specSection, reviewerFindings, huId,
@@ -63,7 +75,8 @@ export class CoderRole extends AgentRole {
63
75
  rtkAvailable: Boolean(this.config?.rtk?.available),
64
76
  productContext: this.config?.productContext || null,
65
77
  domainContext: this.config?.domainContext || null,
66
- projectDir: this.config?.projectDir || null,
78
+ projectDir,
79
+ stack, testFramework,
67
80
  provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null)
68
81
  });
69
82
  return { prompt };
@@ -30,7 +30,7 @@
30
30
 
31
31
  import fs from "node:fs/promises";
32
32
  import path from "node:path";
33
- import os from "node:os";
33
+ import { resolveHome } from "./paths.js";
34
34
 
35
35
  const FINAL_PLAN_STATUSES = new Set([
36
36
  // Terminal plan states the user explicitly opted into — retention applies.
@@ -39,13 +39,15 @@ const FINAL_PLAN_STATUSES = new Set([
39
39
  const KNOWN_PLAN_STATUSES = new Set([...FINAL_PLAN_STATUSES, "draft", "running"]);
40
40
  const FINAL_SESSION_STATUSES = new Set(["approved", "failed", "stopped", "rejected", "completed"]);
41
41
 
42
- function getKjHome() {
43
- return process.env.KJ_HOME || path.join(os.homedir(), ".kj");
44
- }
45
-
42
+ // Post-PR-3 of KJC-PCS-0047 both helpers point at the canonical
43
+ // `~/.karajan/` root. `getKjHome` is kept as an alias of
44
+ // `getKarajanHome` so legacy callers inside the file continue to
45
+ // compile during the consolidation; both names resolve to the same
46
+ // directory now that the layout is unified.
46
47
  function getKarajanHome() {
47
- return process.env.KARAJAN_HOME || path.join(os.homedir(), ".karajan");
48
+ return resolveHome({ defaultSegment: ".karajan" });
48
49
  }
50
+ const getKjHome = getKarajanHome;
49
51
 
50
52
  const DAY_MS = 24 * 60 * 60 * 1000;
51
53
 
@@ -0,0 +1,87 @@
1
+ // One-shot ~/.kj/ → ~/.karajan/ home consolidation (PR 2 of KJC-PCS-0047).
2
+ // Idempotent via `<target>/.kj-migrated.json`. Tarball backup under
3
+ // `<target>/backup/`. `plans/`, `standby/`, `worktrees/` are moved
4
+ // wholesale; `runs/` is merged with `.karajan/` winning on conflict.
5
+ // Cross-device safe. Non-fatal: CLI bootstrap wraps in try/catch.
6
+
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ import { execa } from "execa";
11
+
12
+ const MARKER = ".kj-migrated.json";
13
+ const MOVE_DIRS = ["plans", "standby", "worktrees"];
14
+
15
+ async function readdirOrNull(dir) {
16
+ try { return await fs.readdir(dir); } catch (err) {
17
+ if (err.code === "ENOENT") return null;
18
+ throw err;
19
+ }
20
+ }
21
+
22
+ async function moveChildren(src, dst, { mergeMode = false } = {}) {
23
+ const children = await readdirOrNull(src);
24
+ if (!children?.length) return 0;
25
+ await fs.mkdir(dst, { recursive: true });
26
+ let n = 0;
27
+ for (const c of children) {
28
+ const dstChild = path.join(dst, c);
29
+ if (mergeMode) {
30
+ try { await fs.access(dstChild); continue; } catch { /* no conflict */ }
31
+ }
32
+ const srcChild = path.join(src, c);
33
+ try { await fs.rename(srcChild, dstChild); } catch (err) {
34
+ if (err.code !== "EXDEV") throw err;
35
+ await fs.cp(srcChild, dstChild, { recursive: true });
36
+ await fs.rm(srcChild, { recursive: true, force: true });
37
+ }
38
+ n += 1;
39
+ }
40
+ await fs.rm(src, { recursive: true, force: true }).catch(() => {});
41
+ return n;
42
+ }
43
+
44
+ export async function migrateKjToKarajan({ dryRun = false, force = false } = {}) {
45
+ // VITEST guard — tests setting KJ_HOME would otherwise migrate against
46
+ // the developer's real HOME. Migrator's own tests pass `force: true`.
47
+ if (process.env.VITEST && !force) return { migrated: false, reason: "vitest-skip" };
48
+
49
+ const sourceDir = process.env.KJ_HOME || path.join(os.homedir(), ".kj");
50
+ const targetDir = process.env.KARAJAN_HOME || path.join(os.homedir(), ".karajan");
51
+
52
+ if (path.resolve(sourceDir) === path.resolve(targetDir)) {
53
+ return { migrated: false, reason: "same-path" };
54
+ }
55
+ const entries = await readdirOrNull(sourceDir);
56
+ if (!entries) return { migrated: false, reason: "source-missing" };
57
+ if (entries.length === 0) return { migrated: false, reason: "source-empty" };
58
+
59
+ await fs.mkdir(targetDir, { recursive: true });
60
+ const markerPath = path.join(targetDir, MARKER);
61
+ try { await fs.access(markerPath); return { migrated: false, reason: "already-migrated" }; }
62
+ catch { /* not present — proceed */ }
63
+
64
+ if (dryRun) return { migrated: false, reason: "dry-run", sourceDir, targetDir };
65
+
66
+ const backupDir = path.join(targetDir, "backup");
67
+ await fs.mkdir(backupDir, { recursive: true });
68
+ const ts = new Date().toISOString().replaceAll(/[:.]/g, "-");
69
+ const backupPath = path.join(backupDir, `kj-pre-migration-${ts}.tar.gz`);
70
+ await execa("tar", ["-czf", backupPath, "-C", path.dirname(sourceDir), path.basename(sourceDir)]);
71
+
72
+ const counts = {};
73
+ for (const sub of MOVE_DIRS) {
74
+ counts[sub] = await moveChildren(path.join(sourceDir, sub), path.join(targetDir, sub));
75
+ }
76
+ counts.runs = await moveChildren(path.join(sourceDir, "runs"), path.join(targetDir, "runs"), { mergeMode: true });
77
+
78
+ const marker = { version: 1, migrated_at: new Date().toISOString(), items_moved: counts, backup_path: backupPath, source_dir: sourceDir, target_dir: targetDir };
79
+ await fs.writeFile(markerPath, JSON.stringify(marker, null, 2));
80
+
81
+ const summary = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(" + ");
82
+ if (summary) {
83
+ // eslint-disable-next-line no-console
84
+ console.warn(`\x1b[33m[warn]\x1b[0m Migrated ${summary} from ${sourceDir} to ${targetDir} (backup: ${backupPath})`);
85
+ }
86
+ return { migrated: true, counts, backupPath, marker };
87
+ }
@@ -1,32 +1,85 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
- // Per-process tmp dir for VITEST runs that forget to set KJ_HOME. Same
5
- // rationale as src/plan/plan-store.js — without this, tests that use
6
- // real session/board paths leak files into the developer's real
7
- // `~/.karajan/` and pollute the HU Board's SQLite DB. Memoised so
8
- // every helper in the same process agrees on the path. The trailing
9
- // segment is `.karajan` to keep semantic parity with non-VITEST
10
- // defaults (existing tests assert paths contain ".karajan/...").
11
- let _vitestKarajanHome = null;
12
- function vitestTmpKarajanHome() {
13
- if (_vitestKarajanHome) return _vitestKarajanHome;
14
- _vitestKarajanHome = path.join(
4
+ // Per-process vitest root. We memoise the *root* (not the suffixed
5
+ // `.karajan` / `.kj` path) so callers with different legacy defaults
6
+ // — plan-store wants `.kj`, db.js wants `.karajan` — can share one
7
+ // random tmp prefix per test run without colliding.
8
+ let _vitestRoot = null;
9
+ function vitestRoot() {
10
+ if (_vitestRoot) return _vitestRoot;
11
+ _vitestRoot = path.join(
15
12
  os.tmpdir(),
16
- `karajan-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`,
17
- ".karajan"
13
+ `karajan-vitest-${process.pid}-${Math.random().toString(36).slice(2, 10)}`
18
14
  );
19
- return _vitestKarajanHome;
15
+ return _vitestRoot;
20
16
  }
21
17
 
22
- export function getKarajanHome() {
18
+ function vitestTmpHome(defaultSegment) {
19
+ return path.join(vitestRoot(), defaultSegment);
20
+ }
21
+
22
+ // One-shot warning so users with KJ_HOME set in their shell rcfile do
23
+ // not get spammed once per `kj` invocation. Reset across processes,
24
+ // which is the granularity that matters for a humans-reading-stderr
25
+ // audience.
26
+ let _kjHomeWarned = false;
27
+ function emitKjHomeDeprecationWarning() {
28
+ if (_kjHomeWarned) return;
29
+ _kjHomeWarned = true;
30
+ // eslint-disable-next-line no-console
31
+ console.warn(
32
+ "\x1b[33m[warn]\x1b[0m KJ_HOME is deprecated, rename to KARAJAN_HOME (KJ_HOME will be removed in a future release)"
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Unified resolver for Karajan's HOME-level storage root. Used by every
38
+ * helper that previously rolled its own `getKjHome()` with subtly
39
+ * different defaults and VITEST handling.
40
+ *
41
+ * Precedence (highest first):
42
+ * 1. KARAJAN_HOME env var — explicit, no warning
43
+ * 2. KJ_HOME env var — explicit, prints deprecation warning once
44
+ * 3. VITEST tmp dir — auto-isolation under `os.tmpdir()/karajan-vitest-<pid>-<rand>/<defaultSegment>`
45
+ * 4. `~/<defaultSegment>` — production default
46
+ *
47
+ * Callers pass `defaultSegment` so we can preserve `.kj` for legacy
48
+ * paths (plan-store, standby-store) and `.karajan` for the canonical
49
+ * root, until PR 3 unifies the defaults too.
50
+ *
51
+ * @param {object} [options]
52
+ * @param {string} [options.defaultSegment=".karajan"] HOME subdir name
53
+ * @returns {string} absolute path
54
+ */
55
+ export function resolveHome({ defaultSegment = ".karajan" } = {}) {
56
+ if (process.env.KARAJAN_HOME) {
57
+ return path.resolve(process.env.KARAJAN_HOME);
58
+ }
23
59
  if (process.env.KJ_HOME) {
60
+ emitKjHomeDeprecationWarning();
24
61
  return path.resolve(process.env.KJ_HOME);
25
62
  }
26
63
  if (process.env.VITEST) {
27
- return vitestTmpKarajanHome();
64
+ return vitestTmpHome(defaultSegment);
28
65
  }
29
- return path.join(os.homedir(), ".karajan");
66
+ return path.join(os.homedir(), defaultSegment);
67
+ }
68
+
69
+ /**
70
+ * Canonical Karajan home (`~/.karajan` by default). Every helper that
71
+ * used to roll its own `getKjHome()` now goes through this one — see
72
+ * KJC-PCS-0047 PR 3 for the consolidation.
73
+ */
74
+ export function getKarajanHome() {
75
+ return resolveHome({ defaultSegment: ".karajan" });
76
+ }
77
+
78
+ // Test-only export. Tests that depend on observing the deprecation
79
+ // warning being emitted exactly once need to reset the latch between
80
+ // cases.
81
+ export function __resetKjHomeWarningForTests() {
82
+ _kjHomeWarned = false;
30
83
  }
31
84
 
32
85
  export function getSessionRoot() {