karajan-code 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -1,31 +1,36 @@
1
1
  #!/bin/sh
2
- # Karajan Code — standalone binary installer (no Node required).
2
+ # Karajan Code installer — guarantees a COMPLETE install (KJC-TSK-0658).
3
3
  #
4
4
  # curl -fsSL https://karajancode.com/install.sh | sh
5
5
  #
6
- # Downloads the prebuilt `kj` binary for your OS/arch from the GitHub
7
- # release, verifies its SHA256 checksum, and installs it to ~/.local/bin.
8
- # Override with env vars: KJ_VERSION (e.g. v3.7.2), KJ_INSTALL_DIR.
6
+ # Default route: npm — the full product (CLI + RAG + HU Board + MCP).
7
+ # 1. Node >= 22.12 present → npm install -g karajan-code
8
+ # 2. No usable Node → auto-provision official Node LTS into
9
+ # ~/.karajan/node (checksum-verified, nothing system-wide touched),
10
+ # install karajan-code with it, symlink kj into ~/.local/bin.
11
+ # Standalone route (CLI only, no native-module features — RAG/board/MCP
12
+ # unavailable): opt-in with `--standalone` or KJ_STANDALONE=1.
9
13
  #
10
- # POSIX sh only — no bashisms, no Node. KJC-TSK-0593.
14
+ # POSIX sh only. Env overrides: KJ_VERSION, KJ_INSTALL_DIR.
11
15
  set -eu
12
16
 
13
17
  REPO="manufosela/karajan-code"
14
18
  VERSION="${KJ_VERSION:-latest}"
15
19
  INSTALL_DIR="${KJ_INSTALL_DIR:-$HOME/.local/bin}"
20
+ NODE_MAJOR="22"
21
+ NODE_MIN_MINOR="12"
22
+ MODE="full"
23
+ [ "${1:-}" = "--standalone" ] && MODE="standalone"
24
+ [ "${KJ_STANDALONE:-0}" = "1" ] && MODE="standalone"
16
25
 
17
- die() {
18
- echo "kj-install: $1" >&2
19
- exit 1
20
- }
26
+ die() { echo "kj-install: $1" >&2; exit 1; }
21
27
 
22
- # --- Detect OS/arch, map to the release asset names we actually build. ---
23
- os="$(uname -s)"
24
- arch="$(uname -m)"
28
+ # --- Detect OS/arch (shared by both routes). ---
29
+ os="$(uname -s)"; arch="$(uname -m)"
25
30
  case "$os" in
26
31
  Linux) os="linux" ;;
27
32
  Darwin) os="darwin" ;;
28
- *) die "unsupported OS '$os'. Supported: Linux, macOS. Use npm instead: npm install -g karajan-code" ;;
33
+ *) die "unsupported OS '$os'. Supported: Linux, macOS." ;;
29
34
  esac
30
35
  case "$arch" in
31
36
  x86_64 | amd64) arch="x64" ;;
@@ -33,80 +38,122 @@ case "$arch" in
33
38
  *) die "unsupported architecture '$arch'" ;;
34
39
  esac
35
40
 
36
- target="${os}-${arch}"
37
- case "$target" in
38
- linux-x64) ;;
39
- darwin-arm64) ;;
40
- *) die "no prebuilt binary for '$target'. Available: linux-x64, darwin-arm64. Use npm instead: npm install -g karajan-code" ;;
41
- esac
41
+ # --- Downloader + checksum tool. ---
42
+ if command -v curl >/dev/null 2>&1; then fetch() { curl -fsSL "$1" -o "$2"; }
43
+ elif command -v wget >/dev/null 2>&1; then fetch() { wget -qO "$2" "$1"; }
44
+ else die "need curl or wget"; fi
45
+ if command -v sha256sum >/dev/null 2>&1; then sha256() { sha256sum "$1" | cut -d' ' -f1; }
46
+ elif command -v shasum >/dev/null 2>&1; then sha256() { shasum -a 256 "$1" | cut -d' ' -f1; }
47
+ else die "need sha256sum or shasum to verify downloads"; fi
42
48
 
