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 +1 -1
- package/scripts/install-binary.sh +119 -72
- package/src/checks/mcp-health.js +8 -2
- package/src/checks/native-build.js +4 -1
- package/src/commands/harden.js +9 -0
- package/src/commands/install-tools.js +27 -1
- package/src/commands/review-gate.js +7 -0
- package/src/harden/test-on-push.js +38 -0
- package/src/review/gate-gitignore.js +0 -0
package/package.json
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Karajan Code —
|
|
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
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
# ---
|
|
70
|
-
|
|
71
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
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."
|
package/src/checks/mcp-health.js
CHANGED
|
@@ -102,8 +102,14 @@ export function createKarajanMcpCheck() {
|
|
|
102
102
|
return {
|
|
103
103
|
ok: result.ok,
|
|
104
104
|
severity: result.ok ? "info" : "warn",
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/harden.js
CHANGED
|
@@ -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
|
-
|
|
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
|