karajan-code 5.0.1 → 5.0.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/README.md CHANGED
@@ -83,6 +83,10 @@ git config core.hooksPath .karajan/hooks
83
83
 
84
84
  Requires git and at least one AI agent CLI — two enables cross-AI review; three enables arbitration. The npm route is `npm install -g @karajan-family/code` (published as `karajan-code` before joining the scope; the legacy name still installs the same versions). All install routes (npm, binaries, brew, Python wrapper) in the [install docs](https://karajancode.com/docs/v4/install/).
85
85
 
86
+ ### Verify what you installed
87
+
88
+ Since 5.0.0 every release is published from GitHub Actions with signed provenance (SLSA), tied to the tagged commit that built it. Check the npm package with `npm audit signatures` in the folder where kj is installed (kj must be listed with verified attestations), and a standalone binary with `gh attestation verify kj-linux-x64 --repo manufosela/karajan-code` (GitHub CLI 2.49+). Details in the [install docs](https://karajancode.com/docs/v4/install/#verify-what-you-installed).
89
+
86
90
  ## The daily loop
87
91
 
88
92
  1. You describe what you want. Your agent creates the card (`kj hu add`), queries the RAG, writes the failing test, then the code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "5.0.1",
3
+ "version": "5.0.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",
@@ -7,7 +7,7 @@
7
7
  * - Idempotent: safe to run many times.
8
8
  * - Never fails hard (exits 0 even on error) so `npm install` is not blocked.
9
9
  */
10
- import { existsSync } from "node:fs";
10
+ import { existsSync, statSync } from "node:fs";
11
11
  import fs from "node:fs/promises";
12
12
  import os from "node:os";
13
13
  import path from "node:path";
@@ -140,7 +140,21 @@ async function setupCodexMcp(kjHome) {
140
140
  await fs.writeFile(configPath, updated, "utf8");
141
141
  }
142
142
 
143
+ // KJC-BUG-0474 (#3128): a git worktree (a kj lane) has a `.git` FILE. `npm ci` there runs this
144
+ // script, and registering would point every session of the machine at the lane's server.
145
+ function insideLinkedWorktree(dir = ROOT_DIR) {
146
+ try {
147
+ return statSync(path.join(dir, ".git")).isFile();
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
143
153
  async function main() {
154
+ if (insideLinkedWorktree()) {
155
+ process.stderr.write("karajan-mcp: not registered from a git worktree (a lane); the global MCP keeps pointing at the installed kj\n");
156
+ return;
157
+ }
144
158
  const claudeConfig = await readClaudeConfig();
145
159
  const kjHome = await resolveKjHome(claudeConfig.mcpServers?.["karajan-mcp"]);
146
160
 
@@ -8,7 +8,7 @@
8
8
  * ~/.karajan/kj.config.yml (strategy: manual — structural fixes needed)
9
9
  */
10
10
 
11
- import { existsSync } from "node:fs";
11
+ import { existsSync, statSync } from "node:fs";
12
12
  import fs from "node:fs/promises";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
@@ -146,6 +146,19 @@ export function karajanMcpServerPath(config) {
146
146
  return args.find((a) => typeof a === "string" && /[\\/]mcp[\\/]server\.js$/.test(a)) ?? null;
147
147
  }
148
148
 
149
+ /**
150
+ * KJC-BUG-0474 (#3128): whether a file lives in a linked git worktree (a kj lane). The
151
+ * nearest `.git` up the tree decides: a worktree's is a file, a normal checkout's a folder.
152
+ */
153
+ export function inLinkedWorktree(file) {
154
+ for (let dir = path.dirname(path.resolve(file)); ; dir = path.dirname(dir)) {
155
+ try {
156
+ return statSync(path.join(dir, ".git")).isFile();
157
+ } catch { /* no .git here: keep climbing */ }
158
+ if (path.dirname(dir) === dir) return false;
159
+ }
160
+ }
161
+
149
162
  /**
150
163
  * Repair a stale karajan-mcp registration in place: re-point it to this
151
164
  * install's server when that server exists, or drop the dead entry when this
@@ -191,12 +204,19 @@ function createStaleMcpPathCheck() {
191
204
  }
192
205
  const registered = karajanMcpServerPath(config);
193
206
  if (!registered) return { ok: true, severity: "info", detail: "karajan-mcp not registered (skipped)" };
194
- if (await exists(registered)) return { ok: true, severity: "info", detail: "karajan-mcp path valid" };
207
+ // KJC-BUG-0474 (#3128): a path inside a lane works until the lane goes, and meanwhile runs its code everywhere.
208
+ const inLane = inLinkedWorktree(registered);
209
+ if (!inLane && (await exists(registered))) return { ok: true, severity: "info", detail: "karajan-mcp path valid" };
195
210
  const correct = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "mcp", "server.js");
211
+ if (inLane && inLinkedWorktree(correct)) {
212
+ return { ok: false, severity: "warn", detail: `karajan-mcp runs a lane's server: ${registered}`, fix: "Run kj doctor from the installed kj (not from a lane) to re-point it" };
213
+ }
196
214
  return {
197
215
  ok: false,
198
216
  severity: "warn",
199
- detail: `karajan-mcp points at a path that no longer exists: ${registered} — this is the CONNECTION_CLOSED cause`,
217
+ detail: inLane
218
+ ? `karajan-mcp runs a lane's server (${registered}): every session runs that lane's code, and removing the lane breaks it`
219
+ : `karajan-mcp points at a path that no longer exists: ${registered} — this is the CONNECTION_CLOSED cause`,
200
220
  fix: `Re-point karajan-mcp to ${correct} (or remove the entry) in ~/.claude.json`,
201
221
  extra: { claudeJsonPath, correct },
202
222
  };
@@ -29,6 +29,29 @@ const shellQuote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
29
29
  const git = (projectDir, args) => execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
30
30
  const gitOk = (projectDir, args) => { try { git(projectDir, args); return true; } catch { return false; } };
31
31
 
32
+ /**
33
+ * KJC-BUG-0473 (#3080): the main branch is main, always. With master as the default branch,
34
+ * every gate kj writes for [main] never runs, and nothing says so. Read from origin/HEAD when
35
+ * the clone knows it; with no origin, from a local master that has no main beside it.
36
+ * @returns {{branch: string, detail: string, fix: string} | null}
37
+ */
38
+ export function defaultBranchProblem(projectDir) {
39
+ let branch = null;
40
+ try {
41
+ branch = git(projectDir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).trim().replace(/^origin\//, "");
42
+ } catch {
43
+ if (gitOk(projectDir, ["rev-parse", "--verify", "-q", "refs/heads/master"])
44
+ && !gitOk(projectDir, ["rev-parse", "--verify", "-q", "refs/heads/main"])) branch = "master";
45
+ }
46
+ if (!branch || branch === "main") return null;
47
+ return {
48
+ branch,
49
+ detail: `la rama por defecto es ${branch}, no main: los gates que kj escribe para main no se lanzan nunca`,
50
+ fix: `renombra ${branch} a main (en GitHub: Settings > Branches > Rename; si no existe, crea main) y actualiza el clon: `
51
+ + `git branch -m ${branch} main && git fetch origin && git branch -u origin/main main && git remote set-head origin -a`,
52
+ };
53
+ }
54
+
32
55
  /** @internal Exported for dynamic import from tests. */
33
56
  export function createRepoStateCheck({ projectDir }) {
34
57
  return {
@@ -100,6 +123,9 @@ export function createRepoStateCheck({ projectDir }) {
100
123
  });
101
124
  }
102
125
 
126
+ const notMain = defaultBranchProblem(dir);
127
+ if (notMain) symptoms.push({ detail: notMain.detail, fix: notMain.fix });
128
+
103
129
  if (symptoms.length === 0) return { ok: true, severity: "info", detail: "el repositorio está en orden" };
104
130
  return {
105
131
  ok: false,
@@ -32,6 +32,7 @@ import { installKarajanCommands } from "./init-atril.js";
32
32
  import { hardenCommand } from "./harden.js";
33
33
  import { contractChanges } from "../environment/contract-commit.js";
34
34
  import { detectProjectStack } from "../utils/stack-detect.js";
35
+ import { defaultBranchProblem } from "../checks/repo-state.js";
35
36
  import { bootstrapSonarToken } from "../sonar/token-bootstrap.js";
36
37
 
37
38
  /**
@@ -775,6 +776,15 @@ export async function resolveConfigScope({ flags, interactive }) {
775
776
  return { configPath: getConfigPath(), scope: "global" };
776
777
  }
777
778
 
779
+ /** KJC-BUG-0473 (#3080): said at the close, so a master repo never ends init looking governed. */
780
+ export function warnIfDefaultBranchNotMain({ projectDir, logger }) {
781
+ const problem = defaultBranchProblem(projectDir);
782
+ if (!problem) return false;
783
+ logger.warn(`⚠ ${problem.detail}`);
784
+ logger.warn(` Fix: ${problem.fix}`);
785
+ return true;
786
+ }
787
+
778
788
  /**
779
789
  * KJC-BUG-0187: the enforcement gates live in git hooks, so a project without
780
790
  * a repo gets no harness at all. Init creates it, exactly like `kj run` does
@@ -1017,6 +1027,7 @@ export async function initCommand({ logger, flags = {} }) {
1017
1027
  logger.info(hardened || skipHarden
1018
1028
  ? "✓ Karajan is set up in this project."
1019
1029
  : "⚠ Karajan is set up EXCEPT the quality harness — without it there are no gates.");
1030
+ warnIfDefaultBranchNotMain({ projectDir: process.cwd(), logger });
1020
1031
  logger.info(" Next: ask your agent for the work; it follows the method kj installed");
1021
1032
  logger.info(" More: kj --help · kj doctor (check your setup)");
1022
1033
  }
@@ -134,9 +134,22 @@ export const holdLane = (top, now = Date.now()) => {
134
134
  return "(no se pudo reclamar " + top + ")";
135
135
  }
136
136
  };
137
+ // KJC-BUG-0475 (#3125): during a rebase HEAD is detached; the branch being rebased is in head-name.
138
+ const rebasingBranch = (top) => {
139
+ for (const dir of ["rebase-merge", "rebase-apply"]) {
140
+ try {
141
+ const file = execSync("git rev-parse --path-format=absolute --git-path " + dir + "/head-name", { cwd: top, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
142
+ const name = readFileSync(file, "utf8").trim();
143
+ if (name.startsWith("refs/heads/")) return name.slice("refs/heads/".length);
144
+ } catch { /* no rebase of this kind in progress */ }
145
+ }
146
+ return null;
147
+ };
137
148
  export const branchOf = (top = laneTop()) => {
138
- try { return execSync("git rev-parse --abbrev-ref HEAD", { cwd: top, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); }
149
+ let branch;
150
+ try { branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: top, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); }
139
151
  catch { return null; }
152
+ return branch === "HEAD" ? (rebasingBranch(top) ?? branch) : branch;
140
153
  };
141
154
  // ADR 0034 (KJC-TSK-1243): the lane a write lands in is the worktree holding its path when
142
155
  // that worktree is of this repo; any other path is judged in the session's lane. It walks up
@@ -53,7 +53,10 @@ export async function publishPrReview({ projectDir, pr, run = runCommand, sealed
53
53
  await labelKarajanPr({ url: String(pr), cwd: projectDir, run });
54
54
  const reviews = await json(run, "gh", ["api", `repos/{owner}/{repo}/pulls/${pr}/reviews`], projectDir);
55
55
  // KJC-BUG-0456: one a non-member posted (gh switched to another account) does not count in CI.
56
- if ((reviews ?? []).some((r) => TRUSTED.has(r.author_association) && String(r.body ?? "").includes(MARKER(info.headRefOid)))) {
56
+ // KJC-BUG-0477 (#3127): nor does one that misses a commit the PR gained under the same head
57
+ // (a stacked PR retargeted to main); verify-pr adds up every kj review, so publish another.
58
+ const listsAll = (body) => [...rows, ...seals].every((c) => body.includes(`\`${c.oid.slice(0, 7)}\``));
59
+ if ((reviews ?? []).some((r) => TRUSTED.has(r.author_association) && String(r.body ?? "").includes(MARKER(info.headRefOid)) && listsAll(String(r.body)))) {
57
60
  return { published: false, reason: `a kj review for head ${info.headRefOid.slice(0, 7)} is already published` };
58
61
  }
59
62
  const body = [
@@ -23,11 +23,16 @@ const SECURITY_PATTERN = /injection|xss|csrf|secret|credential|password|token le
23
23
  // Structured signal first (`category: "security"` — part of the reviewer's
24
24
  // JSON schema since AB-E); the free-text pattern stays as a fail-closed net
25
25
  // for verdicts recorded before the schema carried categories.
26
- function isSecurityIssue(issue) {
27
- return issue.category === "security"
28
- || SECURITY_PATTERN.test(`${issue.id || ""} ${issue.description || ""}`);
26
+ // KJC-BUG-0476 (#3137): returns why the issue counts as security, or null.
27
+ function securityReason(issue) {
28
+ if (issue.category === "security") return "category security";
29
+ const match = SECURITY_PATTERN.exec(`${issue.id || ""} ${issue.description || ""}`);
30
+ return match ? `matches "${match[0]}"` : null;
29
31
  }
30
32
 
33
+ const describeSecurityIssue = (issue) =>
34
+ `${issue.id || "?"} (${securityReason(issue)}): ${String(issue.description || "").slice(0, 160)}`;
35
+
31
36
  export async function pickThirdParty({ config, hostAgent, reviewerAgent, detectAgents = detectAvailableAgents }) {
32
37
  const excluded = new Set([hostAgent, reviewerAgent]);
33
38
  const configured = config?.roles?.solomon?.provider;
@@ -52,13 +57,14 @@ export async function runSolomonArbitration({
52
57
  }
53
58
 
54
59
  // ABSOLUTE RULE: security findings are never overridable.
55
- const securityIssues = (verdict.issues || []).filter(isSecurityIssue);
60
+ const securityIssues = (verdict.issues || []).filter((issue) => securityReason(issue));
56
61
  if (securityIssues.length > 0) {
62
+ const named = securityIssues.map(describeSecurityIssue).join("; ");
57
63
  const record = await saveVerdict(projectDir, diff, {
58
64
  ...verdict,
59
- arbitration: { position, ruling: "reject", reasoning: "security issues from the reviewer are never overridable", solomon: null },
65
+ arbitration: { position, ruling: "reject", reasoning: `security issues from the reviewer are never overridable: ${named}`, solomon: null },
60
66
  });
61
- return { ruling: "reject", reasoning: "security issues are never overridable — fix them", solomon: null, record };
67
+ return { ruling: "reject", reasoning: `security issues are never overridable, fix them: ${named}`, solomon: null, record };
62
68
  }
63
69
 
64
70
  const solomon = await pickThirdParty({ config, hostAgent, reviewerAgent: verdict.reviewer, detectAgents });