43
- # --- Resolve the download URL for the requested version (or latest). ---
44
- asset="kj-${target}"
45
- if [ "$VERSION" = "latest" ]; then
46
- base="https://github.com/${REPO}/releases/latest/download"
47
- else
48
- base="https://github.com/${REPO}/releases/download/${VERSION}"
49
- fi
49
+ tmp="$(mktemp -d "${TMPDIR:-/tmp}/kj-install.XXXXXX")"
50
+ trap 'rm -rf "$tmp"' EXIT INT TERM
50
51
 
51
- # --- Pick a downloader. ---
52
- if command -v curl >/dev/null 2>&1; then
53
- fetch() { curl -fsSL "$1" -o "$2"; }
54
- elif command -v wget >/dev/null 2>&1; then
55
- fetch() { wget -qO "$2" "$1"; }
56
- else
57
- die "need curl or wget to download the binary"
58
- fi
52
+ path_hint() {
53
+ case ":${PATH}:" in
54
+ *":$1:"*) ;;
55
+ *) echo "kj-install: add '$1' to your PATH: export PATH=\"$1:\$PATH\" (persist it in ~/.bashrc / ~/.zshrc)" ;;
56
+ esac
57
+ }
58
+
59
+ # --- Is there a usable Node (>= NODE_MAJOR.NODE_MIN_MINOR)? ---
60
+ node_ok() {
61
+ command -v node >/dev/null 2>&1 || return 1
62
+ v="$(node --version 2>/dev/null | sed 's/^v//')"
63
+ major="${v%%.*}"; rest="${v#*.}"; minor="${rest%%.*}"
64
+ [ "$major" -gt "$NODE_MAJOR" ] 2>/dev/null && return 0
65
+ [ "$major" -eq "$NODE_MAJOR" ] 2>/dev/null && [ "$minor" -ge "$NODE_MIN_MINOR" ] 2>/dev/null
66
+ }
67
+
68
+ npm_pkg() { if [ "$VERSION" = "latest" ]; then echo "karajan-code"; else echo "karajan-code@${VERSION#v}"; fi; }
69
+
70
+ if [ "$MODE" = "full" ]; then
71
+ if node_ok; then
72
+ echo "kj-install: Node $(node --version) found — installing via npm (full product)..."
73
+ npm install -g "$(npm_pkg)" || die "npm install failed. If it was a permissions error, set a user prefix (npm config set prefix ~/.local) and re-run."
74
+ echo "kj-install: installed $(kj --version 2>/dev/null || echo karajan-code). Run 'kj doctor' next."
75
+ exit 0
76
+ fi
59
77
 
