karajan-code 4.40.0 → 4.41.0
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/src/commands/bootstrap.js +12 -41
- package/src/commands/env.js +8 -0
- package/src/commands/rules-approve.js +11 -7
- package/src/commands/rules-compile.js +2 -1
- package/src/commands/rules-decide.js +2 -1
- package/src/commands/rules-review.js +15 -2
- package/src/environment/contract-commit.js +79 -0
- package/src/rules/approval-view.js +6 -1
- package/src/rules/compiled.js +4 -1
package/package.json
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
* first and a git that cannot run is the end of the line, not a warning.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { execFileSync } from "node:child_process";
|
|
19
18
|
import { existsSync } from "node:fs";
|
|
20
19
|
import { join } from "node:path";
|
|
21
20
|
import { ensureGitRepo } from "./init.js";
|
|
22
21
|
import { initCommand } from "./init.js";
|
|
23
22
|
import { envInstallCommand } from "./env.js";
|
|
23
|
+
import { commitContract, contractChanges } from "../environment/contract-commit.js";
|
|
24
24
|
import { runStartScript, START_SCRIPT_CONTRACT } from "../start/project-script.js";
|
|
25
25
|
|
|
26
26
|
const STEP_LABEL = {
|
|
@@ -31,25 +31,6 @@ const STEP_LABEL = {
|
|
|
31
31
|
start: "arranque del proyecto",
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
/** What kj generates and the whole team must inherit by cloning. */
|
|
35
|
-
const CONTRACT_PATHS = [
|
|
36
|
-
".gitignore",
|
|
37
|
-
".karajan/hooks",
|
|
38
|
-
".karajan/review-gate",
|
|
39
|
-
".karajan/adrs",
|
|
40
|
-
".karajan/policy.yml",
|
|
41
|
-
".claude",
|
|
42
|
-
"CLAUDE.md",
|
|
43
|
-
"AGENTS.md",
|
|
44
|
-
"GEMINI.md",
|
|
45
|
-
// KJC-TSK-0879: in a Rulesync repo kj's rules live in .rulesync/rules/karajan.md.
|
|
46
|
-
".rulesync",
|
|
47
|
-
];
|
|
48
|
-
const CONTRACT_MESSAGE = "chore(bootstrap): el contrato del método, para que quien clone lo herede";
|
|
49
|
-
|
|
50
|
-
const hasCommits = (projectDir, git) => {
|
|
51
|
-
try { git(["rev-parse", "--verify", "HEAD"]); return true; } catch { return false; }
|
|
52
|
-
};
|
|
53
34
|
|
|
54
35
|
/**
|
|
55
36
|
* @returns {Promise<{ok: boolean, pending: string|null, steps: Array<{name: string, status: "done"|"already"|"pending"}>}>}
|
|
@@ -75,6 +56,8 @@ export async function bootstrapCommand({ config = {}, logger = console, flags =
|
|
|
75
56
|
return stop("git", "git no está disponible y las garantías de Karajan viven en sus hooks");
|
|
76
57
|
}
|
|
77
58
|
say("git", hadRepo ? "already" : "done", hadRepo ? "ya existía" : "creado, sin commit todavía");
|
|
59
|
+
// What is dirty NOW is the person's: kj commits only what it generates below.
|
|
60
|
+
const before = contractChanges(projectDir);
|
|
78
61
|
|
|
79
62
|
// 2. The project's own configuration.
|
|
80
63
|
const hasConfig = existsSync(join(projectDir, ".karajan", "kj.config.yml"));
|
|
@@ -98,27 +81,15 @@ export async function bootstrapCommand({ config = {}, logger = console, flags =
|
|
|
98
81
|
// 4. The contract commit. project-new.md asked the USER for it, and it was
|
|
99
82
|
// the commit their own freshly installed gates rejected: the review gate
|
|
100
83
|
// (KJC-BUG-0165) and the branch guard (KJC-BUG-0186) both exempt it now,
|
|
101
|
-
// so kj
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// act with its own four layers (ADR 0009).
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
else
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
else {
|
|
111
|
-
try {
|
|
112
|
-
git(["add", "--", ...present]);
|
|
113
|
-
git(["commit", "-m", CONTRACT_MESSAGE]);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
// Nunca explotar aquí: lo más probable es que falte la identidad del
|
|
116
|
-
// clon, y ese cauce ya lo pide el paso anterior (KJC-BUG-0188).
|
|
117
|
-
return stop("contract", `git no pudo commitear el contrato: ${String(err.message).split("\n")[0]}`);
|
|
118
|
-
}
|
|
119
|
-
say("contract", "done", `${present.length} ruta(s) del contrato`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
84
|
+
// so kj makes it itself (src/environment/contract-commit.js). Only what
|
|
85
|
+
// kj generated, never the person's own code, never on the base branch
|
|
86
|
+
// once there is history (KJC-BUG-0273). This is NOT the supervisor seal,
|
|
87
|
+
// which stays a human act with its own four layers (ADR 0009).
|
|
88
|
+
const contract = commitContract({ projectDir, before, baseBranch: config.base_branch || "main" });
|
|
89
|
+
if (contract.committed) say("contract", "done", `${contract.files.length} ruta(s) del contrato`);
|
|
90
|
+
else if (contract.reason.startsWith("nothing")) say("contract", "already", "no hay contrato que commitear");
|
|
91
|
+
// Lo más probable: la identidad del clon (KJC-BUG-0188) o la rama base.
|
|
92
|
+
else return stop("contract", contract.reason);
|
|
122
93
|
|
|
123
94
|
// 5. Does the project actually run? kj verified the method; nobody verified
|
|
124
95
|
// the application (BOOT-C, KJC-TSK-0862). It REPORTS, never blocks: a
|
package/src/commands/env.js
CHANGED
|
@@ -21,6 +21,7 @@ import { reviewGateCommand } from "./review-gate.js";
|
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { openProjectStore, projectDbPath } from "../rag/project-store.js";
|
|
23
23
|
import { maybeRulesyncGenerate } from "../utils/rulesync.js";
|
|
24
|
+
import { commitContract, contractChanges } from "../environment/contract-commit.js";
|
|
24
25
|
|
|
25
26
|
function hasRagIndex(config, projectDir) {
|
|
26
27
|
// KJC-BUG-0128: probing must never CREATE the store — openVecStore runs
|
|
@@ -52,6 +53,8 @@ export function briefCommand({ config = null, flags = {}, role = null }) {
|
|
|
52
53
|
|
|
53
54
|
export async function envInstallCommand({ config = null, logger = null, flags = {} }) {
|
|
54
55
|
const projectDir = config?.projectDir || process.cwd();
|
|
56
|
+
// KJC-BUG-0273: what is dirty NOW is the person's; kj commits only what it generates below.
|
|
57
|
+
const dirtyBefore = contractChanges(projectDir);
|
|
55
58
|
|
|
56
59
|
// KJC-TSK-0709 — the board is chosen BEFORE the playbook renders (its
|
|
57
60
|
// tracking line depends on the backend): interactive installs ask when
|
|
@@ -214,6 +217,11 @@ export async function envInstallCommand({ config = null, logger = null, flags =
|
|
|
214
217
|
wizard?.close();
|
|
215
218
|
} catch { /* privacy onboarding is best-effort */ }
|
|
216
219
|
console.log(" The host agent now follows the method: RAG first, TDD, cross-AI review before commit.");
|
|
220
|
+
// KJC-BUG-0273: the contract is kj's to commit. Left to the agent, its own
|
|
221
|
+
// PR-size rules forbid a 1200-line commit of generated files and it stops.
|
|
222
|
+
const contract = commitContract({ projectDir, before: dirtyBefore, baseBranch: config?.base_branch || "main" });
|
|
223
|
+
if (contract.committed) console.log(`✓ contract committed by kj: ${contract.files.length} generated file(s), nothing for the agent to commit`);
|
|
224
|
+
else if (!contract.reason.startsWith("nothing")) console.log(`⚠ the contract kj generated is NOT committed (${contract.reason}): run \`kj env install\` again once that is fixed`);
|
|
217
225
|
// KJC-BUG-0133 (part 2): a mid-session install lands the playbook in
|
|
218
226
|
// CLAUDE.md, but the running session loaded its context BEFORE — nobody
|
|
219
227
|
// re-reads it. Print the method so it enters THIS conversation now.
|
|
@@ -32,14 +32,18 @@ export async function rulesApprove({ projectDir, file = PROPOSAL_FILE, home, env
|
|
|
32
32
|
const checked = rulesCheck({ projectDir, home, text });
|
|
33
33
|
if (checked.code !== 0) return { code: 1, lines: checked.lines };
|
|
34
34
|
// KJC-TSK-0963: whoever wrote the proposal does not call it good. A different
|
|
35
|
-
// AI must have
|
|
36
|
-
const
|
|
37
|
-
if (!
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
// AI must have read these exact bytes; a touched proposal is reviewed again.
|
|
36
|
+
const { ok, verdict } = await checkVerdict(projectDir, text);
|
|
37
|
+
if (!verdict) return { code: 1, lines: ["✗ this proposal has no cross-AI review of its exact content (none recorded, or it changed since): run `kj rules review`"] };
|
|
38
|
+
if (ok) {
|
|
39
|
+
log(`Reviewed by ${verdict.reviewer}, a different AI from the one that wrote it: ${verdict.summary || "approved"}`);
|
|
40
|
+
} else {
|
|
41
|
+
// KJC-TSK-0974 (ADR 0017): a rejection does not decide, the human does. What
|
|
42
|
+
// the defense asks is that nothing weak is approved UNSEEN: objections first.
|
|
43
|
+
log(`REJECTED by ${verdict.reviewer}, a different AI from the one that wrote it: ${verdict.summary || "see its objections"}`);
|
|
44
|
+
log("Its objections, before anything else. Approving installs the proposal as it is, objections included:");
|
|
45
|
+
for (const issue of verdict.issues ?? []) log(` - ${issue.description ?? issue.message ?? JSON.stringify(issue)}`);
|
|
41
46
|
}
|
|
42
|
-
log(`Reviewed by ${reviewed.verdict.reviewer}, a different AI from the one that wrote it: ${reviewed.verdict.summary || "approved"}`);
|
|
43
47
|
const { rules, local } = checked;
|
|
44
48
|
// KJC-TSK-0962: read in the order of what can hurt, weakened rules first.
|
|
45
49
|
const view = approvalView(rules, loadRules(projectDir).rules, local, { versioned: RULES_FILE, unversioned: LOCAL_RULES_FILE });
|
|
@@ -42,7 +42,8 @@ const HOW = [
|
|
|
42
42
|
"Choose the kind of each rule:",
|
|
43
43
|
"- deterministic: ONE tool call breaks it and the tool name and its arguments are enough to tell. Name the tools",
|
|
44
44
|
" as you see them (MCP tools included). Deny only what the rule forbids: a gate that denies honest calls gets removed.",
|
|
45
|
-
"- judgment: a tool call breaks it, but telling takes reading what the call says. Give `when.tool`
|
|
45
|
+
"- judgment: a tool call breaks it, but telling takes reading what the call says. Give `when.tool`, and a `reason`",
|
|
46
|
+
" when a condition looks possible and is not (a reviewer will ask): say what it cannot see or what it would deny.",
|
|
46
47
|
"- out-of-scope: it is about how you think or answer, and no tool call breaks it. Give the `reason`.",
|
|
47
48
|
"Do not stretch a rule into a condition it does not state, and do not leave a rule out: every rule gets a kind.",
|
|
48
49
|
];
|
|
@@ -18,7 +18,8 @@ const failed = (line) => ({ code: 1, lines: [`✗ ${line}`] });
|
|
|
18
18
|
function decision({ kind, tools = [], reason = "" }) {
|
|
19
19
|
if (kind === "judgment") {
|
|
20
20
|
if (tools.length === 0) return { error: "a judgment rule names the tool it shows on: --tool <glob> (repeatable)" };
|
|
21
|
-
|
|
21
|
+
// KJC-TSK-0973: why it takes no condition, when there is something to say.
|
|
22
|
+
return { fields: { kind, when: { tool: tools.length === 1 ? tools[0] : tools }, ...(reason.trim() ? { reason: reason.trim() } : {}) } };
|
|
22
23
|
}
|
|
23
24
|
if (kind === "out-of-scope") {
|
|
24
25
|
if (!reason.trim()) return { error: "an out-of-scope rule says why no tool call breaks it: --reason <text>" };
|
|
@@ -21,8 +21,21 @@ const TASK = [
|
|
|
21
21
|
" examples picked so that a weak condition passes, is a defect. So is a condition so wide that it denies honest calls.",
|
|
22
22
|
"- kind judgment or out-of-scope: nothing is blocked on these. If a condition over a tool name and its arguments could",
|
|
23
23
|
" have enforced the text, the rule has been weakened.",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"Any rule may give a `reason` for how it is compiled: weigh it, do not take it on trust.",
|
|
25
|
+
"Know what a condition can see before you ask for one (KJC-TSK-0973): the tool name and the arguments of ONE call, with",
|
|
26
|
+
"the operators equals, in, matches, exists, gt, lt. It cannot read a file a command names (a commit message passed with -F,",
|
|
27
|
+
"a body file), earlier calls, or the state of the repo. A rule whose breach is only visible there cannot be deterministic,",
|
|
28
|
+
"and judgment is its honest kind. A condition that would deny honest calls (a word that is also ordinary text, a method",
|
|
29
|
+
"name, a comment) is a defect too: do not ask for one, and do not ask for more than the text forbids.",
|
|
30
|
+
"A deterministic rule that enforces every part of its text a condition CAN see is not weak for leaving out the part no",
|
|
31
|
+
"condition can see: block it only if the visible part is compiled narrower than it could be.",
|
|
32
|
+
"What blocks is MATERIAL weakness: a form of the forbidden call an agent would plausibly write in ordinary work is let",
|
|
33
|
+
"through, or a call an agent would plausibly make in honest work is denied. A pattern over shell text is never complete,",
|
|
34
|
+
"and these gates exist for a rule forgotten or bent in passing; an agent contorting a command to dodge them is stopped at",
|
|
35
|
+
"the checkpoints it does not reach (git hooks, CI), not here. So an exotic form (a command inside a control structure, a",
|
|
36
|
+
"rare global option, unusual casing) is a non-blocking suggestion, not a blocking issue.",
|
|
37
|
+
"Report as a BLOCKING issue every rule materially weaker than its text, naming its id (R-...) and saying what would",
|
|
38
|
+
"enforce it. Approve when none is.",
|
|
26
39
|
].join("\n");
|
|
27
40
|
|
|
28
41
|
/**
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract commit (KJC-TSK-0857, KJC-BUG-0273). What kj generates for a
|
|
3
|
+
* project (playbook, skills, settings, workflows, gate marker, hooks) the whole
|
|
4
|
+
* team must inherit by cloning, so it goes into git. Nobody wrote it, and an
|
|
5
|
+
* agent's own PR-size rules forbid a 1200-line commit, so kj makes the commit
|
|
6
|
+
* itself: only the files it just generated, never what was already dirty, and
|
|
7
|
+
* never on the base branch once the repo has history. This is NOT the supervisor
|
|
8
|
+
* seal (ADR 0009), which stays a human act.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
/** What kj generates and the whole team must inherit by cloning (prefixes). */
|
|
13
|
+
export const CONTRACT_PATHS = [
|
|
14
|
+
".gitignore",
|
|
15
|
+
".karajan/hooks/",
|
|
16
|
+
".karajan/review-gate",
|
|
17
|
+
".karajan/adrs/",
|
|
18
|
+
".karajan/policy.yml",
|
|
19
|
+
".claude/",
|
|
20
|
+
".github/workflows/kj-",
|
|
21
|
+
"CLAUDE.md",
|
|
22
|
+
"AGENTS.md",
|
|
23
|
+
"GEMINI.md",
|
|
24
|
+
// KJC-TSK-0879: in a Rulesync repo kj's rules live in .rulesync/rules/karajan.md.
|
|
25
|
+
".rulesync/",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const FRESH_MESSAGE = "chore(bootstrap): el contrato del método, para que quien clone lo herede";
|
|
29
|
+
const REGEN_MESSAGE = "chore(kj): el contrato del método, generado por kj";
|
|
30
|
+
|
|
31
|
+
const runner = (projectDir, env) => (args) => execFileSync("git", ["-C", projectDir, ...args], { encoding: "utf8", env, stdio: ["ignore", "pipe", "pipe"] });
|
|
32
|
+
const isContract = (file) => CONTRACT_PATHS.some((p) => (p.endsWith("/") || p.endsWith("-") ? file.startsWith(p) : file === p));
|
|
33
|
+
|
|
34
|
+
/** The contract files git sees as changed, with their porcelain code (`??` untracked). */
|
|
35
|
+
function contractStatus(git) {
|
|
36
|
+
try {
|
|
37
|
+
const lines = git(["status", "--porcelain", "--untracked-files=all"]).split("\n").filter(Boolean);
|
|
38
|
+
return lines.map((line) => [line.slice(3).trim().split(" -> ").at(-1), line.slice(0, 2)]).filter(([file]) => isContract(file));
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The contract files that differ from HEAD (or are untracked), one by one.
|
|
46
|
+
* @returns {Set<string>} empty when git cannot answer
|
|
47
|
+
*/
|
|
48
|
+
export function contractChanges(projectDir, git = runner(projectDir, process.env)) {
|
|
49
|
+
return new Set(contractStatus(git).map(([file]) => file));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hasCommits = (git) => { try { git(["rev-parse", "--verify", "HEAD"]); return true; } catch { return false; } };
|
|
53
|
+
const branchOf = (git) => { try { return git(["symbolic-ref", "--short", "HEAD"]).trim(); } catch { return ""; } };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {{projectDir: string, before?: Set<string>, baseBranch?: string, env?: object}} opts
|
|
57
|
+
* `before`: the contract files already dirty BEFORE kj generated anything,
|
|
58
|
+
* tracked or not. They may carry the person's words, so they stay out.
|
|
59
|
+
* @returns {{committed: boolean, files?: string[], reason?: string}}
|
|
60
|
+
*/
|
|
61
|
+
export function commitContract({ projectDir, before = new Set(), baseBranch = "main", env = process.env }) {
|
|
62
|
+
const git = runner(projectDir, env);
|
|
63
|
+
const files = contractStatus(git).map(([file]) => file).filter((file) => !before.has(file)).sort();
|
|
64
|
+
if (files.length === 0) return { committed: false, reason: "nothing of the contract to commit" };
|
|
65
|
+
const history = hasCommits(git);
|
|
66
|
+
if (history && branchOf(git) === baseBranch) {
|
|
67
|
+
// kj never commits on the base branch, and will not switch the person's branch
|
|
68
|
+
// for them: the files are named so the commit can be made where it belongs.
|
|
69
|
+
return { committed: false, files, reason: `on the base branch '${baseBranch}', where kj never commits: create a branch and commit these generated files there (git checkout -b chore/kj-contract && git add -- ${files.join(" ")} && git commit -m "chore(kj): el contrato del método")` };
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
git(["add", "--", ...files]);
|
|
73
|
+
// --only: these paths and nothing else. What the person had staged stays staged.
|
|
74
|
+
git(["commit", "--only", "-m", history ? REGEN_MESSAGE : FRESH_MESSAGE, "--", ...files]);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { committed: false, files, reason: `git could not commit the contract: ${String(err.stderr || err.message).trim().split("\n")[0]}` };
|
|
77
|
+
}
|
|
78
|
+
return { committed: true, files };
|
|
79
|
+
}
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
const GATE = "deterministic";
|
|
12
12
|
|
|
13
|
-
const compiled = (rule) =>
|
|
13
|
+
const compiled = (rule) => {
|
|
14
|
+
if (rule.kind === "out-of-scope") return `reason: ${rule.reason}`;
|
|
15
|
+
// KJC-TSK-0973: why a judgment rule takes no condition is read next to it.
|
|
16
|
+
const why = rule.reason ? " (" + rule.reason + ")" : "";
|
|
17
|
+
return `when: ${JSON.stringify(rule.when)}${why}`;
|
|
18
|
+
};
|
|
14
19
|
|
|
15
20
|
/** How a proposed rule weakens or changes the approved one, or "" when it does not. */
|
|
16
21
|
function weakening(rule, approved) {
|
package/src/rules/compiled.js
CHANGED
|
@@ -79,7 +79,10 @@ function ruleErrors(rule) {
|
|
|
79
79
|
const notText = ["source", "text", "message"].filter((k) => k in rule && typeof rule[k] !== "string");
|
|
80
80
|
if (notText.length) errors.push(`${notText.join(", ")} must be text`);
|
|
81
81
|
if (rule.kind === OUT_OF_SCOPE) return [...errors, ...outOfScopeErrors(rule)];
|
|
82
|
-
|
|
82
|
+
// KJC-TSK-0973: any rule may say why it is compiled as it is. A judgment rule,
|
|
83
|
+
// why it takes no condition; a deterministic one, where its condition stops
|
|
84
|
+
// (what breaks the rest is not in the arguments of one call).
|
|
85
|
+
if ("reason" in rule && (typeof rule.reason !== "string" || !rule.reason.trim())) errors.push("reason is text");
|
|
83
86
|
// `examples` are tool calls: the command that runs them checks their shape (kj rules test).
|
|
84
87
|
const tools = [rule.when?.tool].flat();
|
|
85
88
|
if (tools.length === 0 || tools.some((t) => typeof t !== "string" || !t)) errors.push("when.tool names the tool: a glob, or a list where every item is one");
|