karajan-code 4.0.0 → 4.0.1
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
package/src/commands/harden.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
+
import { loadConfig } from "../config.js";
|
|
12
13
|
import { compareHarden, formatAdvisoryReport } from "../harden/advisory.js";
|
|
13
14
|
import { interactiveHarden } from "../harden/interactive.js";
|
|
14
15
|
import { createWizard, isTTY } from "../utils/wizard.js";
|
|
@@ -120,9 +121,16 @@ export async function hardenCommand({
|
|
|
120
121
|
|
|
121
122
|
const roots = detectStackRoots(projectDir, { only: onlyDirs, exclude: excludeDirs });
|
|
122
123
|
const cmds = await resolveCmds(projectDir, roots[0]?.language ?? null);
|
|
124
|
+
// KJC-TSK-0648: branch-first guard — the base branch only moves via PR.
|
|
125
|
+
// Resolved from the project's kj config (default "main"); best-effort so
|
|
126
|
+
// harden keeps working on repos that never ran kj init.
|
|
127
|
+
let baseBranch = "main";
|
|
128
|
+
try {
|
|
129
|
+
baseBranch = (await loadConfig(projectDir))?.base_branch || "main";
|
|
130
|
+
} catch { /* no kj config — default stands */ }
|
|
123
131
|
let result;
|
|
124
132
|
try {
|
|
125
|
-
result = await installHooks({ projectDir, profile, cmds, dryRun });
|
|
133
|
+
result = await installHooks({ projectDir, profile, cmds, dryRun, baseBranch });
|
|
126
134
|
} catch (err) {
|
|
127
135
|
if (json) logger.info?.(JSON.stringify({ ok: false, error: err.message }));
|
|
128
136
|
else logger.error?.(`kj harden: ${err.message}`);
|
|
@@ -41,7 +41,9 @@ You are the orchestrator; Karajan governs the method. Follow this on every task:
|
|
|
41
41
|
5. **Security checklist** (you absorb the security role): validate all inputs,
|
|
42
42
|
never commit secrets or keys, no deprecated APIs, parameterized queries,
|
|
43
43
|
sanitize anything user-controlled before it reaches HTML/shell/SQL.
|
|
44
|
-
6. **
|
|
44
|
+
6. **Branch first**: never commit on the base branch — create a branch per
|
|
45
|
+
task/bug and every change reaches the base only through a PR.
|
|
46
|
+
7. **Ship small**: Conventional Commits, atomic PRs (~150 net lines), each PR
|
|
45
47
|
compiles and passes tests on its own.
|
|
46
48
|
|
|
47
49
|
Useful commands: \`kj rag query\` · \`kj review --staged\` · \`kj review --check\` ·
|
|
@@ -33,11 +33,25 @@ export async function installHooks({
|
|
|
33
33
|
profile = "standard",
|
|
34
34
|
cmds = {},
|
|
35
35
|
dryRun = false,
|
|
36
|
+
baseBranch = null,
|
|
36
37
|
} = {}) {
|
|
37
38
|
if (!isGitRepo(projectDir)) throw new Error(`Not a git repository: ${projectDir}`);
|
|
38
39
|
const hooks = PROFILE_HOOKS[profile];
|
|
39
40
|
if (!hooks) throw new Error(`Unknown harden profile: ${profile}`);
|
|
40
41
|
|
|
42
|
+
// KJC-TSK-0645: setting a repo-local hooksPath ECLIPSES the machine's
|
|
43
|
+
// global hooks dir — personal guards (AI-attribution commit-msg, protected
|
|
44
|
+
// branch pre-push) would silently stop applying here. Detect the previous
|
|
45
|
+
// global dir and have every generated hook chain to it. `~` is emitted as
|
|
46
|
+
// `$HOME` so the committed hook stays portable across machines (the -x
|
|
47
|
+
// guard silences it where the dir doesn't exist).
|
|
48
|
+
let globalHooksDir = null;
|
|
49
|
+
const globalCfg = await runCommand("git", ["config", "--global", "core.hooksPath"], { cwd: projectDir });
|
|
50
|
+
const rawGlobal = globalCfg.exitCode === 0 ? globalCfg.stdout.trim() : "";
|
|
51
|
+
if (rawGlobal && rawGlobal !== HOOKS_DIR) {
|
|
52
|
+
globalHooksDir = rawGlobal.startsWith("~") ? `$HOME${rawGlobal.slice(1)}` : rawGlobal;
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
const absHooksDir = join(projectDir, HOOKS_DIR);
|
|
42
56
|
const results = [];
|
|
43
57
|
for (const hook of hooks) {
|
|
@@ -47,7 +61,7 @@ export async function installHooks({
|
|
|
47
61
|
source,
|
|
48
62
|
blockId: `hook:${hook}`,
|
|
49
63
|
version: BLOCK_VERSION,
|
|
50
|
-
body: hookBody(hook, cmds),
|
|
64
|
+
body: hookBody(hook, cmds, { globalHooksDir, baseBranch }),
|
|
51
65
|
style: "hash",
|
|
52
66
|
});
|
|
53
67
|
results.push({ hook, target, action });
|
|
@@ -21,10 +21,42 @@ export const PROFILE_HOOKS = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/** Build the managed body for a single hook. */
|
|
24
|
-
|
|
24
|
+
// KJC-TSK-0645: `core.hooksPath .karajan/hooks` eclipses the user's global
|
|
25
|
+
// hooks dir, silently disabling personal guards. When a previous global dir
|
|
26
|
+
// is known, every generated hook ends by chaining its namesake there —
|
|
27
|
+
// guarded, so machines without it are unaffected.
|
|
28
|
+
function chainToGlobal(hook, globalHooksDir) {
|
|
29
|
+
if (!globalHooksDir) return [];
|
|
30
|
+
return [
|
|
31
|
+
"# Chain the machine's previous global hook (kj harden keeps it active).",
|
|
32
|
+
`if [ -x "${globalHooksDir}/${hook}" ]; then`,
|
|
33
|
+
` "${globalHooksDir}/${hook}" "$@"; rc=$?`,
|
|
34
|
+
' [ "$rc" -eq 0 ] || exit "$rc"',
|
|
35
|
+
"fi",
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// KJC-TSK-0648: branch-first, enforced. The first external session of the
|
|
40
|
+
// v4 environment committed on local main following literal instructions —
|
|
41
|
+
// the playbook now orders "branch first" and this guard backs it up.
|
|
42
|
+
function baseBranchGuard(baseBranch) {
|
|
43
|
+
if (!baseBranch) return [];
|
|
44
|
+
return [
|
|
45
|
+
"# Branch-first guard — the base branch only moves via PR.",
|
|
46
|
+
'if [ "$KJ_ALLOW_BASE_COMMIT" != "1" ]; then',
|
|
47
|
+
' current_branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")',
|
|
48
|
+
` if [ "$current_branch" = "${baseBranch}" ]; then`,
|
|
49
|
+
` echo 'kj harden: direct commits on ${baseBranch} are not allowed — create a branch and open a PR (KJ_ALLOW_BASE_COMMIT=1 to override)'; exit 1`,
|
|
50
|
+
" fi",
|
|
51
|
+
"fi",
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function hookBody(hook, cmds = {}, { globalHooksDir = null, baseBranch = null } = {}) {
|
|
56
|
+
const chain = chainToGlobal(hook, globalHooksDir);
|
|
25
57
|
switch (hook) {
|
|
26
58
|
case "pre-commit": {
|
|
27
|
-
const lines = ["# Lint + format the working tree with the project's native tools."];
|
|
59
|
+
const lines = [...baseBranchGuard(baseBranch), "# Lint + format the working tree with the project's native tools."];
|
|
28
60
|
if (cmds.lint) lines.push(`${cmds.lint} || { echo 'kj harden: lint failed'; exit 1; }`);
|
|
29
61
|
if (cmds.format) lines.push(`${cmds.format} || { echo 'kj harden: format check failed'; exit 1; }`);
|
|
30
62
|
if (!cmds.lint && !cmds.format) lines.push("# (no lint/format command detected for this stack)");
|
|
@@ -38,7 +70,7 @@ export function hookBody(hook, cmds = {}) {
|
|
|
38
70
|
" kj review --check || { echo 'kj: no approved cross-AI verdict for the staged diff — run `kj review --staged`'; exit 1; }",
|
|
39
71
|
"fi"
|
|
40
72
|
);
|
|
41
|
-
return lines.join("\n");
|
|
73
|
+
return [...lines, ...chain].join("\n");
|
|
42
74
|
}
|
|
43
75
|
case "commit-msg":
|
|
44
76
|
// Pure POSIX — Conventional Commits header, length cap, AI-attribution.
|
|
@@ -52,6 +84,7 @@ export function hookBody(hook, cmds = {}) {
|
|
|
52
84
|
`if grep -qiE '${AI_ATTRIBUTION}' "$msg_file"; then`,
|
|
53
85
|
" echo 'kj harden: AI attribution is not allowed in commit messages'; exit 1",
|
|
54
86
|
"fi",
|
|
87
|
+
...chain,
|
|
55
88
|
].join("\n");
|
|
56
89
|
case "pre-push": {
|
|
57
90
|
const lines = [
|
|
@@ -64,7 +97,7 @@ export function hookBody(hook, cmds = {}) {
|
|
|
64
97
|
];
|
|
65
98
|
if (cmds.test) lines.push(`${cmds.test} || { echo 'kj harden: tests failed'; exit 1; }`);
|
|
66
99
|
else lines.push("# (no test command detected for this stack)");
|
|
67
|
-
return lines.join("\n");
|
|
100
|
+
return [...lines, ...chain].join("\n");
|
|
68
101
|
}
|
|
69
102
|
case "post-merge":
|
|
70
103
|
return [
|
|
@@ -72,6 +105,7 @@ export function hookBody(hook, cmds = {}) {
|
|
|
72
105
|
"if command -v kj >/dev/null 2>&1; then",
|
|
73
106
|
" kj rag index --since auto >/dev/null 2>&1 || true",
|
|
74
107
|
"fi",
|
|
108
|
+
...chain,
|
|
75
109
|
].join("\n");
|
|
76
110
|
default:
|
|
77
111
|
throw new Error(`Unknown hook: ${hook}`);
|