60
- # --- Pick a checksum tool. ---
61
- if command -v sha256sum >/dev/null 2>&1; then
62
- sha256() { sha256sum "$1" | cut -d' ' -f1; }
63
- elif command -v shasum >/dev/null 2>&1; then
64
- sha256() { shasum -a 256 "$1" | cut -d' ' -f1; }
65
- else
66
- die "need sha256sum or shasum to verify the download"
78
+ echo "kj-install: no usable Node (need >= ${NODE_MAJOR}.${NODE_MIN_MINOR}) — provisioning official Node LTS into ~/.karajan/node (nothing system-wide)..."
79
+ dist="https://nodejs.org/dist/latest-v${NODE_MAJOR}.x"
80
+ fetch "${dist}/SHASUMS256.txt" "${tmp}/SHASUMS256.txt" || die "could not fetch the Node checksum list"
81
+ node_asset="$(grep -o "node-v[0-9.]*-${os}-${arch}\.tar\.gz" "${tmp}/SHASUMS256.txt" | head -1)"
82
+ [ -n "$node_asset" ] || die "no official Node build for ${os}-${arch}"
83
+ echo "kj-install: downloading ${node_asset}..."
84
+ fetch "${dist}/${node_asset}" "${tmp}/${node_asset}" || die "could not download Node"
85
+ expected="$(grep "${node_asset}\$" "${tmp}/SHASUMS256.txt" | head -1 | cut -d' ' -f1)"
86
+ actual="$(sha256 "${tmp}/${node_asset}")"
87
+ [ "$expected" = "$actual" ] || die "Node checksum mismatch — aborting, nothing installed"
88
+
89
+ # Stage the whole install (extract + npm install) in a sibling dir and only
90
+ # swap it into place once EVERYTHING succeeded — a failed download/extract/
91
+ # install must never destroy a previous working ~/.karajan/node.
92
+ node_home="$HOME/.karajan/node"
93
+ staging="${node_home}.staging.$$"
94
+ rm -rf "$staging"; mkdir -p "$staging"
95
+ trap 'rm -rf "$tmp" "$staging"' EXIT INT TERM
96
+ tar -xzf "${tmp}/${node_asset}" -C "$staging" --strip-components=1 || die "could not extract Node"
97
+
98
+ echo "kj-install: installing karajan-code with the provisioned Node..."
99
+ PATH="${staging}/bin:$PATH" "${staging}/bin/npm" install -g "$(npm_pkg)" || die "npm install failed with the provisioned Node"
100
+
101
+ # Swap keeping the previous install recoverable at EVERY instant: park it
102
+ # as a backup, arm a trap that restores it on any exit (signal included),
103
+ # move the staged one in, and only then disarm the trap and drop the
104
+ # backup — the user can never end up without a working install.
105
+ backup="${node_home}.old.$$"
106
+ if [ -e "$node_home" ]; then
107
+ mv "$node_home" "$backup" || die "could not park the previous install"
108
+ trap '[ -e "$node_home" ] || mv "$backup" "$node_home" 2>/dev/null; rm -rf "$tmp" "$staging"' EXIT INT TERM
109
+ fi
110
+ mv "$staging" "$node_home" || die "could not move the staged install into place (previous install restored)"
111
+ trap 'rm -rf "$tmp"' EXIT INT TERM
112
+ rm -rf "$backup"
113
+ mkdir -p "$INSTALL_DIR"
114
+ # Wrappers, not symlinks: the shebang (#!/usr/bin/env node) must find the
115
+ # provisioned Node even though it is not on the user's PATH.
116
+ for bin in kj karajan-mcp; do
117
+ [ -e "${node_home}/bin/${bin}" ] || continue
118
+ {
119
+ echo '#!/bin/sh'
120
+ echo "export PATH=\"${node_home}/bin:\$PATH\""
121
+ echo "exec \"${node_home}/bin/${bin}\" \"\$@\""
122
+ } >"${INSTALL_DIR}/${bin}"
123
+ chmod +x "${INSTALL_DIR}/${bin}"
124
+ done
125
+ installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
126
+ echo "kj-install: installed kj ${installed} (full product) — kj at ${INSTALL_DIR}/kj"
127
+ path_hint "$INSTALL_DIR"
128
+ echo "kj-install: next — run 'kj doctor', then 'kj install-tools' to complete the whole stack."
129
+ exit 0
67
130
  fi
68
131
 
69
- # --- Download to a temp dir; only install after the checksum matches. ---
70
- tmp="$(mktemp -d "${TMPDIR:-/tmp}/kj-install.XXXXXX")"
71
- trap 'rm -rf "$tmp"' EXIT INT TERM
132
+ # --- Standalone route: prebuilt single binary (CLI only). ---
133
+ echo "kj-install: standalone mode — single binary, NO native-module features (RAG, HU Board and MCP need the npm install)."
134
+ target="${os}-${arch}"
135
+ case "$target" in
136
+ linux-x64 | darwin-arm64) ;;
137
+ *) die "no prebuilt binary for '$target' (available: linux-x64, darwin-arm64) — run without --standalone for the npm route" ;;
138
+ esac
139
+ asset="kj-${target}"
140
+ if [ "$VERSION" = "latest" ]; then base="https://github.com/${REPO}/releases/latest/download"
141
+ else base="https://github.com/${REPO}/releases/download/${VERSION}"; fi
72
142
 
73
143
  echo "kj-install: downloading ${asset} (${VERSION})..."
