@karajan-family/code 5.0.1 → 5.0.3
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 +4 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +15 -1
- package/src/checks/config-files.js +23 -3
- package/src/checks/repo-state.js +28 -1
- package/src/commands/init.js +11 -0
- package/src/harden/sentinel/sentinel-lib.mjs +14 -1
- package/src/review/publish-pr.js +4 -1
- package/src/review/solomon-arbitration.js +12 -6
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
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
};
|
package/src/checks/repo-state.js
CHANGED
|
@@ -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,10 +123,14 @@ export function createRepoStateCheck({ projectDir }) {
|
|
|
100
123
|
});
|
|
101
124
|
}
|
|
102
125
|
|
|
126
|
+
const notMain = defaultBranchProblem(dir);
|
|
127
|
+
// KJC-BUG-0481 (#3080, point 3): a failure until it is fixed, not a warning, so doctor exits 1.
|
|
128
|
+
if (notMain) symptoms.push({ detail: notMain.detail, fix: notMain.fix, fails: true });
|
|
129
|
+
|
|
103
130
|
if (symptoms.length === 0) return { ok: true, severity: "info", detail: "el repositorio está en orden" };
|
|
104
131
|
return {
|
|
105
132
|
ok: false,
|
|
106
|
-
severity: "warn",
|
|
133
|
+
severity: symptoms.some((s) => s.fails) ? "fail" : "warn",
|
|
107
134
|
detail: symptoms.map((s) => s.detail).join(" · "),
|
|
108
135
|
fix: symptoms.map((s) => s.fix).join(" · "),
|
|
109
136
|
};
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/review/publish-pr.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
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:
|
|
65
|
+
arbitration: { position, ruling: "reject", reasoning: `security issues from the reviewer are never overridable: ${named}`, solomon: null },
|
|
60
66
|
});
|
|
61
|
-
return { ruling: "reject", reasoning:
|
|
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 });
|