74
- fetch "${base}/${asset}" "${tmp}/kj" || die "could not download ${base}/${asset} — does that version/asset exist?"
144
+ fetch "${base}/${asset}" "${tmp}/kj" || die "could not download ${base}/${asset}"
75
145
  fetch "${base}/${asset}.sha256" "${tmp}/kj.sha256" || die "could not download the checksum for ${asset}"
76
-
77
146
  expected="$(cut -d' ' -f1 <"${tmp}/kj.sha256")"
78
147
  actual="$(sha256 "${tmp}/kj")"
79
- [ "$expected" = "$actual" ] || die "checksum mismatch (expected ${expected}, got ${actual}). Aborting, nothing installed."
148
+ [ "$expected" = "$actual" ] || die "checksum mismatch — aborting, nothing installed"
80
149
 
81
- # --- Install atomically: chmod on the temp file, then move into place. ---
82
150
  chmod +x "${tmp}/kj"
83
151
  mkdir -p "$INSTALL_DIR"
84
152
  mv -f "${tmp}/kj" "${INSTALL_DIR}/kj"
85
-
86
- # On macOS the binary is ad-hoc signed, not notarized with a paid Apple
87
- # certificate, so Gatekeeper can quarantine it. Clear the flag on the copy
88
- # we just checksummed ourselves (no-op on Linux / if the flag is absent).
89
153
  if [ "$os" = "darwin" ] && command -v xattr >/dev/null 2>&1; then
90
154
  xattr -d com.apple.quarantine "${INSTALL_DIR}/kj" 2>/dev/null || true
91
155
  fi
92
-
93
156
  installed="$("${INSTALL_DIR}/kj" --version 2>/dev/null || echo '?')"
94
- echo "kj-install: installed kj ${installed} to ${INSTALL_DIR}/kj"
95
-
96
- # --- Tell the user how to reach it if the dir is not on PATH. ---
97
- case ":${PATH}:" in
98
- *":${INSTALL_DIR}:"*) echo "kj-install: '${INSTALL_DIR}' is on your PATH — run 'kj --help' to get started." ;;
99
- *)
100
- echo "kj-install: '${INSTALL_DIR}' is not on your PATH. Add it with:"
101
- echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
102
- echo " (add that line to ~/.bashrc, ~/.zshrc or ~/.profile to make it permanent)"
103
- ;;
104
- esac
105
-
106
- # --- Prerequisites: the binary bundles no toolchain. kj orchestrates ---
107
- # --- external tools, so name the hard requirements and let `kj doctor` ---
108
- # --- check them precisely for this machine.
109
- echo ""
110
- echo "kj-install: next step — run 'kj doctor' to check prerequisites."
111
- echo " Required: git, plus at least one agent CLI (Claude Code, Codex or Gemini)."
112
- echo " Optional: Docker (local models, SonarQube) and Node/npm (helper tools: Squeezr, qmd)."
157
+ echo "kj-install: installed standalone kj ${installed} to ${INSTALL_DIR}/kj"
158
+ path_hint "$INSTALL_DIR"
159
+ echo "kj-install: next — run 'kj doctor'. For the full product later: re-run this installer without --standalone."
@@ -102,8 +102,14 @@ export function createKarajanMcpCheck() {
102
102
  return {
103
103
  ok: result.ok,
104
104
  severity: result.ok ? "info" : "warn",
105
- detail: result.ok ? result.detail : `karajan-mcp did not respond: ${result.detail}`,
106
- fix: result.ok ? undefined : "Re-run karajan-mcp via `node bin/karajan-mcp.js`",
105
+ // KJC-BUG-0118: the MCP is OPTIONAL in the v4 environment (the host
106
+ // agent drives kj via CLI) — a dead MCP must read as a degraded
107
+ // optional, never as a broken install. The standalone (curl) binary
108
+ // cannot run it at all (no bundled native modules).
109
+ detail: result.ok
110
+ ? result.detail
111
+ : `karajan-mcp did not respond: ${result.detail} — OPTIONAL in v4: your agent runs kj directly; only shell-less hosts need MCP`,
112
+ fix: result.ok ? undefined : "Only if you need MCP: install via npm (`npm i -g karajan-code`) — the standalone binary does not bundle it",
107
113
  extra: { binPath },
108
114
  };
109
115
  },
@@ -51,7 +51,10 @@ export function createNativeBuildCheck({ loadDb = defaultLoadDb, isPnpm = isPnpm
51
51
  return {
52
52
  ok: false,
53
53
  severity: "warn",
54
- detail: `better-sqlite3 failed to load: ${firstLine}`,
54
+ // KJC-BUG-0118: the standalone (curl) binary ships without native
55
+ // modules by design — name the consequence (RAG/board/MCP need the
56
+ // npm install) instead of implying the install is broken.
57
+ detail: `better-sqlite3 failed to load: ${firstLine} — DB-backed features (RAG, board, MCP) unavailable; the standalone binary does not bundle native modules`,
55
58
  fix: "Reinstall to rebuild native modules: `npm i -g karajan-code`",
56
59
  };
57
60
  }
@@ -10,6 +10,7 @@ import { existsSync, readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
 
12
12
  import { loadConfig } from "../config.js";
13
+ import { resolveTestOnPush } from "../harden/test-on-push.js";
13
14
  import { compareHarden, formatAdvisoryReport } from "../harden/advisory.js";
14
15
  import { interactiveHarden } from "../harden/interactive.js";
15
16
  import { createWizard, isTTY } from "../utils/wizard.js";
@@ -121,6 +122,14 @@ export async function hardenCommand({
121
122
 
122
123
  const roots = detectStackRoots(projectDir, { only: onlyDirs, exclude: excludeDirs });
123
124
  const cmds = await resolveCmds(projectDir, roots[0]?.language ?? null);
125
+ // KJC-TSK-0647: CI already running the suite per PR ⇒ pre-push drops its
126
+ // test command (false-red source under local parallelism); opt back in
127
+ // via package.json kj.harden.test_on_push.
128
+ const top = resolveTestOnPush({ projectDir, repoCfg, testCmd: cmds.test });
129
+ if (top.reason) {
130
+ logger.info?.(`kj harden: ${top.reason}`);
131
+ cmds.test = undefined;
132
+ }
124
133
  // KJC-TSK-0648: branch-first guard — the base branch only moves via PR.
125
134
  // Resolved from the project's kj config (default "main"); best-effort so
126
135
  // harden keeps working on repos that never ran kj init.
@@ -28,7 +28,17 @@ const execFileAsync = promisify(execFile);
28
28
 
29
29
  // git and the agent CLI lead the list: both are `kj doctor` *required* tools,
30
30
  // so a blank machine wants them before the optional audit tools.
31
- const ALL_TOOLS = ["git", "agent-cli", "semgrep", "osv-scanner", "lighthouse", "docker", "sonar"];
31
+ // KJC-TSK-0657: rtk/squeezr/qmd (token+context optimizers, previously
32
+ // init-only) complete the list — one pass leaves the machine 100%
33
+ // operational, per the quality-default-on product rule.
34
+ const ALL_TOOLS = ["git", "agent-cli", "semgrep", "osv-scanner", "lighthouse", "docker", "sonar", "rtk", "squeezr", "qmd"];
35
+
36
+ // Optimizer tools reuse the init installers (same {ok, version, error} contract).
37
+ const OPTIMIZER_INSTALLERS = {
38
+ rtk: () => import("../utils/rtk-install.js").then((m) => m.installRtk),
39
+ squeezr: () => import("../utils/squeezr-install.js").then((m) => m.installSqueezr),
40
+ qmd: () => import("../utils/qmd-install.js").then((m) => m.installQmd),
41
+ };
32
42
 
33
43
  // The default pipeline is coder=claude, reviewer=codex, so `agent-cli` installs
34
44
  // exactly those two. gemini stays out of the default: it is a supported
@@ -335,6 +345,22 @@ export async function installToolsCommand(opts = {}) {
335
345
  continue;
336
346
  }
337
347
 
348
+ if (OPTIMIZER_INSTALLERS[tool]) {
349
+ if (dryRun) {
350
+ results.push({ tool, action: "planned", command: `kj install-tools --only ${tool}` });
351
+ logger.info?.(`▸ ${tool}: would install (init installer)`);
352
+ continue;
353
+ }
354
+ const proceed = yes || await promptYesNo(`Install ${tool}?`);
355
+ if (!proceed) { results.push({ tool, action: "declined" }); continue; }
356
+ const install = await OPTIMIZER_INSTALLERS[tool]();
357
+ const r = await install(logger);
358
+ results.push(r.ok
359
+ ? { tool, action: "installed", version: r.version }
360
+ : { tool, action: "failed", error: r.error });
361
+ continue;
362
+ }
363
+
338
364
  if (tool === "git") {
339
365
  const result = await handleGit({ available, dryRun, yes, logger });
340
366
  results.push(result);
@@ -9,6 +9,7 @@ import { runCommand } from "../utils/process.js";
9
9
  import { checkVerdict } from "../review/verdict-store.js";
10
10
  import { runOneShotReview } from "../review/one-shot-review.js";
11
11
  import { runSolomonArbitration } from "../review/solomon-arbitration.js";
12
+ import { ensureGateTrackable } from "../review/gate-gitignore.js";
12
13
 
13
14
  // Raw git always — never a wrapped/compressing runner (KJC-BUG-0115).
14
15
  async function rawDiff(range) {
@@ -64,6 +65,12 @@ export async function reviewGateCommand({ config, logger = null, flags = {} }) {
64
65
  await fs.mkdir(path.dirname(marker), { recursive: true });
65
66
  await fs.writeFile(marker, "# Cross-AI review gate enabled (ENV-C1). Commit this file so the whole team inherits the gate.\n");
66
67
  console.log("✓ review gate enabled — commits now require an approved cross-AI verdict (kj review --staged)");
68
+ // KJC-TSK-0646: a `.karajan/` dir-exclude would silently keep the
69
+ // contract out of git — rewrite it so the team actually inherits it.
70
+ const gi = await ensureGateTrackable(projectDir);
71
+ if (gi.changed) {
72
+ console.log("✓ .gitignore adjusted: the gate contract (.karajan/review-gate, hooks) is now trackable — commit it");
73
+ }
67
74
  const hookProbe = await runCommand("git", ["config", "core.hooksPath"]);
68
75
  if (!hookProbe.stdout?.trim()) {
69
76
  console.log("⚠ no core.hooksPath configured — run `kj harden` so the pre-commit hook enforces the gate");
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Test-on-push policy (KJC-TSK-0647) — the full suite on every push is
3
+ * redundant when CI already runs it per PR, and a source of false reds
4
+ * (suites unstable under local parallelism). Field-reproduced.
5
+ *
6
+ * Policy: a CI workflow that runs tests ⇒ pre-push drops its test command
7
+ * (identity guard and chaining stay). Repos can force it back with
8
+ * `"kj": { "harden": { "test_on_push": true } }` in package.json. Repos
9
+ * without CI keep tests on push — there it IS the last safety net.
10
+ */
11
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ const TEST_STEP = /\b(npm (run )?test|vitest|jest|pytest|go test|phpunit|cargo test)\b/;
15
+
16
+ export function ciRunsTests(projectDir) {
17
+ const dir = join(projectDir, ".github", "workflows");
18
+ if (!existsSync(dir)) return false;
19
+ try {
20
+ return readdirSync(dir)
21
+ .filter((f) => /\.ya?ml$/.test(f))
22
+ .some((f) => TEST_STEP.test(readFileSync(join(dir, f), "utf8")));
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ export function resolveTestOnPush({ projectDir, repoCfg = {}, testCmd = null }) {
29
+ if (!testCmd) return { testCmd: null, reason: null };
30
+ if (repoCfg.test_on_push === true) return { testCmd, reason: null };
31
+ if (ciRunsTests(projectDir)) {
32
+ return {
33
+ testCmd: null,
34
+ reason: "CI already runs the suite per PR — pre-push keeps only the identity guard (opt back in with kj.harden.test_on_push: true in package.json)",
35
+ };
36
+ }
37
+ return { testCmd, reason: null };
38
+ }
Binary file